From 2aef77230afe673ec2ecec313e3a6c6284a8883a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Aug 2023 11:17:52 -0400 Subject: [PATCH 001/250] started modfiying glm --- src/neurostatslib/glm.py | 148 ++++++++++++++++++++------------------- 1 file changed, 75 insertions(+), 73 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index ae8a0b82..fadf823c 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -48,13 +48,10 @@ class GLM: def __init__( self, - spike_basis_matrix: NDArray, solver_name: str = "GradientDescent", solver_kwargs: dict = dict(), inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, ): - # (n_basis_funcs, window_size) - self.spike_basis_matrix = spike_basis_matrix self.solver_name = solver_name try: solver_args = inspect.getfullargspec(getattr(jaxopt, solver_name)).args @@ -73,6 +70,7 @@ def __init__( def fit( self, spike_data: NDArray, + X: NDArray, init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): """Fit GLM to spiking data. @@ -82,11 +80,13 @@ def fit( Parameters ---------- - spike_data : (n_neurons, n_timebins) - Spike counts arranged in a matrix. - init_params : ((n_neurons, n_basis_funcs, n_neurons), (n_neurons,)) + spike_data : + Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). + X : + Predictors, shape (n_time_bins, n_neurons, n_features) + init_params : Initial values for the spike basis coefficients and bias terms. If - None, we initialize with zeros. + None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) Raises ------ @@ -105,30 +105,25 @@ def fit( ) n_neurons, _ = spike_data.shape - n_basis_funcs, window_size = self.spike_basis_matrix.shape - - # Convolve spikes with basis functions. We drop the last sample, as - # those are the features that could be used to predict spikes in the - # next time bin - X = convolve_1d_basis(self.spike_basis_matrix, spike_data)[:, :, :-1] + n_features = X.shape[2] # Initialize parameters if init_params is None: # Ws, spike basis coeffs init_params = ( - jnp.zeros((n_neurons, n_basis_funcs, n_neurons)), + jnp.zeros((n_neurons, n_features)), # bs, bias terms jnp.zeros(n_neurons), ) - if init_params[0].ndim != 3: + if init_params[0].ndim != 2: raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_basis_funcs, n_neurons), but" + "spike basis coefficients must be of shape (n_neurons, n_features), but" f" init_params[0] has {init_params[0].ndim} dimensions!" ) if init_params[0].shape[0] != init_params[0].shape[-1]: raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_basis_funcs, n_neurons), but" + "spike basis coefficients must be of shape (n_neurons, n_features), but" f" init_params[0] has shape {init_params[0].shape}!" ) if init_params[1].ndim != 1: @@ -138,24 +133,23 @@ def fit( ) if init_params[0].shape[0] != init_params[1].shape[0]: raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_basis_funcs, n_neurons), and" + "spike basis coefficients must be of shape (n_neurons, n_features), and" "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both!" f"init_params[0]: {init_params[0].shape[0]}, init_params[1]: {init_params[1].shape[0]}" ) - if init_params[0].shape[0] != spike_data.shape[0]: + if init_params[0].shape[0] != spike_data.shape[1]: raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_basis_funcs, n_neurons), and" - "spike_data must be of shape (n_neurons, n_timebins) but n_neurons doesn't look the same in both!" - f"init_params[0]: {init_params[0].shape[0]}, spike_data: {spike_data.shape[0]}" + "spike basis coefficients must be of shape (n_neurons, n_features), and" + "spike_data must be of shape (n_time_bins, n_neurons) but n_neurons doesn't look the same in both!" + f"init_params[0]: {init_params[0].shape[0]}, spike_data: {spike_data.shape[1]}" ) def loss(params, X, y): - predicted_firing_rates = self._predict(params, X) - return self._score(predicted_firing_rates, y) + return self._score(X, y, params) # Run optimization solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) - params, state = solver.run(init_params, X=X, y=spike_data[:, window_size:]) + params, state = solver.run(init_params, X=X, y=spike_data) if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): raise ValueError( @@ -172,7 +166,7 @@ def loss(params, X, y): self.solver = solver def _predict( - self, params: Tuple[jnp.ndarray, jnp.ndarray], convolved_spike_data: NDArray + self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray ) -> jnp.ndarray: """Helper function for generating predictions. @@ -187,22 +181,22 @@ def _predict( ---------- params : ((n_neurons, n_basis_funcs, n_neurons), (n_neurons,)) Values for the spike basis coefficients and bias terms. - convolved_spike_data : (n_basis_funcs, n_neurons, n_timebins) - Spike counts convolved with some set of bases functions. + X : (n_time_bins, n_features) + The model matrix. Returns ------- - predicted_firing_rates : (n_neurons, n_timebins) + predicted_firing_rates : (n_time_bins, n_neurons) The predicted firing rates. """ Ws, bs = params return self.inverse_link_function( - jnp.einsum("nbt,nbj->nt", convolved_spike_data, Ws) + bs[:, None] + jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :] ) def _score( - self, predicted_firing_rates: NDArray, target_spikes: NDArray + self, X: NDArray, target_spikes: NDArray, params: Tuple[jnp.ndarray, jnp.ndarray] ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. @@ -213,10 +207,12 @@ def _score( Parameters ---------- - predicted_firing_rates : (n_neurons, n_timebins) - The predicted firing rates. - target_spikes : (n_neurons, n_timebins) + X : (n_time_bins, n_neurons, n_features) + The exogenous variables. + target_spikes : (n_time_bins, n_neurons ) The target spikes to compare against + params : ((n_neurons, n_features), (n_neurons,)) + Values for the spike basis coefficients and bias terms. Returns ------- @@ -243,6 +239,7 @@ def _score( ``predicted_firing_rates`` is $\lambda$ """ + predicted_firing_rates = self._predict(params, X) x = target_spikes * jnp.log(predicted_firing_rates) # this is a jax jit-friendly version of saying "put a 0 wherever # there's a NaN". we do this because NaNs result from 0*log(0) @@ -254,18 +251,41 @@ def _score( predicted_firing_rates - x + jax.scipy.special.gammaln(target_spikes + 1) ) - def predict(self, spike_data: NDArray) -> jnp.ndarray: + def check_is_fit(self): + if not hasattr(self, "spike_basis_coeff_"): + raise NotFittedError( + "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." + ) + + def check_n_neurons(self, spike_data, bs): + if spike_data.shape[1] != bs.shape[0]: + raise ValueError( + "Number of neurons must be the same during prediction and fitting! " + f"spike_data n_neurons: {spike_data.shape[1]}, " + f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" + ) + def check_n_features(self, spike_data, bs): + if spike_data.shape[1] != bs.shape[0]: + raise ValueError( + "Number of neurons must be the same during prediction and fitting! " + f"spike_data n_neurons: {spike_data.shape[1]}, " + f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" + ) + + def predict(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: """Predict firing rates based on fit parameters, for checking against existing data. Parameters ---------- - spike_data : (n_neurons, n_timebins) + X : (n_time_bins, n_neurons, n_features) + The exogenous variables. + spike_data : (n_time_bins, n_neurons) Spike counts arranged in a matrix. n_neurons must be the same as during the fitting of this GLM instance. Returns ------- - predicted_firing_rates : (n_neurons, n_timebins - window_size + 1) + predicted_firing_rates : (n_neurons, n_time_bins) The predicted firing rates. Raises @@ -285,34 +305,26 @@ def predict(self, spike_data: NDArray) -> jnp.ndarray: Simulate spikes using GLM as a recurrent network, for extrapolating into the future. """ - try: - Ws = self.spike_basis_coeff_ - except AttributeError: - raise NotFittedError( - "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." - ) + self.check_is_fit() + Ws = self.spike_basis_coeff_ bs = self.baseline_log_fr_ - if spike_data.shape[0] != bs.shape[0]: - raise ValueError( - "Number of neurons must be the same during prediction and fitting! " - f"spike_data n_neurons: {spike_data.shape[0]}, " - f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" - ) - X = convolve_1d_basis(self.spike_basis_matrix, spike_data) + self.check_n_neurons(spike_data, bs) return self._predict((Ws, bs), X) - def score(self, spike_data: NDArray) -> jnp.ndarray: + def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: """Score the predicted firing rates (based on fit) to the target spike counts. This ignores the last time point of the prediction. This computes the Poisson negative log-likehood, thus the lower the number the better, and zero isn't special (you can have a negative - score if ``spike_data > 0`` and and ``log(predicted_firing_rates) < 0`` + score if ``spike_data > 0`` and ``log(predicted_firing_rates) < 0`` Parameters ---------- - spike_data : (n_neurons, n_timebins) + X : (n_time_bins, n_neurons, n_features) + The exogenous variables. + spike_data : (n_time_bins, n_neurons) Spike counts arranged in a matrix. n_neurons must be the same as during the fitting of this GLM instance. @@ -336,14 +348,11 @@ def score(self, spike_data: NDArray) -> jnp.ndarray: """ # ignore the last time point from predict, because that corresponds to # the next time step, which we have no observed data for - predicted_firing_rates = self.predict(spike_data)[:, :-1] - if (predicted_firing_rates == 0).any(): - warnings.warn( - "predicted_firing_rates array contained zeros, this can " - "lead to infinite log-likelihood values." - ) - window_size = self.spike_basis_matrix.shape[1] - return self._score(predicted_firing_rates, spike_data[:, window_size:]) + self.check_is_fit() + Ws = self.spike_basis_coeff_ + bs = self.baseline_log_fr_ + self.check_n_neurons(spike_data, bs) + return self._score(X, spike_data, (Ws, bs)) def simulate( self, @@ -387,20 +396,12 @@ def simulate( Predict firing rates based on fit parameters, for checking against existing data. """ - try: - Ws = self.spike_basis_coeff_ - except AttributeError: - raise NotFittedError( - "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." - ) + self.check_is_fit() + + Ws = self.spike_basis_coeff_ bs = self.baseline_log_fr_ + self.check_n_neurons(init_spikes, bs) - if init_spikes.shape[0] != bs.shape[0]: - raise ValueError( - "Number of neurons must be the same during simulation and fitting! " - f"init_spikes n_neurons: {init_spikes.shape[0]}, " - f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" - ) if init_spikes.shape[1] != self.spike_basis_matrix.shape[1]: raise ValueError( "init_spikes has the wrong number of time steps!" @@ -415,6 +416,7 @@ def scan_fn(spikes, key): X = convolve_1d_basis(self.spike_basis_matrix, spikes) fr = self._predict((Ws, bs), X).squeeze(-1) new_spikes = jax.random.poisson(key, fr) + # this remains always of the same shape concat_spikes = jnp.column_stack((spikes[:, 1:], new_spikes)) return concat_spikes, new_spikes From f05471682b0e1c47ed594fdf04459706e87bd689 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Aug 2023 11:47:36 -0400 Subject: [PATCH 002/250] set up required variables --- src/neurostatslib/glm.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index fadf823c..7d10701e 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -359,6 +359,10 @@ def simulate( random_key: jax.random.PRNGKeyArray, n_timesteps: int, init_spikes: NDArray, + coupling_basis_matrix: NDArray, + X_input: NDArray, + index_coupling: NDArray[int], + index_input: NDArray[int], ) -> jnp.ndarray: """Simulate spikes using GLM as a recurrent network, for extrapolating into the future. @@ -373,7 +377,16 @@ def simulate( forward simulation. ``n_neurons`` must be the same as during the fitting of this GLM instance and ``window_size`` must be the same as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``) - + coupling_basis_matrix: + Coupling and auto-correlation filter basis matrix. Shape (n_neurons, n_basis_coupling) + X_input: + Part of the exogenous matrix that captures the external inputs (currents convolved with a basis, + images convolved with basis, position time series evaluated in a basis). + Shape (n_timesteps, n_basis_input). + index_coupling: + Indices of the exogenous corresponding to the coupling filters, must be 0 <= index_coupling <= n_features - 1 + index_input: + Indices of the exogenous corresponding to the feedforward inputs, must be 0 <= index_input <= n_features - 1 Returns ------- simulated_spikes : (n_neurons, n_timesteps) @@ -395,6 +408,10 @@ def simulate( predict Predict firing rates based on fit parameters, for checking against existing data. + Notes + ----- + n_basis_input + n_basis_coupling = self.spike_basis_coeff_.shape[1] + """ self.check_is_fit() @@ -413,6 +430,11 @@ def simulate( def scan_fn(spikes, key): # (n_neurons, n_basis_funcs, 1) + # new syntax with equivalent output + # X = jnp.transpose( + # convolve_1d_trials(self.spike_basis_matrix.T, spikes.T[None, :, :])[0], + # (1, 2, 0), + # ) X = convolve_1d_basis(self.spike_basis_matrix, spikes) fr = self._predict((Ws, bs), X).squeeze(-1) new_spikes = jax.random.poisson(key, fr) From 84b7d58117bcc48faaed7d12d993cc6b1ed24fa0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 8 Aug 2023 16:12:58 -0400 Subject: [PATCH 003/250] fit debugged --- src/neurostatslib/glm.py | 51 ++++++++++++++++++++-------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 7d10701e..19e1fa98 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -104,7 +104,7 @@ def fit( "spike_data must be two-dimensional, with shape (n_neurons, n_timebins)" ) - n_neurons, _ = spike_data.shape + _, n_neurons = spike_data.shape n_features = X.shape[2] # Initialize parameters @@ -121,11 +121,7 @@ def fit( "spike basis coefficients must be of shape (n_neurons, n_features), but" f" init_params[0] has {init_params[0].ndim} dimensions!" ) - if init_params[0].shape[0] != init_params[0].shape[-1]: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), but" - f" init_params[0] has shape {init_params[0].shape}!" - ) + if init_params[1].ndim != 1: raise ValueError( "bias terms must be of shape (n_neurons,) but init_params[0] have" @@ -139,9 +135,9 @@ def fit( ) if init_params[0].shape[0] != spike_data.shape[1]: raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), and" - "spike_data must be of shape (n_time_bins, n_neurons) but n_neurons doesn't look the same in both!" - f"init_params[0]: {init_params[0].shape[0]}, spike_data: {spike_data.shape[1]}" + "spike basis coefficients must be of shape (n_neurons, n_features), and " + "spike_data must be of shape (n_time_bins, n_neurons) but n_neurons doesn't look the same in both! " + f"init_params[0]: {init_params[0].shape[1]}, spike_data: {spike_data.shape[1]}" ) def loss(params, X, y): @@ -179,9 +175,9 @@ def _predict( Parameters ---------- - params : ((n_neurons, n_basis_funcs, n_neurons), (n_neurons,)) + params : ((n_neurons, n_features), (n_neurons,)) Values for the spike basis coefficients and bias terms. - X : (n_time_bins, n_features) + X : (n_time_bins, n_neurons, n_features) The model matrix. Returns @@ -360,9 +356,7 @@ def simulate( n_timesteps: int, init_spikes: NDArray, coupling_basis_matrix: NDArray, - X_input: NDArray, - index_coupling: NDArray[int], - index_input: NDArray[int], + X_input: NDArray ) -> jnp.ndarray: """Simulate spikes using GLM as a recurrent network, for extrapolating into the future. @@ -372,21 +366,18 @@ def simulate( jax PRNGKey to seed simulation with. n_timesteps Number of time steps to simulate. - init_spikes : (n_neurons, window_size) + init_spikes : Spike counts arranged in a matrix. These are used to jump start the forward simulation. ``n_neurons`` must be the same as during the fitting of this GLM instance and ``window_size`` must be the same - as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``) + as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``), shape (n_neurons, window_size) coupling_basis_matrix: Coupling and auto-correlation filter basis matrix. Shape (n_neurons, n_basis_coupling) X_input: Part of the exogenous matrix that captures the external inputs (currents convolved with a basis, images convolved with basis, position time series evaluated in a basis). Shape (n_timesteps, n_basis_input). - index_coupling: - Indices of the exogenous corresponding to the coupling filters, must be 0 <= index_coupling <= n_features - 1 - index_input: - Indices of the exogenous corresponding to the feedforward inputs, must be 0 <= index_input <= n_features - 1 + Returns ------- simulated_spikes : (n_neurons, n_timesteps) @@ -419,27 +410,37 @@ def simulate( bs = self.baseline_log_fr_ self.check_n_neurons(init_spikes, bs) - if init_spikes.shape[1] != self.spike_basis_matrix.shape[1]: + if X_input.shape[2] + coupling_basis_matrix.shape[1]*bs.shape[0] != Ws.shape[1]: + raise ValueError("The number of feed forward input features" + "and the number of recurrent features must add up to" + "the overall model features." + f"The total number of feature of the model is {Ws.shape[1]}. {X_input.shape[1]} " + f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " + f"provided instead.") + + if init_spikes.shape[1] != coupling_basis_matrix.shape[1]: raise ValueError( "init_spikes has the wrong number of time steps!" f"init_spikes time steps: {init_spikes.shape[1]}, " - f"spike_basis_matrix window size: {self.spike_basis_matrix.shape[1]}" + f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" ) subkeys = jax.random.split(random_key, num=n_timesteps) - def scan_fn(spikes, key): + def scan_fn(data, key): # (n_neurons, n_basis_funcs, 1) # new syntax with equivalent output # X = jnp.transpose( # convolve_1d_trials(self.spike_basis_matrix.T, spikes.T[None, :, :])[0], # (1, 2, 0), # ) - X = convolve_1d_basis(self.spike_basis_matrix, spikes) + spikes, chunk = data + X = convolve_1d_basis(coupling_basis_matrix, spikes) + X = jnp.hstack((X, X_input[chunk:chunk+1, :])) fr = self._predict((Ws, bs), X).squeeze(-1) new_spikes = jax.random.poisson(key, fr) # this remains always of the same shape - concat_spikes = jnp.column_stack((spikes[:, 1:], new_spikes)) + concat_spikes = jnp.column_stack((spikes[:, 1:], new_spikes)), chunk + 1 return concat_spikes, new_spikes _, simulated_spikes = jax.lax.scan(scan_fn, init_spikes, subkeys) From e7ef538dca8996eb6a88861495fe3bf90f2b590c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 9 Aug 2023 12:05:40 -0400 Subject: [PATCH 004/250] fixed conv --- tests/tmp.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/tmp.py diff --git a/tests/tmp.py b/tests/tmp.py new file mode 100644 index 00000000..e69de29b From 938fe790df6393098368d811b9119342a51e09c2 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 9 Aug 2023 12:08:46 -0400 Subject: [PATCH 005/250] fixed randomization --- src/neurostatslib/glm.py | 32 +++++++++++++++++--------------- tests/tmp.py | 0 2 files changed, 17 insertions(+), 15 deletions(-) delete mode 100644 tests/tmp.py diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 19e1fa98..256ba9c1 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -370,7 +370,7 @@ def simulate( Spike counts arranged in a matrix. These are used to jump start the forward simulation. ``n_neurons`` must be the same as during the fitting of this GLM instance and ``window_size`` must be the same - as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``), shape (n_neurons, window_size) + as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``), shape (window_size,n_neurons) coupling_basis_matrix: Coupling and auto-correlation filter basis matrix. Shape (n_neurons, n_basis_coupling) X_input: @@ -404,6 +404,7 @@ def simulate( n_basis_input + n_basis_coupling = self.spike_basis_coeff_.shape[1] """ + from jax.experimental import host_callback self.check_is_fit() Ws = self.spike_basis_coeff_ @@ -418,31 +419,32 @@ def simulate( f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " f"provided instead.") - if init_spikes.shape[1] != coupling_basis_matrix.shape[1]: + if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( "init_spikes has the wrong number of time steps!" f"init_spikes time steps: {init_spikes.shape[1]}, " f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" ) + subkeys = jax.random.split(random_key, num=n_timesteps) def scan_fn(data, key): - # (n_neurons, n_basis_funcs, 1) - # new syntax with equivalent output - # X = jnp.transpose( - # convolve_1d_trials(self.spike_basis_matrix.T, spikes.T[None, :, :])[0], - # (1, 2, 0), - # ) spikes, chunk = data - X = convolve_1d_basis(coupling_basis_matrix, spikes) - X = jnp.hstack((X, X_input[chunk:chunk+1, :])) - fr = self._predict((Ws, bs), X).squeeze(-1) - new_spikes = jax.random.poisson(key, fr) + conv_spk = jnp.transpose( + jnp.array(convolve_1d_basis(coupling_basis_matrix.T, spikes.T)), + (2, 0, 1) + ) + slice = jax.lax.dynamic_slice( + X_input, (chunk, 0, 0), (1, X_input.shape[1], X_input.shape[2]) + ) + X = jnp.concatenate([conv_spk] * spikes.shape[1] + [slice], axis=2) + firing_rate = self._predict((Ws, bs), X) + new_spikes = jax.random.poisson(key, firing_rate) # this remains always of the same shape - concat_spikes = jnp.column_stack((spikes[:, 1:], new_spikes)), chunk + 1 + concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 return concat_spikes, new_spikes - _, simulated_spikes = jax.lax.scan(scan_fn, init_spikes, subkeys) + _, simulated_spikes = jax.lax.scan(scan_fn, (init_spikes,0), subkeys) - return simulated_spikes.T + return jnp.squeeze(simulated_spikes, axis=1) diff --git a/tests/tmp.py b/tests/tmp.py deleted file mode 100644 index e69de29b..00000000 From cff79e805ad23e4ec0fb47033f6801c4478c38d5 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 9 Aug 2023 16:37:59 -0400 Subject: [PATCH 006/250] test and compare to sklearn --- tests/basic_test.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 tests/basic_test.py diff --git a/tests/basic_test.py b/tests/basic_test.py new file mode 100644 index 00000000..b5ad64cb --- /dev/null +++ b/tests/basic_test.py @@ -0,0 +1,41 @@ +from neurostatslib.glm import GLM +from sklearn.linear_model import PoissonRegressor +import numpy as np +import matplotlib.pylab as plt +from scipy.optimize import minimize +from jax import grad +import scipy.stats as sts +import jax.numpy as jnp + + + +np.random.seed(100) + +nn, nt, ws, nb,nbi = 2, 15000, 30, 5, 0 +X = np.random.normal(size=(nt, nn, nb*nn+nbi)) +W_true = np.random.normal(size=(nn, nb*nn+nbi)) * 0.8 +b_true = -3*np.ones(nn) +firing_rate = np.exp(np.einsum("ik,tik->ti", W_true, X) + b_true[None, :]) +spikes = np.random.poisson(firing_rate) + +# check likelihood +poiss_rand = sts.poisson(firing_rate) +mean_ll = poiss_rand.logpmf(spikes).mean() + +# SKL FIT +weights_skl = np.zeros((nn, nb*nn+nbi)) +b_skl = np.zeros(nn) +pred_skl = np.zeros((nt,nn)) +for k in range(nn): + model_skl = PoissonRegressor(alpha=0,tol=10**-8,solver="lbfgs",max_iter=1000,fit_intercept=True) + model_skl.fit(X[:,k,:], spikes[:, k]) + weights_skl[k] = model_skl.coef_ + b_skl[k] = model_skl.intercept_ + pred_skl[:, k] = model_skl.predict(X[:, k,:]) + +model_jax = GLM(solver_name="BFGS", solver_kwargs={'tol':10**-8, 'maxiter':1000},inverse_link_function=jnp.exp) +model_jax.fit(spikes, X) +mean_ll_jax = model_jax._score(X, spikes, (W_true, b_true)) +firing_rate_jax = model_jax._predict((W_true, b_true),X) + +print('jax pars - skl pars:', np.max(np.abs(model_jax.spike_basis_coeff_-weights_skl))) \ No newline at end of file From d51317dcf3b5ef5a4142323a7e50b9e6d0206e70 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 9 Aug 2023 17:43:51 -0400 Subject: [PATCH 007/250] add test_dandi --- tests/test_dandi.py | 87 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 tests/test_dandi.py diff --git a/tests/test_dandi.py b/tests/test_dandi.py new file mode 100644 index 00000000..1bbab8cb --- /dev/null +++ b/tests/test_dandi.py @@ -0,0 +1,87 @@ +import pynwb + +from pynwb import NWBHDF5IO, TimeSeries + +from dandi.dandiapi import DandiAPIClient +import pynapple as nap +import numpy as np +import jax.numpy as jnp +import fsspec +from fsspec.implementations.cached import CachingFileSystem + +import pynwb +import h5py + +from matplotlib.pylab import * + +##################################### +# Dandi +##################################### + +# ecephys, Buzsaki Lab (15.2 GB) +dandiset_id, filepath = "000582", "sub-10073/sub-10073_ses-17010302_behavior+ecephys.nwb" + + +with DandiAPIClient() as client: + asset = client.get_dandiset(dandiset_id, "draft").get_asset_by_path(filepath) + s3_url = asset.get_content_url(follow_redirects=1, strip_query=True) + + + + +# first, create a virtual filesystem based on the http protocol +fs=fsspec.filesystem("http") + +# create a cache to save downloaded data to disk (optional) +fs = CachingFileSystem( + fs=fs, + cache_storage="nwb-cache", # Local folder for the cache +) + +# next, open the file +file = h5py.File(fs.open(s3_url, "rb")) +io = pynwb.NWBHDF5IO(file=file, load_namespaces=True) + + +##################################### +# Pynapple +##################################### + +nwb = nap.NWBFile(io.read()) + +units = nwb["units"] + +position = nwb["SpatialSeriesLED1"] + +tc, binsxy = nap.compute_2d_tuning_curves(units, position, 15) + + +figure() +for i in tc.keys(): + subplot(3,3,i+1) + imshow(tc[i]) +#show() + +figure() +for i in units.keys(): + subplot(3,3,i+1) + plot(position['x'], position['y']) + spk_pos = units[i].value_from(position) + plot(spk_pos["x"], spk_pos["y"], 'o', color = 'red', markersize = 1, alpha = 0.5) + +show() + + +##################################### +# GLM +##################################### +# create the binning +t0 = position.time_support.start[0] +tend = position.time_support.end[0] +ts = np.arange(t0-0.01, tend+0.01, 0.02) +binning = nap.IntervalSet(start=ts[:-1], end=ts[1:], time_units='s') + +# bin and convert to jax array +counts = jnp.asarray(units.count(ep=binning)) +position_binned = jnp.asarray(position.restrict(binning)) + From a2dc451c9baa66f437f6e337a0ce2b185475cade Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 11 Aug 2023 20:29:01 -0400 Subject: [PATCH 008/250] checked fit --- src/neurostatslib/glm.py | 69 ++++++++++++++++++++-------------------- tests/basic_test.py | 2 +- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 556a6e24..f2298646 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -47,10 +47,10 @@ class GLM: """ def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, ): self.solver_name = solver_name try: @@ -68,10 +68,10 @@ def __init__( self.inverse_link_function = inverse_link_function def fit( - self, - spike_data: NDArray, - X: NDArray, - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + self, + X: NDArray, + spike_data: NDArray, + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): """Fit GLM to spiking data. @@ -80,10 +80,10 @@ def fit( Parameters ---------- - spike_data : - Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). X : Predictors, shape (n_time_bins, n_neurons, n_features) + spike_data : + Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). init_params : Initial values for the spike basis coefficients and bias terms. If None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) @@ -113,7 +113,7 @@ def fit( init_params = ( jnp.zeros((n_neurons, n_features)), # bs, bias terms - jnp.zeros(n_neurons), + jnp.log(jnp.mean(spike_data, axis=0)) ) if init_params[0].ndim != 2: @@ -242,7 +242,7 @@ def _score( """ # Avoid the edge-case of 0*log(0), much faster than # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) x = target_spikes * jnp.log(predicted_firing_rates) # see above for derivation of this. return - jnp.mean( @@ -262,6 +262,7 @@ def check_n_neurons(self, spike_data, bs): f"spike_data n_neurons: {spike_data.shape[1]}, " f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" ) + def check_n_features(self, spike_data, bs): if spike_data.shape[1] != bs.shape[0]: raise ValueError( @@ -270,16 +271,13 @@ def check_n_features(self, spike_data, bs): f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" ) - def predict(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: + def predict(self, X: NDArray) -> jnp.ndarray: """Predict firing rates based on fit parameters, for checking against existing data. Parameters ---------- X : (n_time_bins, n_neurons, n_features) The exogenous variables. - spike_data : (n_time_bins, n_neurons) - Spike counts arranged in a matrix. n_neurons must be the same as - during the fitting of this GLM instance. Returns ------- @@ -306,7 +304,7 @@ def predict(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: self.check_is_fit() Ws = self.spike_basis_coeff_ bs = self.baseline_log_fr_ - self.check_n_neurons(spike_data, bs) + self.check_n_neurons(X, bs) return self._predict((Ws, bs), X) def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: @@ -329,7 +327,7 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: Returns ------- score : (1,) - The Poisson negative log-likehood + The Poisson log-likehood Raises ------ @@ -354,12 +352,12 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: return self._score(X, spike_data, (Ws, bs)) - norm_factor def simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_spikes: NDArray, - coupling_basis_matrix: NDArray, - X_input: NDArray + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_spikes: NDArray, + coupling_basis_matrix: NDArray, + feedforward_input: NDArray ) -> jnp.ndarray: """Simulate spikes using GLM as a recurrent network, for extrapolating into the future. @@ -376,7 +374,7 @@ def simulate( as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``), shape (window_size,n_neurons) coupling_basis_matrix: Coupling and auto-correlation filter basis matrix. Shape (n_neurons, n_basis_coupling) - X_input: + feedforward_input: Part of the exogenous matrix that captures the external inputs (currents convolved with a basis, images convolved with basis, position time series evaluated in a basis). Shape (n_timesteps, n_basis_input). @@ -414,11 +412,11 @@ def simulate( bs = self.baseline_log_fr_ self.check_n_neurons(init_spikes, bs) - if X_input.shape[2] + coupling_basis_matrix.shape[1]*bs.shape[0] != Ws.shape[1]: + if feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] != Ws.shape[1]: raise ValueError("The number of feed forward input features" "and the number of recurrent features must add up to" "the overall model features." - f"The total number of feature of the model is {Ws.shape[1]}. {X_input.shape[1]} " + f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " f"provided instead.") @@ -429,25 +427,26 @@ def simulate( f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" ) - subkeys = jax.random.split(random_key, num=n_timesteps) - def scan_fn(data, key): + def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray) -> Tuple[Tuple[NDArray, int], NDArray]: spikes, chunk = data conv_spk = jnp.transpose( - jnp.array(convolve_1d_basis(coupling_basis_matrix.T, spikes.T)), - (2, 0, 1) + convolve_1d_trials(coupling_basis_matrix.T, spikes.T[None, :, :])[0], + (1, 2, 0), ) - slice = jax.lax.dynamic_slice( - X_input, (chunk, 0, 0), (1, X_input.shape[1], X_input.shape[2]) + input_slice = jax.lax.dynamic_slice( + feedforward_input, + (chunk, 0, 0), + (1, feedforward_input.shape[1], feedforward_input.shape[2]) ) - X = jnp.concatenate([conv_spk] * spikes.shape[1] + [slice], axis=2) + X = jnp.concatenate([conv_spk] * spikes.shape[1] + [input_slice], axis=2) firing_rate = self._predict((Ws, bs), X) new_spikes = jax.random.poisson(key, firing_rate) # this remains always of the same shape concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 return concat_spikes, new_spikes - _, simulated_spikes = jax.lax.scan(scan_fn, (init_spikes,0), subkeys) + _, simulated_spikes = jax.lax.scan(scan_fn, (init_spikes, 0), subkeys) return jnp.squeeze(simulated_spikes, axis=1) diff --git a/tests/basic_test.py b/tests/basic_test.py index b5ad64cb..432dd689 100644 --- a/tests/basic_test.py +++ b/tests/basic_test.py @@ -34,7 +34,7 @@ pred_skl[:, k] = model_skl.predict(X[:, k,:]) model_jax = GLM(solver_name="BFGS", solver_kwargs={'tol':10**-8, 'maxiter':1000},inverse_link_function=jnp.exp) -model_jax.fit(spikes, X) +model_jax.fit(X, spikes) mean_ll_jax = model_jax._score(X, spikes, (W_true, b_true)) firing_rate_jax = model_jax._predict((W_true, b_true),X) From 86148cd7b1724a6251125c4936a3109ab435fc94 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 18 Aug 2023 15:14:17 -0400 Subject: [PATCH 009/250] glm class split in a base class and a unregularized GLM --- src/neurostatslib/glm.py | 287 +++++++++++++++++++++++--------- src/neurostatslib/model_base.py | 158 ++++++++++++++++++ 2 files changed, 364 insertions(+), 81 deletions(-) create mode 100644 src/neurostatslib/model_base.py diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index f2298646..8899d6a9 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,8 +1,8 @@ """GLM core module """ import inspect -import warnings -from typing import Callable, Optional, Tuple +import abc +from typing import Callable, Optional, Tuple, Literal import jax import jax.numpy as jnp @@ -11,9 +11,10 @@ from sklearn.exceptions import NotFittedError from .utils import convolve_1d_trials +from .model_base import Model -class GLM: +class GLMBase(Model, abc.ABC): """Generalized Linear Model for neural responses. No stimulus / external variables, only connections to other neurons. @@ -51,6 +52,8 @@ def __init__( solver_name: str = "GradientDescent", solver_kwargs: dict = dict(), inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + **kwargs ): self.solver_name = solver_name try: @@ -59,20 +62,30 @@ def __init__( raise AttributeError( f"module jaxopt has no attribute {solver_name}, pick a different solver!" ) + for k in solver_kwargs.keys(): if k not in solver_args: raise NameError( f"kwarg {k} in solver_kwargs is not a kwarg for jaxopt.{solver_name}!" ) + + if score_type not in ['log-likelihood', 'pseudo-r2']: + raise NotImplementedError("Scoring method not implemented. " + f"score_type must be either 'log-likelihood', or 'pseudo-r2'." + f" {score_type} provided instead.") + self.score_type = score_type self.solver_kwargs = solver_kwargs self.inverse_link_function = inverse_link_function + # set additional kwargs e.g. regularization hyperparameters and so on... + super().__init__(**kwargs) + @abc.abstractmethod def fit( self, X: NDArray, spike_data: NDArray, init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, - ): + ) -> None: """Fit GLM to spiking data. Following scikit-learn API, the solutions are stored as attributes @@ -99,67 +112,7 @@ def fit( an invalid solution. Try tuning optimization hyperparameters. """ - if spike_data.ndim != 2: - raise ValueError( - "spike_data must be two-dimensional, with shape (n_neurons, n_timebins)" - ) - - _, n_neurons = spike_data.shape - n_features = X.shape[2] - - # Initialize parameters - if init_params is None: - # Ws, spike basis coeffs - init_params = ( - jnp.zeros((n_neurons, n_features)), - # bs, bias terms - jnp.log(jnp.mean(spike_data, axis=0)) - ) - - if init_params[0].ndim != 2: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), but" - f" init_params[0] has {init_params[0].ndim} dimensions!" - ) - - if init_params[1].ndim != 1: - raise ValueError( - "bias terms must be of shape (n_neurons,) but init_params[0] have" - f"{init_params[1].ndim} dimensions!" - ) - if init_params[0].shape[0] != init_params[1].shape[0]: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), and" - "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both!" - f"init_params[0]: {init_params[0].shape[0]}, init_params[1]: {init_params[1].shape[0]}" - ) - if init_params[0].shape[0] != spike_data.shape[1]: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), and " - "spike_data must be of shape (n_time_bins, n_neurons) but n_neurons doesn't look the same in both! " - f"init_params[0]: {init_params[0].shape[1]}, spike_data: {spike_data.shape[1]}" - ) - - def loss(params, X, y): - return -self._score(X, y, params) - - # Run optimization - solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) - params, state = solver.run(init_params, X=X, y=spike_data) - - if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): - raise ValueError( - "Solver returned at least one NaN parameter, so solution is invalid!" - " Try tuning optimization hyperparameters." - ) - # Store parameters - self.spike_basis_coeff_ = params[0] - self.baseline_log_fr_ = params[1] - # note that this will include an error value, which is not the same as - # the output of loss. I believe it's the output of - # solver.l2_optimality_error - self.solver_state = state - self.solver = solver + pass def _predict( self, @@ -201,7 +154,7 @@ def _score( ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. - This computes the Poisson negative log-likehood. + This computes the Poisson negative log-likelihood. Note that you can end up with infinities in here if there are zeros in ``predicted_firing_rates``. We raise a warning in that case. @@ -245,10 +198,55 @@ def _score( predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) x = target_spikes * jnp.log(predicted_firing_rates) # see above for derivation of this. - return - jnp.mean( + return jnp.mean( predicted_firing_rates - x ) + def _residual_deviance(self, predicted_rate, y): + """Compute the residual deviance for a Poisson model. + + Parameters + ---------- + X: + The predictors. Shape (n_time_bins, n_neurons, n_features). + y: + The spike counts. Shape (n_time_bins, n_neurons). + + Returns + ------- + The residual deviance of the model. + """ + # this takes care of 0s in the log + ratio = jnp.clip(y / predicted_rate, self.FLOAT_EPS, jnp.inf) + resid_dev = y * jnp.log(ratio) - (y - predicted_rate) + return resid_dev + + def _pseudo_r2(self, X, y): + """Pseudo-R2 calculation. + + Parameters + ---------- + X: + The predictors. Shape (n_time_bins, n_neurons, n_features). + y: + The spike counts. Shape (n_time_bins, n_neurons). + + Returns + ------- + : + The pseudo-r2 of the model. + """ + mu = self.predict(X) + res_dev_t = self._residual_deviance(mu, y) + resid_deviance = jnp.sum(res_dev_t ** 2) + + null_mu = jnp.ones(y.shape) * y.sum() / y.size + null_dev_t = self._residual_deviance(null_mu, y) + null_deviance = jnp.sum(null_dev_t ** 2) + + return (null_deviance - resid_deviance) / null_deviance + + def check_is_fit(self): if not hasattr(self, "spike_basis_coeff_"): raise NotFittedError( @@ -308,13 +306,29 @@ def predict(self, X: NDArray) -> jnp.ndarray: return self._predict((Ws, bs), X) def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: - """Score the predicted firing rates (based on fit) to the target spike counts. + r"""Score the predicted firing rates (based on fit) to the target spike counts. This ignores the last time point of the prediction. - This computes the Poisson negative log-likehood, thus the lower the - number the better, and zero isn't special (you can have a negative - score if ``spike_data > 0`` and ``log(predicted_firing_rates) < 0`` + This computes the Poisson mean log-likelihood or the pseudo-R2, thus the higher the + number the better. + + The formula for the mean log-likelihood is the following, + + $$ + \text{LL}(\hat{\lambda} | y) = \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} + [y_{tn} \log(\hat{\lambda}_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] + $$ + + The pseudo-R2 can be computed as follows, + + $$ + \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) + - \log \text{LL}(\bar{\lambda}| y)}, + $$ + + where $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate + of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. Parameters ---------- @@ -340,6 +354,14 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: UserWarning If there are any zeros in ``self.predict(spike_data)``, since this will likely lead to infinite log-likelihood values being returned. + Notes + ----- + + The log-likelihood is not on a standard scale, its value is influenced by many factors, + among which the number of model parameters. The log-likelihood can assume both positive + and negative values. + + The pseudo-R2 is a standardized metric and assumes values between 0 and 1. """ # ignore the last time point from predict, because that corresponds to @@ -348,8 +370,14 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: Ws = self.spike_basis_coeff_ bs = self.baseline_log_fr_ self.check_n_neurons(spike_data, bs) - norm_factor = jnp.mean(jax.scipy.special.gammaln(spike_data + 1)) - return self._score(X, spike_data, (Ws, bs)) - norm_factor + if self.score_type == "log-likelihood": + score = -(self._score(X, spike_data, (Ws, bs)) + jax.scipy.special.gammaln(spike_data + 1).mean()) + elif self.score_type == "pseudo-r2": + score = self._pseudo_r2(X, spike_data) + else: + # this should happen only if one manually set score_type + raise NotImplementedError(f"Scoring method {self.score_type} not implemented!") + return score def simulate( self, @@ -377,7 +405,7 @@ def simulate( feedforward_input: Part of the exogenous matrix that captures the external inputs (currents convolved with a basis, images convolved with basis, position time series evaluated in a basis). - Shape (n_timesteps, n_basis_input). + Shape (n_timesteps, n_neurons, n_basis_input). Returns ------- @@ -423,18 +451,17 @@ def simulate( if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( "init_spikes has the wrong number of time steps!" - f"init_spikes time steps: {init_spikes.shape[1]}, " - f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" + f"init_spikes time steps: {init_spikes.shape[0]}, " + f"spike_basis_matrix window size: {coupling_basis_matrix.shape[0]}" ) - subkeys = jax.random.split(random_key, num=n_timesteps) + #subkeys = jax.random.split(random_key[0], num=n_timesteps) + subkeys = jax.random.split(random_key) def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray) -> Tuple[Tuple[NDArray, int], NDArray]: spikes, chunk = data - conv_spk = jnp.transpose( - convolve_1d_trials(coupling_basis_matrix.T, spikes.T[None, :, :])[0], - (1, 2, 0), - ) + conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] + input_slice = jax.lax.dynamic_slice( feedforward_input, (chunk, 0, 0), @@ -442,6 +469,7 @@ def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray) -> Tuple[Tu ) X = jnp.concatenate([conv_spk] * spikes.shape[1] + [input_slice], axis=2) firing_rate = self._predict((Ws, bs), X) + #key = jnp.squeeze(jax.lax.dynamic_slice(random_key, (chunk, 0), (1, random_key.shape[1]))) new_spikes = jax.random.poisson(key, firing_rate) # this remains always of the same shape concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 @@ -450,3 +478,100 @@ def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray) -> Tuple[Tu _, simulated_spikes = jax.lax.scan(scan_fn, (init_spikes, 0), subkeys) return jnp.squeeze(simulated_spikes, axis=1) + +class GLM(GLMBase): + + + def fit( + self, + X: NDArray, + spike_data: NDArray, + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + ): + """Fit GLM to spiking data. + + Following scikit-learn API, the solutions are stored as attributes + ``spike_basis_coeff_`` and ``baseline_log_fr``. + + Parameters + ---------- + X : + Predictors, shape (n_time_bins, n_neurons, n_features) + spike_data : + Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). + init_params : + Initial values for the spike basis coefficients and bias terms. If + None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) + + Raises + ------ + ValueError + If spike_data is not two-dimensional. + ValueError + If shapes of init_params are not correct. + ValueError + If solver returns at least one NaN parameter, which means it found + an invalid solution. Try tuning optimization hyperparameters. + + """ + if spike_data.ndim != 2: + raise ValueError( + "spike_data must be two-dimensional, with shape (n_neurons, n_timebins)" + ) + + _, n_neurons = spike_data.shape + n_features = X.shape[2] + + # Initialize parameters + if init_params is None: + # Ws, spike basis coeffs + init_params = ( + jnp.zeros((n_neurons, n_features)), + # bs, bias terms + jnp.log(jnp.mean(spike_data, axis=0)) + ) + + if init_params[0].ndim != 2: + raise ValueError( + "spike basis coefficients must be of shape (n_neurons, n_features), but" + f" init_params[0] has {init_params[0].ndim} dimensions!" + ) + + if init_params[1].ndim != 1: + raise ValueError( + "bias terms must be of shape (n_neurons,) but init_params[0] have" + f"{init_params[1].ndim} dimensions!" + ) + if init_params[0].shape[0] != init_params[1].shape[0]: + raise ValueError( + "spike basis coefficients must be of shape (n_neurons, n_features), and" + "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both!" + f"init_params[0]: {init_params[0].shape[0]}, init_params[1]: {init_params[1].shape[0]}" + ) + if init_params[0].shape[0] != spike_data.shape[1]: + raise ValueError( + "spike basis coefficients must be of shape (n_neurons, n_features), and " + "spike_data must be of shape (n_time_bins, n_neurons) but n_neurons doesn't look the same in both! " + f"init_params[0]: {init_params[0].shape[1]}, spike_data: {spike_data.shape[1]}" + ) + + def loss(params, X, y): + return -self._score(X, y, params) + + # Run optimization + solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) + params, state = solver.run(init_params, X=X, y=spike_data) + + if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): + raise ValueError( + "Solver returned at least one NaN parameter, so solution is invalid!" + " Try tuning optimization hyperparameters." + ) + # Store parameters + self.spike_basis_coeff_ = params[0] + self.baseline_log_fr_ = params[1] + # note that this will include an error value, which is not the same as + # the output of loss. I believe it's the output of + # solver.l2_optimality_error + self.solver_state = state + self.solver = solver \ No newline at end of file diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py new file mode 100644 index 00000000..9c1c6989 --- /dev/null +++ b/src/neurostatslib/model_base.py @@ -0,0 +1,158 @@ +"""Abstract class for models. + +Inheriting this class will result in compatibility with sci-kit learn pipelines. +""" + +from collections import defaultdict +import abc + +import inspect +import warnings +from typing import Tuple + +import jax.numpy as jnp +from numpy.typing import NDArray + + +class Model(abc.ABC): + FLOAT_EPS = jnp.finfo(jnp.float32).eps + + def __init__(self, **kwargs): + for key in kwargs: + setattr(self, key, kwargs[key]) + + def get_params(self, deep=True): + """ + from scikit-learn, get parameters by inspecting init + Parameters + ---------- + deep + + Returns + ------- + + """ + out = dict() + for key in self._get_param_names(): + value = getattr(self, key) + if deep and hasattr(value, "get_params") and not isinstance(value, type): + deep_items = value.get_params().items() + out.update((key + "__" + k, val) for k, val in deep_items) + out[key] = value + return out + + def set_params(self, **params): + """Set the parameters of this estimator. + + The method works on simple estimators as well as on nested objects + (such as :class:`~sklearn.pipeline.Pipeline`). The latter have + parameters of the form ``__`` so that it's + possible to update each component of a nested object. + + Parameters + ---------- + **params : dict + Estimator parameters. + + Returns + ------- + self : estimator instance + Estimator instance. + """ + if not params: + # Simple optimization to gain speed (inspect is slow) + return self + valid_params = self.get_params(deep=True) + + nested_params = defaultdict(dict) # grouped by prefix + for key, value in params.items(): + key, delim, sub_key = key.partition("__") + if key not in valid_params: + local_valid_params = self._get_param_names() + raise ValueError( + f"Invalid parameter {key!r} for estimator {self}. " + f"Valid parameters are: {local_valid_params!r}." + ) + + if delim: + nested_params[key][sub_key] = value + else: + setattr(self, key, value) + valid_params[key] = value + + for key, sub_params in nested_params.items(): + # TODO(1.4): remove specific handling of "base_estimator". + # The "base_estimator" key is special. It was deprecated and + # renamed to "estimator" for several estimators. This means we + # need to translate it here and set sub-parameters on "estimator", + # but only if the user did not explicitly set a value for + # "base_estimator". + if ( + key == "base_estimator" + and valid_params[key] == "deprecated" + and self.__module__.startswith("sklearn.") + ): + warnings.warn( + ( + f"Parameter 'base_estimator' of {self.__class__.__name__} is" + " deprecated in favor of 'estimator'. See" + f" {self.__class__.__name__}'s docstring for more details." + ), + FutureWarning, + stacklevel=2, + ) + key = "estimator" + valid_params[key].set_params(**sub_params) + + return self + + @classmethod + def _get_param_names(cls): + """Get parameter names for the estimator""" + # fetch the constructor or the original constructor before + # deprecation wrapping if any + init = getattr(cls.__init__, "deprecated_original", cls.__init__) + if init is object.__init__: + # No explicit constructor to introspect + return [] + + # introspect the constructor arguments to find the model parameters + # to represent + init_signature = inspect.signature(init) + # Consider the constructor parameters excluding 'self' + parameters = [ + p + for p in init_signature.parameters.values() + if p.name != "self" and p.kind != p.VAR_KEYWORD + ] + for p in parameters: + if p.kind == p.VAR_POSITIONAL: + raise RuntimeError( + "GLM estimators should always " + "specify their parameters in the signature" + " of their __init__ (no varargs)." + " %s with constructor %s doesn't " + " follow this convention." % (cls, init_signature) + ) + # Extract and sort argument names excluding 'self' + return sorted([p.name for p in parameters]) + + @abc.abstractmethod + def fit(self, X: NDArray, y: NDArray): + pass + + @abc.abstractmethod + def predict(self, X: NDArray): + pass + + @abc.abstractmethod + def score(self, X: NDArray, y: NDArray): + pass + + @abc.abstractmethod + def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray): + pass + + @abc.abstractmethod + def _score(self, X: NDArray, y: NDArray, params: Tuple[jnp.ndarray, jnp.ndarray], ): + pass \ No newline at end of file From d050c32dcc200c324e836d3865f53e95e4d0538f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 18 Aug 2023 15:46:40 -0400 Subject: [PATCH 010/250] fixed scanf --- src/neurostatslib/glm.py | 114 +++++++++++++++++++++++++-------------- 1 file changed, 73 insertions(+), 41 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 8899d6a9..d5eaf3db 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -379,61 +379,76 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: raise NotImplementedError(f"Scoring method {self.score_type} not implemented!") return score + def simulate( self, random_key: jax.random.PRNGKeyArray, n_timesteps: int, init_spikes: NDArray, coupling_basis_matrix: NDArray, - feedforward_input: NDArray - ) -> jnp.ndarray: - """Simulate spikes using GLM as a recurrent network, for extrapolating into the future. + feedforward_input: NDArray, + device: str = 'cpu' + ) -> Tuple[jnp.ndarray, jnp.ndarray]: + """ + Simulate spike trains using GLM as a recurrent network. + + This method extrapolates spike trains into the future. By default, it runs on the CPU. GPU + implementations may be slow due to the non-parallelizable nature of computations. Nonetheless, + device selection is provided to avoid data transfer overheads between devices. Parameters ---------- random_key - jax PRNGKey to seed simulation with. + PRNGKey for seeding the simulation. n_timesteps - Number of time steps to simulate. - init_spikes : - Spike counts arranged in a matrix. These are used to jump start the - forward simulation. ``n_neurons`` must be the same as during the - fitting of this GLM instance and ``window_size`` must be the same - as the bases functions (i.e., ``self.spike_basis_matrix.shape[1]``), shape (window_size,n_neurons) - coupling_basis_matrix: - Coupling and auto-correlation filter basis matrix. Shape (n_neurons, n_basis_coupling) - feedforward_input: - Part of the exogenous matrix that captures the external inputs (currents convolved with a basis, - images convolved with basis, position time series evaluated in a basis). - Shape (n_timesteps, n_neurons, n_basis_input). + Number of time steps for simulation. + init_spikes + Spike counts matrix used to initiate the simulation. + Expected shape: (window_size, n_neurons). + coupling_basis_matrix + Basis matrix for coupling and auto-correlation filters. + Expected shape: (window_size, n_basis_coupling). + feedforward_input + Exogenous matrix representing external inputs like convolved currents, images, etc. + Expected shape: (n_timesteps, n_neurons, n_basis_input). + device : optional + Computational device to use ('cpu' or 'gpu'). Default is 'cpu'. Returns ------- - simulated_spikes : (n_neurons, n_timesteps) - The simulated spikes. + simulated_spikes + Simulated spikes. Shape: (n_neurons, n_timesteps). + firing_rates + Simulated firing rates. Shape: (n_neurons, n_timesteps). Raises ------ NotFittedError - If ``fit`` has not been called first with this instance. + Raised if the instance has not been previously fitted. ValueError - If attempting to simulate a different number of neurons than were - present during fitting (i.e., if ``init_spikes.shape[0] != - self.baseline_log_fr_.shape[0]``) or if ``init_spikes`` has the - wrong number of time steps (i.e., if ``init_spikes.shape[1] != - self.spike_basis_matrix.shape[1]``) + Raised for incompatible shapes between `init_spikes` and the fitting data, + or between `init_spikes` and `coupling_basis_matrix`. See Also -------- - predict - Predict firing rates based on fit parameters, for checking against existing data. + predict : Method to predict firing rates using fit parameters. Notes ----- - n_basis_input + n_basis_coupling = self.spike_basis_coeff_.shape[1] - + The sum of n_basis_input and n_basis_coupling should match `self.spike_basis_coeff_.shape[1]`. """ - from jax.experimental import host_callback + if device == 'cpu': + target_device = jax.devices('cpu')[0] + elif device == 'gpu': + target_device = jax.devices('gpu')[0] + else: + raise ValueError(f"Invalid device: {device}. Choose 'cpu' or 'gpu'.") + + # Transfer data to the target device + init_spikes = jax.device_put(init_spikes, target_device) + coupling_basis_matrix = jax.device_put(coupling_basis_matrix, target_device) + feedforward_input = jax.device_put(feedforward_input, target_device) + self.check_is_fit() Ws = self.spike_basis_coeff_ @@ -451,33 +466,50 @@ def simulate( if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( "init_spikes has the wrong number of time steps!" - f"init_spikes time steps: {init_spikes.shape[0]}, " - f"spike_basis_matrix window size: {coupling_basis_matrix.shape[0]}" + f"init_spikes time steps: {init_spikes.shape[1]}, " + f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" ) - #subkeys = jax.random.split(random_key[0], num=n_timesteps) - subkeys = jax.random.split(random_key) + subkeys = jax.random.split(random_key, num=n_timesteps) + + def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray)\ + -> Tuple[Tuple[NDArray, int], NDArray]: + """Function to scan over time steps and simulate spikes and firing rates. - def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray) -> Tuple[Tuple[NDArray, int], NDArray]: + This function simulates the spikes and firing rates for each time step + based on the previous spike data, feedforward input, and model coefficients. + """ spikes, chunk = data + + # Convolve the spike data with the coupling basis matrix conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] + # Extract the corresponding slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( feedforward_input, (chunk, 0, 0), (1, feedforward_input.shape[1], feedforward_input.shape[2]) ) - X = jnp.concatenate([conv_spk] * spikes.shape[1] + [input_slice], axis=2) + + # Reshape the convolved spikes and concatenate with the input slice to form the model input + conv_spk = jnp.tile(conv_spk.reshape(conv_spk.shape[0], -1), + conv_spk.shape[1] + ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) + X = jnp.concatenate([conv_spk, input_slice], axis=2) + + # Predict the firing rate using the model coefficients firing_rate = self._predict((Ws, bs), X) - #key = jnp.squeeze(jax.lax.dynamic_slice(random_key, (chunk, 0), (1, random_key.shape[1]))) + + # Simulate spikes based on the predicted firing rate new_spikes = jax.random.poisson(key, firing_rate) - # this remains always of the same shape - concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 - return concat_spikes, new_spikes - _, simulated_spikes = jax.lax.scan(scan_fn, (init_spikes, 0), subkeys) + # Prepare the spikes for the next iteration (keeping the most recent spikes) + concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 + return concat_spikes, (new_spikes, firing_rate) - return jnp.squeeze(simulated_spikes, axis=1) + _, outputs = jax.lax.scan(scan_fn, (init_spikes, 0), subkeys) + simulated_spikes, firing_rates = outputs + return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) class GLM(GLMBase): From 5507c634f262fc07fea86db6e6c632915dec0f10 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Aug 2023 09:33:46 -0400 Subject: [PATCH 011/250] fixed naming and improved docstrings --- src/neurostatslib/glm.py | 422 ++++++++++++---------- src/neurostatslib/model_base.py | 18 +- tests/basic_test.py | 9 +- tests/test_glm_synthetic.py | 8 +- tests/test_glm_synthetic_single_neuron.py | 8 +- 5 files changed, 260 insertions(+), 205 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index d5eaf3db..66c168b6 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,8 +1,8 @@ """GLM core module """ -import inspect import abc -from typing import Callable, Optional, Tuple, Literal +import inspect +from typing import Callable, Literal, Optional, Tuple import jax import jax.numpy as jnp @@ -10,14 +10,16 @@ from numpy.typing import NDArray from sklearn.exceptions import NotFittedError -from .utils import convolve_1d_trials from .model_base import Model +from .utils import convolve_1d_trials -class GLMBase(Model, abc.ABC): - """Generalized Linear Model for neural responses. +class PoissonGLMBase(Model, abc.ABC): + """Base class for Poisson-GLM. - No stimulus / external variables, only connections to other neurons. + The class includes methods for predicting rates, scoring the mode, and simulating counts. + The abstract method `fit` will be reimplemented in concrete classes according + to the type of regularization. Parameters ---------- @@ -40,7 +42,7 @@ class GLMBase(Model, abc.ABC): jaxopt solver, set during ``fit()`` solver_state state of the solver, set during ``fit()`` - spike_basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) + basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) Solutions for the spike basis coefficients, set during ``fit()`` baseline_log_fr : jnp.ndarray, (n_neurons,) Solutions for bias terms, set during ``fit()`` @@ -48,12 +50,12 @@ class GLMBase(Model, abc.ABC): """ def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - **kwargs + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + **kwargs, ): self.solver_name = solver_name try: @@ -69,55 +71,20 @@ def __init__( f"kwarg {k} in solver_kwargs is not a kwarg for jaxopt.{solver_name}!" ) - if score_type not in ['log-likelihood', 'pseudo-r2']: - raise NotImplementedError("Scoring method not implemented. " - f"score_type must be either 'log-likelihood', or 'pseudo-r2'." - f" {score_type} provided instead.") + if score_type not in ["log-likelihood", "pseudo-r2"]: + raise NotImplementedError( + "Scoring method not implemented. " + f"score_type must be either 'log-likelihood', or 'pseudo-r2'." + f" {score_type} provided instead." + ) self.score_type = score_type self.solver_kwargs = solver_kwargs self.inverse_link_function = inverse_link_function # set additional kwargs e.g. regularization hyperparameters and so on... super().__init__(**kwargs) - @abc.abstractmethod - def fit( - self, - X: NDArray, - spike_data: NDArray, - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, - ) -> None: - """Fit GLM to spiking data. - - Following scikit-learn API, the solutions are stored as attributes - ``spike_basis_coeff_`` and ``baseline_log_fr``. - - Parameters - ---------- - X : - Predictors, shape (n_time_bins, n_neurons, n_features) - spike_data : - Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). - init_params : - Initial values for the spike basis coefficients and bias terms. If - None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) - - Raises - ------ - ValueError - If spike_data is not two-dimensional. - ValueError - If shapes of init_params are not correct. - ValueError - If solver returns at least one NaN parameter, which means it found - an invalid solution. Try tuning optimization hyperparameters. - - """ - pass - def _predict( - self, - params: Tuple[jnp.ndarray, jnp.ndarray], - X: NDArray + self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray ) -> jnp.ndarray: """Helper function for generating predictions. @@ -142,65 +109,61 @@ def _predict( """ Ws, bs = params - return self.inverse_link_function( - jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :] - ) + return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) def _score( - self, - X: NDArray, - target_spikes: NDArray, - params: Tuple[jnp.ndarray, jnp.ndarray] + self, + X: NDArray, + target_spikes: NDArray, + params: Tuple[jnp.ndarray, jnp.ndarray], ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. - This computes the Poisson negative log-likelihood. + This computes the Poisson negative log-likelihood. - Note that you can end up with infinities in here if there are zeros in - ``predicted_firing_rates``. We raise a warning in that case. + Note that you can end up with infinities in here if there are zeros in + ``predicted_firing_rates``. We raise a warning in that case. - Parameters - ---------- - X : (n_time_bins, n_neurons, n_features) - The exogenous variables. - target_spikes : (n_time_bins, n_neurons ) - The target spikes to compare against - params : ((n_neurons, n_features), (n_neurons,)) - Values for the spike basis coefficients and bias terms. + Parameters + ---------- + X : (n_time_bins, n_neurons, n_features) + The exogenous variables. + target_spikes : (n_time_bins, n_neurons ) + The target spikes to compare against + params : ((n_neurons, n_features), (n_neurons,)) + Values for the spike basis coefficients and bias terms. - Returns - ------- - score : (1,) - The Poisson log-likehood + Returns + ------- + score : (1,) + The Poisson log-likehood - Notes - ----- - The Poisson probably mass function is: + Notes + ----- + The Poisson probably mass function is: - .. math:: - \frac{\lambda^k \exp(-\lambda)}{k!} + .. math:: + \frac{\lambda^k \exp(-\lambda)}{k!} - Thus, the negative log of it is: + Thus, the negative log of it is: - .. math:: -¨ -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] - &= -k\log(\lambda)-\lambda+\log(\Gamma(k+1)) + .. math:: + ¨ -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] + &= -k\log(\lambda)-\lambda+\log(\Gamma(k+1)) - Because $\Gamma(k+1)=k!$, see - https://en.wikipedia.org/wiki/Gamma_function. + Because $\Gamma(k+1)=k!$, see + https://en.wikipedia.org/wiki/Gamma_function. - And, in our case, ``target_spikes`` is $k$ and - ``predicted_firing_rates`` is $\lambda$ + And, in our case, ``target_spikes`` is $k$ and + ``predicted_firing_rates`` is $\lambda$ """ # Avoid the edge-case of 0*log(0), much faster than # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) x = target_spikes * jnp.log(predicted_firing_rates) # see above for derivation of this. - return jnp.mean( - predicted_firing_rates - x - ) + return jnp.mean(predicted_firing_rates - x) def _residual_deviance(self, predicted_rate, y): """Compute the residual deviance for a Poisson model. @@ -238,44 +201,96 @@ def _pseudo_r2(self, X, y): """ mu = self.predict(X) res_dev_t = self._residual_deviance(mu, y) - resid_deviance = jnp.sum(res_dev_t ** 2) + resid_deviance = jnp.sum(res_dev_t**2) null_mu = jnp.ones(y.shape) * y.sum() / y.size null_dev_t = self._residual_deviance(null_mu, y) - null_deviance = jnp.sum(null_dev_t ** 2) + null_deviance = jnp.sum(null_dev_t**2) return (null_deviance - resid_deviance) / null_deviance - def check_is_fit(self): - if not hasattr(self, "spike_basis_coeff_"): + if not hasattr(self, "basis_coeff_"): raise NotFittedError( "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) - def check_n_neurons(self, spike_data, bs): - if spike_data.shape[1] != bs.shape[0]: + @staticmethod + def check_n_neurons(params, *args): + n_neurons = params[0].shape[0] + if n_neurons != params[1].shape[0]: raise ValueError( - "Number of neurons must be the same during prediction and fitting! " - f"spike_data n_neurons: {spike_data.shape[1]}, " - f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" + "Model parameters have inconsistent shapes." + "spike basis coefficients must be of shape (n_neurons, n_features), and" + "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both!" + f"coefficients n_neurons: {params[0].shape[0]}, bias n_neurons: {params[1].shape[0]}" ) + for arg in args: + if arg.shape[1] != n_neurons: + raise ValueError( + "The number of neuron in the model parameters and in the inputs" + "must match." + f"parameters has n_neurons: {n_neurons}, " + f"the input provided has n_neurons: {arg.shape[1]}" + ) - def check_n_features(self, spike_data, bs): - if spike_data.shape[1] != bs.shape[0]: + @staticmethod + def check_n_features(Ws, X): + if Ws.shape[1] != X.shape[2]: raise ValueError( - "Number of neurons must be the same during prediction and fitting! " - f"spike_data n_neurons: {spike_data.shape[1]}, " - f"self.baseline_log_fr_ n_neurons: {self.baseline_log_fr_.shape[0]}" + "Inconsistent number of features. " + f"spike basis coefficients has {Ws.shape[1]} features, " + f"X has {X.shape[2]} features instead!" ) + def check_params( + self, params: Tuple[NDArray, NDArray], X: NDArray, spike_data: NDArray + ): + """ + + Parameters + ---------- + params + args + + Returns + ------- + + """ + if len(params) != 2: + raise ValueError("Params needs to be a JAX pytree of size two of NDArray.") + + if params[0].ndim != 2: + raise ValueError( + "Weights must be of shape (n_neurons, n_features), but" + f"params[0] has {params[0].ndim} dimensions!" + ) + if params[1].ndim != 1: + raise ValueError( + "params[1] term must be of shape (n_neurons,) but params[1] have" + f"{params[1].ndim} dimensions!" + ) + + # check that the neurons + self.check_n_neurons(params, X, spike_data) + + if spike_data.ndim != 2: + raise ValueError( + "spike_data must be two-dimensional, with shape (n_timebins, n_neurons)" + ) + if X.ndim != 3: + raise ValueError( + "X must be three-dimensional, with shape (n_timebins, n_neurons, n_features)" + ) + self.check_n_features(params[0], X) + def predict(self, X: NDArray) -> jnp.ndarray: """Predict firing rates based on fit parameters, for checking against existing data. Parameters ---------- - X : (n_time_bins, n_neurons, n_features) - The exogenous variables. + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features). Returns ------- @@ -289,7 +304,7 @@ def predict(self, X: NDArray) -> jnp.ndarray: ValueError If attempting to simulate a different number of neurons than were present during fitting (i.e., if ``init_spikes.shape[0] != - self.baseline_log_fr_.shape[0]``). + self.baseline_link_fr_.shape[0]``). See Also -------- @@ -300,9 +315,10 @@ def predict(self, X: NDArray) -> jnp.ndarray: """ self.check_is_fit() - Ws = self.spike_basis_coeff_ - bs = self.baseline_log_fr_ - self.check_n_neurons(X, bs) + Ws = self.basis_coeff_ + bs = self.baseline_link_fr_ + self.check_n_neurons((Ws, bs), X) + self.check_n_features(Ws, X) return self._predict((Ws, bs), X) def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: @@ -350,7 +366,7 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: ValueError If attempting to simulate a different number of neurons than were present during fitting (i.e., if ``init_spikes.shape[0] != - self.baseline_log_fr_.shape[0]``). + self.baseline_link_fr_.shape[0]``). UserWarning If there are any zeros in ``self.predict(spike_data)``, since this will likely lead to infinite log-likelihood values being returned. @@ -367,27 +383,67 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: # ignore the last time point from predict, because that corresponds to # the next time step, which we have no observed data for self.check_is_fit() - Ws = self.spike_basis_coeff_ - bs = self.baseline_log_fr_ - self.check_n_neurons(spike_data, bs) + Ws = self.basis_coeff_ + bs = self.baseline_link_fr_ + self.check_n_neurons((Ws, bs), X, spike_data) + self.check_n_features(Ws, X) if self.score_type == "log-likelihood": - score = -(self._score(X, spike_data, (Ws, bs)) + jax.scipy.special.gammaln(spike_data + 1).mean()) + score = -( + self._score(X, spike_data, (Ws, bs)) + + jax.scipy.special.gammaln(spike_data + 1).mean() + ) elif self.score_type == "pseudo-r2": score = self._pseudo_r2(X, spike_data) else: # this should happen only if one manually set score_type - raise NotImplementedError(f"Scoring method {self.score_type} not implemented!") + raise NotImplementedError( + f"Scoring method {self.score_type} not implemented!" + ) return score + @abc.abstractmethod + def fit( + self, + X: NDArray, + spike_data: NDArray, + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + ): + """Fit GLM to spiking data. + + Following scikit-learn API, the solutions are stored as attributes + ``basis_coeff_`` and ``baseline_log_fr``. + + Parameters + ---------- + X : + Predictors, shape (n_time_bins, n_neurons, n_features) + spike_data : + Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). + init_params : + Initial values for the spike basis coefficients and bias terms. If + None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) + + Raises + ------ + ValueError + If spike_data is not two-dimensional. + ValueError + If shapes of init_params are not correct. + ValueError + If solver returns at least one NaN parameter, which means it found + an invalid solution. Try tuning optimization hyperparameters. + + """ + pass def simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_spikes: NDArray, - coupling_basis_matrix: NDArray, - feedforward_input: NDArray, - device: str = 'cpu' + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_spikes: NDArray, + coupling_basis_matrix: NDArray, + feedforward_input: NDArray, + device: str = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using GLM as a recurrent network. @@ -435,12 +491,12 @@ def simulate( Notes ----- - The sum of n_basis_input and n_basis_coupling should match `self.spike_basis_coeff_.shape[1]`. + The sum of n_basis_input and n_basis_coupling should match `self.basis_coeff_.shape[1]`. """ - if device == 'cpu': - target_device = jax.devices('cpu')[0] - elif device == 'gpu': - target_device = jax.devices('gpu')[0] + if device == "cpu": + target_device = jax.devices("cpu")[0] + elif device == "gpu": + target_device = jax.devices("gpu")[0] else: raise ValueError(f"Invalid device: {device}. Choose 'cpu' or 'gpu'.") @@ -451,17 +507,22 @@ def simulate( self.check_is_fit() - Ws = self.spike_basis_coeff_ - bs = self.baseline_log_fr_ - self.check_n_neurons(init_spikes, bs) + Ws = self.basis_coeff_ + bs = self.baseline_link_fr_ + self.check_n_neurons((Ws, bs), feedforward_input, init_spikes) - if feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] != Ws.shape[1]: - raise ValueError("The number of feed forward input features" - "and the number of recurrent features must add up to" - "the overall model features." - f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " - f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " - f"provided instead.") + if ( + feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] + != Ws.shape[1] + ): + raise ValueError( + "The number of feed forward input features" + "and the number of recurrent features must add up to" + "the overall model features." + f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " + f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " + f"provided instead." + ) if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( @@ -472,8 +533,9 @@ def simulate( subkeys = jax.random.split(random_key, num=n_timesteps) - def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray)\ - -> Tuple[Tuple[NDArray, int], NDArray]: + def scan_fn( + data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray + ) -> Tuple[Tuple[NDArray, int], NDArray]: """Function to scan over time steps and simulate spikes and firing rates. This function simulates the spikes and firing rates for each time step @@ -488,13 +550,13 @@ def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray)\ input_slice = jax.lax.dynamic_slice( feedforward_input, (chunk, 0, 0), - (1, feedforward_input.shape[1], feedforward_input.shape[2]) + (1, feedforward_input.shape[1], feedforward_input.shape[2]), ) # Reshape the convolved spikes and concatenate with the input slice to form the model input - conv_spk = jnp.tile(conv_spk.reshape(conv_spk.shape[0], -1), - conv_spk.shape[1] - ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) + conv_spk = jnp.tile( + conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] + ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) X = jnp.concatenate([conv_spk, input_slice], axis=2) # Predict the firing rate using the model coefficients @@ -511,19 +573,32 @@ def scan_fn(data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray)\ simulated_spikes, firing_rates = outputs return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) -class GLM(GLMBase): +class PoissonGLM(PoissonGLMBase): + def __init__( + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + ): + super().__init__( + solver_name=solver_name, + solver_kwargs=solver_kwargs, + inverse_link_function=inverse_link_function, + score_type=score_type, + ) def fit( - self, - X: NDArray, - spike_data: NDArray, - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + self, + X: NDArray, + spike_data: NDArray, + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): """Fit GLM to spiking data. Following scikit-learn API, the solutions are stored as attributes - ``spike_basis_coeff_`` and ``baseline_log_fr``. + ``basis_coeff_`` and ``baseline_log_fr``. Parameters ---------- @@ -546,11 +621,6 @@ def fit( an invalid solution. Try tuning optimization hyperparameters. """ - if spike_data.ndim != 2: - raise ValueError( - "spike_data must be two-dimensional, with shape (n_neurons, n_timebins)" - ) - _, n_neurons = spike_data.shape n_features = X.shape[2] @@ -560,35 +630,13 @@ def fit( init_params = ( jnp.zeros((n_neurons, n_features)), # bs, bias terms - jnp.log(jnp.mean(spike_data, axis=0)) - ) - - if init_params[0].ndim != 2: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), but" - f" init_params[0] has {init_params[0].ndim} dimensions!" + jnp.log(jnp.mean(spike_data, axis=0)), ) - if init_params[1].ndim != 1: - raise ValueError( - "bias terms must be of shape (n_neurons,) but init_params[0] have" - f"{init_params[1].ndim} dimensions!" - ) - if init_params[0].shape[0] != init_params[1].shape[0]: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), and" - "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both!" - f"init_params[0]: {init_params[0].shape[0]}, init_params[1]: {init_params[1].shape[0]}" - ) - if init_params[0].shape[0] != spike_data.shape[1]: - raise ValueError( - "spike basis coefficients must be of shape (n_neurons, n_features), and " - "spike_data must be of shape (n_time_bins, n_neurons) but n_neurons doesn't look the same in both! " - f"init_params[0]: {init_params[0].shape[1]}, spike_data: {spike_data.shape[1]}" - ) + self.check_params(init_params, X, spike_data) def loss(params, X, y): - return -self._score(X, y, params) + return self._score(X, y, params) # Run optimization solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) @@ -600,10 +648,10 @@ def loss(params, X, y): " Try tuning optimization hyperparameters." ) # Store parameters - self.spike_basis_coeff_ = params[0] - self.baseline_log_fr_ = params[1] + self.basis_coeff_ = params[0] + self.baseline_link_fr_ = params[1] # note that this will include an error value, which is not the same as # the output of loss. I believe it's the output of # solver.l2_optimality_error self.solver_state = state - self.solver = solver \ No newline at end of file + self.solver = solver diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py index 9c1c6989..ed217d98 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/model_base.py @@ -3,11 +3,10 @@ Inheriting this class will result in compatibility with sci-kit learn pipelines. """ -from collections import defaultdict import abc - import inspect import warnings +from collections import defaultdict from typing import Tuple import jax.numpy as jnp @@ -88,9 +87,9 @@ def set_params(self, **params): # but only if the user did not explicitly set a value for # "base_estimator". if ( - key == "base_estimator" - and valid_params[key] == "deprecated" - and self.__module__.startswith("sklearn.") + key == "base_estimator" + and valid_params[key] == "deprecated" + and self.__module__.startswith("sklearn.") ): warnings.warn( ( @@ -154,5 +153,10 @@ def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray): pass @abc.abstractmethod - def _score(self, X: NDArray, y: NDArray, params: Tuple[jnp.ndarray, jnp.ndarray], ): - pass \ No newline at end of file + def _score( + self, + X: NDArray, + y: NDArray, + params: Tuple[jnp.ndarray, jnp.ndarray], + ): + pass diff --git a/tests/basic_test.py b/tests/basic_test.py index 432dd689..d13283ea 100644 --- a/tests/basic_test.py +++ b/tests/basic_test.py @@ -27,15 +27,18 @@ b_skl = np.zeros(nn) pred_skl = np.zeros((nt,nn)) for k in range(nn): - model_skl = PoissonRegressor(alpha=0,tol=10**-8,solver="lbfgs",max_iter=1000,fit_intercept=True) + model_skl = PoissonRegressor(alpha=0.,tol=10**-8,solver="lbfgs",max_iter=1000,fit_intercept=True) model_skl.fit(X[:,k,:], spikes[:, k]) weights_skl[k] = model_skl.coef_ b_skl[k] = model_skl.intercept_ pred_skl[:, k] = model_skl.predict(X[:, k,:]) -model_jax = GLM(solver_name="BFGS", solver_kwargs={'tol':10**-8, 'maxiter':1000},inverse_link_function=jnp.exp) + +model_jax = GLM(score_type="pseudo-r2",solver_name="BFGS", + solver_kwargs={'jit':True, 'tol': 10**-8, 'maxiter':1000}, + inverse_link_function=jnp.exp) model_jax.fit(X, spikes) mean_ll_jax = model_jax._score(X, spikes, (W_true, b_true)) firing_rate_jax = model_jax._predict((W_true, b_true),X) -print('jax pars - skl pars:', np.max(np.abs(model_jax.spike_basis_coeff_-weights_skl))) \ No newline at end of file +print('jax pars - skl pars:', np.max(np.abs(model_jax.basis_coeff_ - weights_skl))) \ No newline at end of file diff --git a/tests/test_glm_synthetic.py b/tests/test_glm_synthetic.py index 55cbb123..df2c67e3 100644 --- a/tests/test_glm_synthetic.py +++ b/tests/test_glm_synthetic.py @@ -68,8 +68,8 @@ def test_fit_glm2(): W[i, :, j] = w0 if (i == j) else w1 simulated_model = GLM(B) - simulated_model.spike_basis_coeff_ = jnp.array(W) - simulated_model.baseline_log_fr_ = jnp.ones(nn) * .1 + simulated_model.basis_coeff_ = jnp.array(W) + simulated_model.baseline_link_fr_ = jnp.ones(nn) * .1 init_spikes = jnp.zeros((2, ws)) spike_data = simulated_model.simulate(simulation_key, nt, init_spikes) @@ -96,11 +96,11 @@ def test_fit_glm2(): fig, axes = plt.subplots(nn, nn, sharey=True) for i, j in itertools.product(range(nn), range(nn)): axes[i, j].plot( - B.T @ simulated_model.spike_basis_coeff_[i, :, j], + B.T @ simulated_model.basis_coeff_[i, :, j], label="true" ) axes[i, j].plot( - B.T @ fitted_model.spike_basis_coeff_[i, :, j], + B.T @ fitted_model.basis_coeff_[i, :, j], label="est" ) axes[i, j].axhline(0, dashes=[2, 2], color='k') diff --git a/tests/test_glm_synthetic_single_neuron.py b/tests/test_glm_synthetic_single_neuron.py index efee344c..29651b2a 100644 --- a/tests/test_glm_synthetic_single_neuron.py +++ b/tests/test_glm_synthetic_single_neuron.py @@ -25,8 +25,8 @@ def test_glm_fit(): B = spike_basis.evaluate(onp.linspace(0, 1, ws)).T simulated_model = GLM(B) - simulated_model.spike_basis_coeff_ = jnp.array([0, 0, -1, -1, -1])[None, :, None] - simulated_model.baseline_log_fr_ = jnp.ones(nn) * .1 + simulated_model.basis_coeff_ = jnp.array([0, 0, -1, -1, -1])[None, :, None] + simulated_model.baseline_link_fr_ = jnp.ones(nn) * .1 init_spikes = jnp.zeros((nn, ws)) spike_data = simulated_model.simulate(simulation_key, nt, init_spikes) @@ -50,11 +50,11 @@ def test_glm_fit(): fig, ax = plt.subplots(1, 1, sharey=True) ax.plot( - B.T @ simulated_model.spike_basis_coeff_[0, :, 0], + B.T @ simulated_model.basis_coeff_[0, :, 0], label="true" ) ax.plot( - B.T @ fitted_model.spike_basis_coeff_[0, :, 0], + B.T @ fitted_model.basis_coeff_[0, :, 0], label="est" ) ax.axhline(0, dashes=[2, 2], color='k') From b9cb88f1da72b46410e41a7047529821adc718eb Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Aug 2023 09:54:09 -0400 Subject: [PATCH 012/250] initialize parameters to none --- src/neurostatslib/glm.py | 127 ++++++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 56 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 66c168b6..cf13cb3c 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -23,9 +23,6 @@ class PoissonGLMBase(Model, abc.ABC): Parameters ---------- - spike_basis_matrix : (n_basis_funcs, window_size) - Matrix of basis functions to use for this GLM. Most likely the output - of ``Basis.gen_basis_funcs()`` solver_name Name of the solver to use when fitting the GLM. Must be an attribute of ``jaxopt``. @@ -35,17 +32,10 @@ class PoissonGLMBase(Model, abc.ABC): inverse_link_function Function to transform outputs of convolution with basis to firing rate. Must accept any number as input and return all non-negative values. - - Attributes - ---------- - solver - jaxopt solver, set during ``fit()`` - solver_state - state of the solver, set during ``fit()`` - basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) - Solutions for the spike basis coefficients, set during ``fit()`` - baseline_log_fr : jnp.ndarray, (n_neurons,) - Solutions for bias terms, set during ``fit()`` + kwargs: + Additional keyword arguments. ``kwargs`` may depend on the concrete + subclass implementation (e.g. alpha, the regularization hyperparamter, will be present for + penalized GLMs but not for the un-penalized case). """ @@ -82,6 +72,9 @@ def __init__( self.inverse_link_function = inverse_link_function # set additional kwargs e.g. regularization hyperparameters and so on... super().__init__(**kwargs) + # initialize parameters to None + self.baseline_link_fr_ = None + self.basis_coeff_ = None def _predict( self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray @@ -90,22 +83,17 @@ def _predict( This way, can use same functions during and after fitting. - Note that the ``n_timebins`` here is not necessarily the same as in - public functions: in particular, this method expects the *convolved* - spike data, which (since we use the "valid" convolutional output) means - that it will have fewer timebins than the un-convolved data. - Parameters ---------- - params : ((n_neurons, n_features), (n_neurons,)) - Values for the spike basis coefficients and bias terms. - X : (n_time_bins, n_neurons, n_features) - The model matrix. + params : + Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). + X : + The model matrix. Shape (n_time_bins, n_neurons, n_features). Returns ------- - predicted_firing_rates : (n_time_bins, n_neurons) - The predicted firing rates. + predicted_firing_rates : + The predicted firing rates. Shape (n_time_bins, n_neurons). """ Ws, bs = params @@ -119,43 +107,43 @@ def _score( ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. - This computes the Poisson negative log-likelihood. + This computes the Poisson negative log-likelihood. - Note that you can end up with infinities in here if there are zeros in - ``predicted_firing_rates``. We raise a warning in that case. + Note that you can end up with infinities in here if there are zeros in + ``predicted_firing_rates``. We raise a warning in that case. - Parameters - ---------- - X : (n_time_bins, n_neurons, n_features) - The exogenous variables. - target_spikes : (n_time_bins, n_neurons ) - The target spikes to compare against - params : ((n_neurons, n_features), (n_neurons,)) - Values for the spike basis coefficients and bias terms. + Parameters + ---------- + X : (n_time_bins, n_neurons, n_features) + The exogenous variables. + target_spikes : (n_time_bins, n_neurons ) + The target spikes to compare against + params : ((n_neurons, n_features), (n_neurons,)) + Values for the spike basis coefficients and bias terms. - Returns - ------- - score : (1,) - The Poisson log-likehood + Returns + ------- + score : (1,) + The Poisson log-likehood - Notes - ----- - The Poisson probably mass function is: + Notes + ----- + The Poisson probably mass function is: - .. math:: - \frac{\lambda^k \exp(-\lambda)}{k!} + .. math:: + \frac{\lambda^k \exp(-\lambda)}{k!} - Thus, the negative log of it is: + Thus, the negative log of it is: - .. math:: - ¨ -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] - &= -k\log(\lambda)-\lambda+\log(\Gamma(k+1)) + .. math:: +¨ -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] + &= -k\log(\lambda)-\lambda+\log(\Gamma(k+1)) - Because $\Gamma(k+1)=k!$, see - https://en.wikipedia.org/wiki/Gamma_function. + Because $\Gamma(k+1)=k!$, see + https://en.wikipedia.org/wiki/Gamma_function. - And, in our case, ``target_spikes`` is $k$ and - ``predicted_firing_rates`` is $\lambda$ + And, in our case, ``target_spikes`` is $k$ and + ``predicted_firing_rates`` is $\lambda$ """ # Avoid the edge-case of 0*log(0), much faster than @@ -210,7 +198,7 @@ def _pseudo_r2(self, X, y): return (null_deviance - resid_deviance) / null_deviance def check_is_fit(self): - if not hasattr(self, "basis_coeff_"): + if self.basis_coeff_ is None: raise NotFittedError( "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) @@ -370,9 +358,9 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: UserWarning If there are any zeros in ``self.predict(spike_data)``, since this will likely lead to infinite log-likelihood values being returned. + Notes ----- - The log-likelihood is not on a standard scale, its value is influenced by many factors, among which the number of model parameters. The log-likelihood can assume both positive and negative values. @@ -411,7 +399,7 @@ def fit( """Fit GLM to spiking data. Following scikit-learn API, the solutions are stored as attributes - ``basis_coeff_`` and ``baseline_log_fr``. + ``basis_coeff_`` and ``baseline_link_fr``. Parameters ---------- @@ -575,6 +563,33 @@ def scan_fn( class PoissonGLM(PoissonGLMBase): + """Un-regularized Poisson-GLM. + + The class fits the un-penalized maximum likelihood Poisson GLM parameter estimate. + + Parameters + ---------- + solver_name + Name of the solver to use when fitting the GLM. Must be an attribute of + ``jaxopt``. + solver_kwargs + Dictionary of keyword arguments to pass to the solver during its + initialization. + inverse_link_function + Function to transform outputs of convolution with basis to firing rate. + Must accept any number as input and return all non-negative values. + + Attributes + ---------- + solver + jaxopt solver, set during ``fit()`` + solver_state + state of the solver, set during ``fit()`` + basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) + Solutions for the spike basis coefficients, set during ``fit()`` + baseline_log_fr : jnp.ndarray, (n_neurons,) + Solutions for bias terms, set during ``fit()`` + """ def __init__( self, solver_name: str = "GradientDescent", From 451be3a67dfac6d7dc4db42357b0fd85d0ff79d5 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Aug 2023 14:33:43 -0400 Subject: [PATCH 013/250] improved docstrings, started off tests --- docs/javascripts/katex.js | 3 +- src/neurostatslib/glm.py | 308 +++++++++++++++++++------------- src/neurostatslib/model_base.py | 10 +- tests/basic_test.py | 4 +- tests/test_glm.py | 53 ++++++ 5 files changed, 243 insertions(+), 135 deletions(-) create mode 100644 tests/test_glm.py diff --git a/docs/javascripts/katex.js b/docs/javascripts/katex.js index f7fd7047..250961c8 100644 --- a/docs/javascripts/katex.js +++ b/docs/javascripts/katex.js @@ -4,7 +4,8 @@ document$.subscribe(({ body }) => { { left: "$$", right: "$$", display: true }, { left: "$", right: "$", display: false }, { left: "\\(", right: "\\)", display: false }, - { left: "\\[", right: "\\]", display: true } + { left: "\\[", right: "\\]", display: true }, + { left: "\\begin{aligned}", right: "\\end{aligned}", display: true } ], }) }) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index cf13cb3c..ed54286d 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -15,11 +15,10 @@ class PoissonGLMBase(Model, abc.ABC): - """Base class for Poisson-GLM. + """Abstract base class for Poisson GLMs. - The class includes methods for predicting rates, scoring the mode, and simulating counts. - The abstract method `fit` will be reimplemented in concrete classes according - to the type of regularization. + Provides methods for score computation, simulation, and prediction. + Must be subclassed with a method for fitting to data. Parameters ---------- @@ -55,11 +54,9 @@ def __init__( f"module jaxopt has no attribute {solver_name}, pick a different solver!" ) - for k in solver_kwargs.keys(): - if k not in solver_args: - raise NameError( - f"kwarg {k} in solver_kwargs is not a kwarg for jaxopt.{solver_name}!" - ) + undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) + if undefined_kwargs: + raise NameError(f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!") if score_type not in ["log-likelihood", "pseudo-r2"]: raise NotImplementedError( @@ -79,22 +76,20 @@ def __init__( def _predict( self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray ) -> jnp.ndarray: - """Helper function for generating predictions. - - This way, can use same functions during and after fitting. + """ + Predict firing rates given predictors and parameters. Parameters ---------- params : - Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). + Tuple containing the spike basis coefficients and bias terms. X : - The model matrix. Shape (n_time_bins, n_neurons, n_features). + Predictors. Shape (n_time_bins, n_neurons, n_features). Returns ------- - predicted_firing_rates : + jnp.ndarray The predicted firing rates. Shape (n_time_bins, n_neurons). - """ Ws, bs = params return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) @@ -107,43 +102,40 @@ def _score( ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. - This computes the Poisson negative log-likelihood. + This computes the Poisson negative log-likelihood up to a constant. Note that you can end up with infinities in here if there are zeros in ``predicted_firing_rates``. We raise a warning in that case. Parameters ---------- - X : (n_time_bins, n_neurons, n_features) - The exogenous variables. - target_spikes : (n_time_bins, n_neurons ) - The target spikes to compare against - params : ((n_neurons, n_features), (n_neurons,)) - Values for the spike basis coefficients and bias terms. + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features). + target_spikes : + The target spikes to compare against. Shape (n_time_bins, n_neurons). + params : + Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). Returns ------- - score : (1,) - The Poisson log-likehood + jnp.ndarray + The Poisson negative log-likehood. Shape (1,). Notes ----- - The Poisson probably mass function is: + The Poisson probability mass function is: - .. math:: + $$ \frac{\lambda^k \exp(-\lambda)}{k!} + $$ - Thus, the negative log of it is: - - .. math:: -¨ -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] - &= -k\log(\lambda)-\lambda+\log(\Gamma(k+1)) - - Because $\Gamma(k+1)=k!$, see - https://en.wikipedia.org/wiki/Gamma_function. + But the $k!$ term is not a function of the parameters and can be disregarded + when computing the loss-function. Thus, the negative log of it is: - And, in our case, ``target_spikes`` is $k$ and - ``predicted_firing_rates`` is $\lambda$ + $$ + -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] + &= -k\log(\lambda)-\lambda + \text{const} + $$ """ # Avoid the edge-case of 0*log(0), much faster than @@ -154,40 +146,62 @@ def _score( return jnp.mean(predicted_firing_rates - x) def _residual_deviance(self, predicted_rate, y): - """Compute the residual deviance for a Poisson model. + r"""Compute the residual deviance for a Poisson model. Parameters ---------- - X: - The predictors. Shape (n_time_bins, n_neurons, n_features). + predicted_rate: + The predicted firing rates. y: - The spike counts. Shape (n_time_bins, n_neurons). + The spike counts. Returns ------- The residual deviance of the model. + + Notes + ----- + Deviance is a measure of the goodness of fit of a statistical model. + For a Poisson model, the residual deviance is computed as: + + $$ + \begin{aligned} + D(y, \hat{y}) &= 2 \sum \left[ y \log\left(\frac{y}{\hat{y}}\right) - (y - \hat{y}) \right]\\\ + &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) + \end{aligned} + $$ + where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model + log-likelihood. Lower values of deviance indicate a better fit. + """ # this takes care of 0s in the log ratio = jnp.clip(y / predicted_rate, self.FLOAT_EPS, jnp.inf) resid_dev = y * jnp.log(ratio) - (y - predicted_rate) return resid_dev - def _pseudo_r2(self, X, y): - """Pseudo-R2 calculation. + def _pseudo_r2(self, params, X, y): + r"""Pseudo-R^2 calculation for a Poisson GLM. + + The Pseudo-R^2 metric gives a sense of how well the model fits the data, + relative to a null (or baseline) model. Parameters ---------- + params : + Tuple containing the spike basis coefficients and bias terms. X: - The predictors. Shape (n_time_bins, n_neurons, n_features). + The predictors. y: - The spike counts. Shape (n_time_bins, n_neurons). + The spike counts. Returns ------- : - The pseudo-r2 of the model. + The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, + whereas a value closer to 0 suggests that the model doesn't improve much over the null model. + """ - mu = self.predict(X) + mu = self._predict(params, X) res_dev_t = self._residual_deviance(mu, y) resid_deviance = jnp.sum(res_dev_t**2) @@ -197,14 +211,25 @@ def _pseudo_r2(self, X, y): return (null_deviance - resid_deviance) / null_deviance - def check_is_fit(self): + def _check_is_fit(self): + """Ensure the instance has been fitted.""" if self.basis_coeff_ is None: raise NotFittedError( "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) @staticmethod - def check_n_neurons(params, *args): + def _check_n_neurons(params, *args): + """ + Validate the number of neurons in model parameters and input arguments. + + This function checks that the number of neurons is consistent across + the model parameters (`params`) and any additional inputs (`args`). + Specifically, it ensures that the spike basis coefficients and bias terms + have the same first dimension and that this dimension matches the second + dimension of all input matrices in `args`. + + """ n_neurons = params[0].shape[0] if n_neurons != params[1].shape[0]: raise ValueError( @@ -223,7 +248,14 @@ def check_n_neurons(params, *args): ) @staticmethod - def check_n_features(Ws, X): + def _check_n_features(Ws, X): + """ + Validate the number of features between model coefficients and input data. + + This function checks that the number of features in the spike basis + coefficients (`Ws`) matches the number of features in the input data (`X`). + + """ if Ws.shape[1] != X.shape[2]: raise ValueError( "Inconsistent number of features. " @@ -231,18 +263,15 @@ def check_n_features(Ws, X): f"X has {X.shape[2]} features instead!" ) - def check_params( + def _check_params( self, params: Tuple[NDArray, NDArray], X: NDArray, spike_data: NDArray ): """ + Validate the dimensions and consistency of parameters and data. - Parameters - ---------- - params - args - - Returns - ------- + This function checks the consistency of shapes and dimensions for model + parameters, input predictors (`X`), and spike counts (`spike_data`). + It ensures that the parameters and data are compatible for the model. """ if len(params) != 2: @@ -260,7 +289,7 @@ def check_params( ) # check that the neurons - self.check_n_neurons(params, X, spike_data) + self._check_n_neurons(params, X, spike_data) if spike_data.ndim != 2: raise ValueError( @@ -270,29 +299,31 @@ def check_params( raise ValueError( "X must be three-dimensional, with shape (n_timebins, n_neurons, n_features)" ) - self.check_n_features(params[0], X) + self._check_n_features(params[0], X) def predict(self, X: NDArray) -> jnp.ndarray: - """Predict firing rates based on fit parameters, for checking against existing data. + """Predict firing rates based on fit parameters. Parameters ---------- - X : + X : NDArray The exogenous variables. Shape (n_time_bins, n_neurons, n_features). Returns ------- - predicted_firing_rates : (n_neurons, n_time_bins) - The predicted firing rates. + predicted_firing_rates : jnp.ndarray + The predicted firing rates with shape (n_neurons, n_time_bins). Raises ------ NotFittedError If ``fit`` has not been called first with this instance. ValueError - If attempting to simulate a different number of neurons than were - present during fitting (i.e., if ``init_spikes.shape[0] != - self.baseline_link_fr_.shape[0]``). + - If `params` is not a JAX pytree of size two. + - If weights and bias terms in `params` don't have the expected dimensions. + - If the number of neurons in the model parameters and in the inputs do not match. + - If `X` is not three-dimensional. + - If there's an inconsistent number of features between spike basis coefficients and `X`. See Also -------- @@ -300,38 +331,48 @@ def predict(self, X: NDArray) -> jnp.ndarray: Score predicted firing rates against target spike counts. simulate Simulate spikes using GLM as a recurrent network, for extrapolating into the future. - """ - self.check_is_fit() + self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self.check_n_neurons((Ws, bs), X) - self.check_n_features(Ws, X) + self._check_n_neurons((Ws, bs), X) + self._check_n_features(Ws, X) return self._predict((Ws, bs), X) - def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: + def score(self, + X: NDArray, + spike_data: NDArray, + score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. - This ignores the last time point of the prediction. - - This computes the Poisson mean log-likelihood or the pseudo-R2, thus the higher the + This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the number the better. The formula for the mean log-likelihood is the following, $$ - \text{LL}(\hat{\lambda} | y) = \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} - [y_{tn} \log(\hat{\lambda}_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] + \begin{aligned} + \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} + [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ + &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] + \end{aligned} $$ - The pseudo-R2 can be computed as follows, + Because $\Gamma(k+1)=k!$, see + https://en.wikipedia.org/wiki/Gamma_function. + + The pseudo-$R^2$ can be computed as follows, $$ - \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) + \begin{aligned} + R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ + &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) - \log \text{LL}(\bar{\lambda}| y)}, + \end{aligned} $$ - where $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate + where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for + the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. Parameters @@ -345,7 +386,7 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: Returns ------- score : (1,) - The Poisson log-likehood + The Poisson log-likehood or the pseudo-$R^2$ of the current model. Raises ------ @@ -355,9 +396,6 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: If attempting to simulate a different number of neurons than were present during fitting (i.e., if ``init_spikes.shape[0] != self.baseline_link_fr_.shape[0]``). - UserWarning - If there are any zeros in ``self.predict(spike_data)``, since this - will likely lead to infinite log-likelihood values being returned. Notes ----- @@ -365,23 +403,29 @@ def score(self, X: NDArray, spike_data: NDArray) -> jnp.ndarray: among which the number of model parameters. The log-likelihood can assume both positive and negative values. - The pseudo-R2 is a standardized metric and assumes values between 0 and 1. - + The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure + of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. + The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. """ + # ignore the last time point from predict, because that corresponds to # the next time step, which we have no observed data for - self.check_is_fit() + self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self.check_n_neurons((Ws, bs), X, spike_data) - self.check_n_features(Ws, X) - if self.score_type == "log-likelihood": + self._check_n_neurons((Ws, bs), X, spike_data) + self._check_n_features(Ws, X) + + if score_type is None: + score_type = self.score_type + + if score_type == "log-likelihood": score = -( self._score(X, spike_data, (Ws, bs)) + jax.scipy.special.gammaln(spike_data + 1).mean() ) - elif self.score_type == "pseudo-r2": - score = self._pseudo_r2(X, spike_data) + elif score_type == "pseudo-r2": + score = self._pseudo_r2((Ws,bs), X, spike_data) else: # this should happen only if one manually set score_type raise NotImplementedError( @@ -434,52 +478,61 @@ def simulate( device: str = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ - Simulate spike trains using GLM as a recurrent network. + Simulate spike trains using the GLM as a recurrent network. - This method extrapolates spike trains into the future. By default, it runs on the CPU. GPU - implementations may be slow due to the non-parallelizable nature of computations. Nonetheless, - device selection is provided to avoid data transfer overheads between devices. + This function projects spike trains into the future, employing the fitted + parameters of the GLM. While the default computation device is the CPU, + users can opt for GPU; however, it may not provide substantial speed-up due + to the inherently sequential nature of certain computations. Parameters ---------- - random_key + random_key : PRNGKey for seeding the simulation. - n_timesteps - Number of time steps for simulation. - init_spikes - Spike counts matrix used to initiate the simulation. + n_timesteps : + Duration of the simulation in terms of time steps. + init_spikes : + Initial spike counts matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). - coupling_basis_matrix - Basis matrix for coupling and auto-correlation filters. - Expected shape: (window_size, n_basis_coupling). - feedforward_input - Exogenous matrix representing external inputs like convolved currents, images, etc. - Expected shape: (n_timesteps, n_neurons, n_basis_input). - device : optional - Computational device to use ('cpu' or 'gpu'). Default is 'cpu'. + coupling_basis_matrix : + Basis matrix for coupling, representing inter-neuron effects + and auto-correlations. Expected shape: (window_size, n_basis_coupling). + feedforward_input : + External input matrix, representing factors like convolved currents, + light intensities, etc. Expected shape: (n_timesteps, n_neurons, n_basis_input). + device : + Computation device to use ('cpu' or 'gpu'). Default is 'cpu'. Returns ------- - simulated_spikes - Simulated spikes. Shape: (n_neurons, n_timesteps). - firing_rates - Simulated firing rates. Shape: (n_neurons, n_timesteps). + simulated_spikes : + Simulated spike counts for each neuron over time. + Shape: (n_neurons, n_timesteps). + firing_rates : + Simulated firing rates for each neuron over time. + Shape: (n_neurons, n_timesteps). Raises ------ NotFittedError - Raised if the instance has not been previously fitted. + If the model hasn't been fitted prior to calling this method. + Raises + ------ ValueError - Raised for incompatible shapes between `init_spikes` and the fitting data, - or between `init_spikes` and `coupling_basis_matrix`. + - If the instance has not been previously fitted. + - If there's an inconsistency between the number of neurons in model parameters. + - If the number of neurons in input arguments doesn't match with model parameters. + - For an invalid computational device selection. + See Also -------- - predict : Method to predict firing rates using fit parameters. + predict : Method to predict firing rates based on the model's parameters. Notes ----- - The sum of n_basis_input and n_basis_coupling should match `self.basis_coeff_.shape[1]`. + The sum of n_basis_input and n_basis_coupling should equal `self.basis_coeff_.shape[1]` to ensure + consistency in the model's input feature dimensionality. """ if device == "cpu": target_device = jax.devices("cpu")[0] @@ -493,11 +546,11 @@ def simulate( coupling_basis_matrix = jax.device_put(coupling_basis_matrix, target_device) feedforward_input = jax.device_put(feedforward_input, target_device) - self.check_is_fit() + self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self.check_n_neurons((Ws, bs), feedforward_input, init_spikes) + self._check_n_neurons((Ws, bs), feedforward_input, init_spikes) if ( feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] @@ -628,12 +681,13 @@ def fit( Raises ------ ValueError - If spike_data is not two-dimensional. - ValueError - If shapes of init_params are not correct. - ValueError - If solver returns at least one NaN parameter, which means it found - an invalid solution. Try tuning optimization hyperparameters. + - If `params` is not a JAX pytree of size two. + - If shapes of init_params are not correct. + - If the number of neurons in the model parameters and in the inputs do not match. + - If `X` is not three-dimensional. + - If spike_data is not two-dimensional. + - If solver returns at least one NaN parameter, which means it found + an invalid solution. Try tuning optimization hyperparameters. """ _, n_neurons = spike_data.shape @@ -648,7 +702,7 @@ def fit( jnp.log(jnp.mean(spike_data, axis=0)), ) - self.check_params(init_params, X, spike_data) + self._check_params(init_params, X, spike_data) def loss(params, X, y): return self._score(X, y, params) diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py index ed217d98..ac2b4338 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/model_base.py @@ -7,7 +7,7 @@ import inspect import warnings from collections import defaultdict -from typing import Tuple +from typing import Tuple, Any import jax.numpy as jnp from numpy.typing import NDArray @@ -141,15 +141,15 @@ def fit(self, X: NDArray, y: NDArray): pass @abc.abstractmethod - def predict(self, X: NDArray): + def predict(self, X: NDArray) -> jnp.ndarray: pass @abc.abstractmethod - def score(self, X: NDArray, y: NDArray): + def score(self, X: NDArray, y: NDArray) -> jnp.ndarray: pass @abc.abstractmethod - def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray): + def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.ndarray: pass @abc.abstractmethod @@ -158,5 +158,5 @@ def _score( X: NDArray, y: NDArray, params: Tuple[jnp.ndarray, jnp.ndarray], - ): + ) -> jnp.ndarray: pass diff --git a/tests/basic_test.py b/tests/basic_test.py index d13283ea..e08b85b1 100644 --- a/tests/basic_test.py +++ b/tests/basic_test.py @@ -1,4 +1,4 @@ -from neurostatslib.glm import GLM +from neurostatslib.glm import PoissonGLM from sklearn.linear_model import PoissonRegressor import numpy as np import matplotlib.pylab as plt @@ -34,7 +34,7 @@ pred_skl[:, k] = model_skl.predict(X[:, k,:]) -model_jax = GLM(score_type="pseudo-r2",solver_name="BFGS", +model_jax = PoissonGLM(score_type="pseudo-r2",solver_name="BFGS", solver_kwargs={'jit':True, 'tol': 10**-8, 'maxiter':1000}, inverse_link_function=jnp.exp) model_jax.fit(X, spikes) diff --git a/tests/test_glm.py b/tests/test_glm.py new file mode 100644 index 00000000..210e840f --- /dev/null +++ b/tests/test_glm.py @@ -0,0 +1,53 @@ +import pytest + +import jaxopt + +import neurostatslib as nsl + +class TestPoissonGLM: + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize", "NotPresent"]) + def test_initialization_solver_name(self, solver_name: str): + try: + getattr(jaxopt, solver_name) + raise_exception = False + except: + raise_exception = True + if raise_exception: + with pytest.raises(AttributeError, match="module jaxopt has no attribute"): + nsl.glm.PoissonGLM(solver_name=solver_name) + else: + nsl.glm.PoissonGLM(solver_name=solver_name) + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize"]) + @pytest.mark.parametrize("solver_kwargs", [ + {"tol":1, "verbose":1, "maxiter":1}, + {"tol":1, "maxiter":1}]) + def test_init_solver_kwargs(self, solver_name, solver_kwargs): + raise_exception = (solver_name == "ScipyMinimize") & ("verbose" in solver_kwargs) + if raise_exception: + with pytest.raises(NameError, match="kwargs {'[a-z]+'} in solver_kwargs not a kwarg"): + nsl.glm.PoissonGLM(solver_name, solver_kwargs=solver_kwargs) + else: + # define glm and instantiate the solver + nsl.glm.PoissonGLM(solver_name, solver_kwargs=solver_kwargs) + getattr(jaxopt, solver_name)(fun=lambda x: x, **solver_kwargs) + def test_fit(self): + pass + + + def test_score(self): + pass + + + def test_predict(self): + pass + + + def test_simulate(self): + pass + + def test_compare_to_scikitlearn(self): + pass + + From e02f7229958349d88d91f4ef8f16d724ea4d46ac Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 21 Aug 2023 14:46:50 -0400 Subject: [PATCH 014/250] test glm started --- src/neurostatslib/glm.py | 6 +++++- tests/test_glm.py | 22 +++++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index ed54286d..a2bb8ff2 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -4,7 +4,7 @@ import inspect from typing import Callable, Literal, Optional, Tuple -import jax +import jax, jaxlib import jax.numpy as jnp import jaxopt from numpy.typing import NDArray @@ -66,6 +66,10 @@ def __init__( ) self.score_type = score_type self.solver_kwargs = solver_kwargs + + if not callable(inverse_link_function): + raise ValueError("inverse_link_function must be a callable!") + self.inverse_link_function = inverse_link_function # set additional kwargs e.g. regularization hyperparameters and so on... super().__init__(**kwargs) diff --git a/tests/test_glm.py b/tests/test_glm.py index 210e840f..cd485082 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,13 +1,14 @@ import pytest import jaxopt +import jax.numpy as jnp import neurostatslib as nsl class TestPoissonGLM: @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize", "NotPresent"]) - def test_initialization_solver_name(self, solver_name: str): + def test_init_solver_name(self, solver_name: str): try: getattr(jaxopt, solver_name) raise_exception = False @@ -32,10 +33,26 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): # define glm and instantiate the solver nsl.glm.PoissonGLM(solver_name, solver_kwargs=solver_kwargs) getattr(jaxopt, solver_name)(fun=lambda x: x, **solver_kwargs) + + @pytest.mark.parametrize("func", [1, "string", lambda x:x, jnp.exp]) + def test_init_callable(self, func): + if not callable(func): + with pytest.raises(ValueError, match="inverse_link_function must be a callable"): + nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) + else: + nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) + + @pytest.mark.parametrize("score_type", [1, "ll", "log-likelihood","pseudo-r2"]) + def test_init_score_type(self, score_type: str): + if score_type not in ["log-likelihood","pseudo-r2"]: + with pytest.raises(NotImplementedError, match="Scoring method not implemented."): + nsl.glm.PoissonGLM("BFGS", score_type=score_type) + else: + nsl.glm.PoissonGLM("BFGS", score_type=score_type) + def test_fit(self): pass - def test_score(self): pass @@ -43,7 +60,6 @@ def test_score(self): def test_predict(self): pass - def test_simulate(self): pass From 94efb2c5dacfa0ad4e11dbfde336199d4b487702 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 15:10:31 -0400 Subject: [PATCH 015/250] updated tests --- pyproject.toml | 1 + src/neurostatslib/glm.py | 324 +++++++++++------- src/neurostatslib/model_base.py | 21 +- tests/conftest.py | 36 ++ tests/test_glm.py | 577 ++++++++++++++++++++++++++++++-- 5 files changed, 808 insertions(+), 151 deletions(-) create mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml index 2bc0bcb6..e91c7973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ dev = [ "flake8", # Code linter "coverage", # Test coverage measurement "pytest-cov", # Test coverage plugin for pytest + "statsmodels" # Used to compare model pseudo-r2 in testing ] docs = [ "mkdocs", # Documentation generator diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index a2bb8ff2..da9bf723 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -2,12 +2,12 @@ """ import abc import inspect -from typing import Callable, Literal, Optional, Tuple +from typing import Callable, Literal, Optional, Tuple, Union import jax, jaxlib import jax.numpy as jnp import jaxopt -from numpy.typing import NDArray +from numpy.typing import NDArray, ArrayLike from sklearn.exceptions import NotFittedError from .model_base import Model @@ -39,12 +39,12 @@ class PoissonGLMBase(Model, abc.ABC): """ def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - **kwargs, + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + **kwargs, ): self.solver_name = solver_name try: @@ -78,7 +78,7 @@ def __init__( self.basis_coeff_ = None def _predict( - self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray + self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray ) -> jnp.ndarray: """ Predict firing rates given predictors and parameters. @@ -99,10 +99,10 @@ def _predict( return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) def _score( - self, - X: NDArray, - target_spikes: NDArray, - params: Tuple[jnp.ndarray, jnp.ndarray], + self, + X: jnp.ndarray, + target_spikes: jnp.ndarray, + params: Tuple[jnp.ndarray, jnp.ndarray], ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. @@ -144,7 +144,7 @@ def _score( """ # Avoid the edge-case of 0*log(0), much faster than # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) x = target_spikes * jnp.log(predicted_firing_rates) # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) @@ -180,7 +180,7 @@ def _residual_deviance(self, predicted_rate, y): """ # this takes care of 0s in the log ratio = jnp.clip(y / predicted_rate, self.FLOAT_EPS, jnp.inf) - resid_dev = y * jnp.log(ratio) - (y - predicted_rate) + resid_dev = 2 * (y * jnp.log(ratio) - (y - predicted_rate)) return resid_dev def _pseudo_r2(self, params, X, y): @@ -207,69 +207,25 @@ def _pseudo_r2(self, params, X, y): """ mu = self._predict(params, X) res_dev_t = self._residual_deviance(mu, y) - resid_deviance = jnp.sum(res_dev_t**2) + resid_deviance = jnp.sum(res_dev_t ** 2) - null_mu = jnp.ones(y.shape) * y.sum() / y.size + null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() null_dev_t = self._residual_deviance(null_mu, y) - null_deviance = jnp.sum(null_dev_t**2) + null_deviance = jnp.sum(null_dev_t ** 2) return (null_deviance - resid_deviance) / null_deviance def _check_is_fit(self): """Ensure the instance has been fitted.""" - if self.basis_coeff_ is None: + if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): raise NotFittedError( "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) @staticmethod - def _check_n_neurons(params, *args): - """ - Validate the number of neurons in model parameters and input arguments. - - This function checks that the number of neurons is consistent across - the model parameters (`params`) and any additional inputs (`args`). - Specifically, it ensures that the spike basis coefficients and bias terms - have the same first dimension and that this dimension matches the second - dimension of all input matrices in `args`. - - """ - n_neurons = params[0].shape[0] - if n_neurons != params[1].shape[0]: - raise ValueError( - "Model parameters have inconsistent shapes." - "spike basis coefficients must be of shape (n_neurons, n_features), and" - "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both!" - f"coefficients n_neurons: {params[0].shape[0]}, bias n_neurons: {params[1].shape[0]}" - ) - for arg in args: - if arg.shape[1] != n_neurons: - raise ValueError( - "The number of neuron in the model parameters and in the inputs" - "must match." - f"parameters has n_neurons: {n_neurons}, " - f"the input provided has n_neurons: {arg.shape[1]}" - ) - - @staticmethod - def _check_n_features(Ws, X): - """ - Validate the number of features between model coefficients and input data. - - This function checks that the number of features in the spike basis - coefficients (`Ws`) matches the number of features in the input data (`X`). - - """ - if Ws.shape[1] != X.shape[2]: - raise ValueError( - "Inconsistent number of features. " - f"spike basis coefficients has {Ws.shape[1]} features, " - f"X has {X.shape[2]} features instead!" - ) - - def _check_params( - self, params: Tuple[NDArray, NDArray], X: NDArray, spike_data: NDArray - ): + def _check_and_convert_params( + params: ArrayLike + ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Validate the dimensions and consistency of parameters and data. @@ -278,39 +234,104 @@ def _check_params( It ensures that the parameters and data are compatible for the model. """ + if not hasattr(params, "__getitem__"): + raise TypeError("Initial parameters must be array-like!") + try: + params = tuple(jnp.asarray(par, dtype=jnp.float32) for par in params) + except ValueError: + raise TypeError("Initial parameters must be array-like of array-like objects" + "with numeric data-type!") + if len(params) != 2: - raise ValueError("Params needs to be a JAX pytree of size two of NDArray.") + raise ValueError("Params needs to be array-like of length two.") if params[0].ndim != 2: raise ValueError( - "Weights must be of shape (n_neurons, n_features), but" + "params[0] term must be of shape (n_neurons, n_features), but" f"params[0] has {params[0].ndim} dimensions!" ) if params[1].ndim != 1: raise ValueError( - "params[1] term must be of shape (n_neurons,) but params[1] have" - f"{params[1].ndim} dimensions!" + "params[1] term must be of shape (n_neurons,) but " + f"params[1] has {params[1].ndim} dimensions!" ) + return params - # check that the neurons - self._check_n_neurons(params, X, spike_data) + @staticmethod + def _check_input_dimensionality(X: Optional[jnp.ndarray] = None, + spike_data: Optional[jnp.ndarray] = None): + if not (spike_data is None): + if spike_data.ndim != 2: + raise ValueError( + "spike_data must be two-dimensional, with shape (n_timebins, n_neurons)" + ) + if not (X is None): + if X.ndim != 3: + raise ValueError( + "X must be three-dimensional, with shape (n_timebins, n_neurons, n_features)" + ) - if spike_data.ndim != 2: - raise ValueError( - "spike_data must be two-dimensional, with shape (n_timebins, n_neurons)" - ) - if X.ndim != 3: + @staticmethod + def _check_input_and_params_consistency(params: Tuple[jnp.ndarray, jnp.ndarray], + X: Optional[jnp.ndarray] = None, + spike_data: Optional[jnp.ndarray] = None): + """ + Validate the number of neurons in model parameters and input arguments. + + Raises: + ------ + ValueError + - if the number of neurons is consistent across the model parameters (`params`) and + any additional inputs (`X` or `spike_data` when provided). + - if the number of features is inconsistent between params[1] and X (when provided). + + """ + n_neurons = params[0].shape[0] + if n_neurons != params[1].shape[0]: raise ValueError( - "X must be three-dimensional, with shape (n_timebins, n_neurons, n_features)" + "Model parameters have inconsistent shapes. " + "Spike basis coefficients must be of shape (n_neurons, n_features), and " + "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both! " + f"Coefficients n_neurons: {params[0].shape[0]}, bias n_neurons: {params[1].shape[0]}" ) - self._check_n_features(params[0], X) - def predict(self, X: NDArray) -> jnp.ndarray: + if spike_data is not None: + if spike_data.shape[1] != n_neurons: + raise ValueError( + "The number of neuron in the model parameters and in the inputs" + "must match." + f"parameters has n_neurons: {n_neurons}, " + f"the input provided has n_neurons: {spike_data.shape[1]}" + ) + + if X is not None: + if X.shape[1] != n_neurons: + raise ValueError( + "The number of neuron in the model parameters and in the inputs" + "must match." + f"parameters has n_neurons: {n_neurons}, " + f"the input provided has n_neurons: {X.shape[1]}" + ) + if params[0].shape[1] != X.shape[2]: + raise ValueError( + "Inconsistent number of features. " + f"spike basis coefficients has {params[0].shape[1]} features, " + f"X has {X.shape[2]} features instead!" + ) + + @staticmethod + def _check_input_n_timepoints(X: jnp.ndarray, spike_data:jnp.ndarray): + if X.shape[0] != spike_data.shape[0]: + raise ValueError("The number of time-points in X and spike_data must agree. " + f"X has {X.shape[0]} time-points, " + f"spike_data has {spike_data.shape[0]} instead!") + + def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: """Predict firing rates based on fit parameters. Parameters ---------- - X : NDArray + X : The exogenous variables. Shape (n_time_bins, n_neurons, n_features). Returns @@ -336,16 +357,21 @@ def predict(self, X: NDArray) -> jnp.ndarray: simulate Simulate spikes using GLM as a recurrent network, for extrapolating into the future. """ + # check that the model is fitted self._check_is_fit() + # extract model params Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self._check_n_neurons((Ws, bs), X) - self._check_n_features(Ws, X) + # check input dimensionality + self._check_input_dimensionality(X=X) + # check consistency between X and params + self._check_input_and_params_consistency((Ws, bs), X=X) + X, = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) return self._predict((Ws, bs), X) def score(self, - X: NDArray, - spike_data: NDArray, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. @@ -377,20 +403,24 @@ def score(self, where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate - of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. + of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. See [1]. Parameters ---------- - X : (n_time_bins, n_neurons, n_features) - The exogenous variables. - spike_data : (n_time_bins, n_neurons) + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features) + spike_data : Spike counts arranged in a matrix. n_neurons must be the same as - during the fitting of this GLM instance. - + during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). + score_type: + String indicating the type of scoring to return. Options are: + - `log-likelihood` for the model log-likelihood. + - `pseudo-r2` for the model pseudo-$R^2$. + Default is defined at class initialization. Returns ------- score : (1,) - The Poisson log-likehood or the pseudo-$R^2$ of the current model. + The Poisson log-likelihood or the pseudo-$R^2$ of the current model. Raises ------ @@ -410,6 +440,12 @@ def score(self, The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. + + References + ---------- + [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. + Routledge, 2013. + """ # ignore the last time point from predict, because that corresponds to @@ -417,19 +453,23 @@ def score(self, self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self._check_n_neurons((Ws, bs), X, spike_data) - self._check_n_features(Ws, X) + self._check_input_dimensionality(X, spike_data) + self._check_input_n_timepoints(X, spike_data) + self._check_input_and_params_consistency((Ws, bs), X=X, spike_data=spike_data) + + X, spike_data = self._convert_to_jnp_ndarray(X, spike_data, + data_type=jnp.float32) if score_type is None: score_type = self.score_type if score_type == "log-likelihood": score = -( - self._score(X, spike_data, (Ws, bs)) - + jax.scipy.special.gammaln(spike_data + 1).mean() + self._score(X, spike_data, (Ws, bs)) + + jax.scipy.special.gammaln(spike_data + 1).mean() ) elif score_type == "pseudo-r2": - score = self._pseudo_r2((Ws,bs), X, spike_data) + score = self._pseudo_r2((Ws, bs), X, spike_data) else: # this should happen only if one manually set score_type raise NotImplementedError( @@ -439,10 +479,10 @@ def score(self, @abc.abstractmethod def fit( - self, - X: NDArray, - spike_data: NDArray, - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + self, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, ): """Fit GLM to spiking data. @@ -473,13 +513,13 @@ def fit( pass def simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_spikes: NDArray, - coupling_basis_matrix: NDArray, - feedforward_input: NDArray, - device: str = "cpu", + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_spikes: Union[NDArray, jnp.ndarray], + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + device: str = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using the GLM as a recurrent network. @@ -550,15 +590,28 @@ def simulate( coupling_basis_matrix = jax.device_put(coupling_basis_matrix, target_device) feedforward_input = jax.device_put(feedforward_input, target_device) - self._check_is_fit() + n_basis_coupling = coupling_basis_matrix.shape[1] + n_neurons = self.baseline_link_fr_.shape[0] + + # add an empty input (simulate with coupling-only) + if feedforward_input is None: + feedforward_input = jnp.zeros((n_timesteps, n_neurons, 0), dtype=jnp.float32) + + + self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self._check_n_neurons((Ws, bs), feedforward_input, init_spikes) + + self._check_input_dimensionality(feedforward_input, init_spikes) + + self._check_input_and_params_consistency((Ws[:, n_basis_coupling*n_neurons:], bs), + X=feedforward_input, + spike_data=init_spikes) if ( - feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] - != Ws.shape[1] + feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] + != Ws.shape[1] ): raise ValueError( "The number of feed forward input features" @@ -576,10 +629,14 @@ def simulate( f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" ) + if feedforward_input.shape[0] != n_timesteps: + raise ValueError("`feedforward_input` must be of length `n_timesteps`. " + f"`feedforward_input` has length {len(feedforward_input)}, " + f"`n_timesteps` is {n_timesteps} instead!") subkeys = jax.random.split(random_key, num=n_timesteps) def scan_fn( - data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray + data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray ) -> Tuple[Tuple[NDArray, int], NDArray]: """Function to scan over time steps and simulate spikes and firing rates. @@ -647,12 +704,13 @@ class PoissonGLM(PoissonGLMBase): baseline_log_fr : jnp.ndarray, (n_neurons,) Solutions for bias terms, set during ``fit()`` """ + def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", ): super().__init__( solver_name=solver_name, @@ -662,10 +720,10 @@ def __init__( ) def fit( - self, - X: NDArray, - spike_data: NDArray, - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + self, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): """Fit GLM to spiking data. @@ -685,15 +743,26 @@ def fit( Raises ------ ValueError - - If `params` is not a JAX pytree of size two. - - If shapes of init_params are not correct. + - If `init_params` is not of length two. + - If dimensionality of `init_params` are not correct. - If the number of neurons in the model parameters and in the inputs do not match. - If `X` is not three-dimensional. - If spike_data is not two-dimensional. - If solver returns at least one NaN parameter, which means it found an invalid solution. Try tuning optimization hyperparameters. + TypeError + - If `init_params` are not array-like + - If `init_params[i]` cannot be converted to jnp.ndarray for all i """ + # check input dimensionality + self._check_input_dimensionality(X, spike_data) + self._check_input_n_timepoints(X, spike_data) + + # convert to jnp.ndarray of floats + X, spike_data = self._convert_to_jnp_ndarray(X, spike_data, + data_type=jnp.float32) + _, n_neurons = spike_data.shape n_features = X.shape[2] @@ -705,8 +774,11 @@ def fit( # bs, bias terms jnp.log(jnp.mean(spike_data, axis=0)), ) - - self._check_params(init_params, X, spike_data) + else: + # check parameter length, shape and dimensionality, convert to jnp.ndarray. + init_params = self._check_and_convert_params(init_params) + # check that the inputs and the parameters has consistent sizes + self._check_input_and_params_consistency(init_params, X, spike_data) def loss(params, X, y): return self._score(X, y, params) diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py index ac2b4338..05829dd3 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/model_base.py @@ -7,7 +7,7 @@ import inspect import warnings from collections import defaultdict -from typing import Tuple, Any +from typing import Tuple, Union import jax.numpy as jnp from numpy.typing import NDArray @@ -137,15 +137,17 @@ def _get_param_names(cls): return sorted([p.name for p in parameters]) @abc.abstractmethod - def fit(self, X: NDArray, y: NDArray): + def fit(self, X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray]): pass @abc.abstractmethod - def predict(self, X: NDArray) -> jnp.ndarray: + def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass @abc.abstractmethod - def score(self, X: NDArray, y: NDArray) -> jnp.ndarray: + def score(self, X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass @abc.abstractmethod @@ -155,8 +157,15 @@ def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.n @abc.abstractmethod def _score( self, - X: NDArray, - y: NDArray, + X: jnp.ndarray, + y: jnp.ndarray, params: Tuple[jnp.ndarray, jnp.ndarray], ) -> jnp.ndarray: pass + + @staticmethod + def _convert_to_jnp_ndarray(*args: Union[NDArray, jnp.ndarray], + data_type: jnp.dtype = jnp.float32) \ + -> Tuple[jnp.ndarray, ...]: + return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) + diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..f2a2dc4b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,36 @@ +import pytest + +import yaml +import jax + +import numpy as np +import jax.numpy as jnp + +import neurostatslib as nsl + + +@pytest.fixture +def poissonGLM_model_instantiation(): + np.random.seed(123) + X = np.random.normal(size=(100, 1, 5)) + b_true = np.zeros((1, )) + w_true = np.random.normal(size=(1, 5)) + model = nsl.glm.PoissonGLM(inverse_link_function=jax.numpy.exp) + rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) + return X, np.random.poisson(rate), model, (w_true, b_true), rate + + +@pytest.fixture +def poissonGLM_coupled_model_config_simulate(): + + with open("simulate_coupled_neurons_params.yml", "r") as fh: + config_dict = yaml.safe_load(fh) + + model = nsl.glm.PoissonGLM(inverse_link_function=jax.numpy.exp) + model.basis_coeff_ = jnp.asarray(config_dict["basis_coeff_"]) + model.baseline_link_fr_ = jnp.asarray(config_dict["baseline_link_fr_"]) + coupling_basis = jnp.asarray(config_dict["coupling_basis"]) + feedforward_input = jnp.asarray(config_dict["feedforward_input"]) + init_spikes = jnp.asarray(config_dict["init_spikes"]) + + return model, coupling_basis, feedforward_input, init_spikes, jax.random.PRNGKey(123) diff --git a/tests/test_glm.py b/tests/test_glm.py index cd485082..695ed537 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,12 +1,19 @@ import pytest -import jaxopt +from typing import Literal, Callable + +import jax, jaxopt import jax.numpy as jnp +import numpy as np +import statsmodels.api as sm import neurostatslib as nsl -class TestPoissonGLM: +class TestPoissonGLM: + ####################### + # Test model.__init__ + ####################### @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize", "NotPresent"]) def test_init_solver_name(self, solver_name: str): try: @@ -22,8 +29,8 @@ def test_init_solver_name(self, solver_name: str): @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize"]) @pytest.mark.parametrize("solver_kwargs", [ - {"tol":1, "verbose":1, "maxiter":1}, - {"tol":1, "maxiter":1}]) + {"tol": 1, "verbose": 1, "maxiter": 1}, + {"tol": 1, "maxiter": 1}]) def test_init_solver_kwargs(self, solver_name, solver_kwargs): raise_exception = (solver_name == "ScipyMinimize") & ("verbose" in solver_kwargs) if raise_exception: @@ -34,36 +41,568 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): nsl.glm.PoissonGLM(solver_name, solver_kwargs=solver_kwargs) getattr(jaxopt, solver_name)(fun=lambda x: x, **solver_kwargs) - @pytest.mark.parametrize("func", [1, "string", lambda x:x, jnp.exp]) - def test_init_callable(self, func): + @pytest.mark.parametrize("func", [1, "string", lambda x: x, jnp.exp]) + def test_init_callable(self, func: Callable[[jnp.ndarray], jnp.ndarray]): if not callable(func): with pytest.raises(ValueError, match="inverse_link_function must be a callable"): nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) else: nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) - @pytest.mark.parametrize("score_type", [1, "ll", "log-likelihood","pseudo-r2"]) - def test_init_score_type(self, score_type: str): - if score_type not in ["log-likelihood","pseudo-r2"]: + @pytest.mark.parametrize("score_type", [1, "ll", "log-likelihood", "pseudo-r2"]) + def test_init_score_type(self, score_type: Literal["log-likelihood", "pseudo-r2"]): + if score_type not in ["log-likelihood", "pseudo-r2"]: with pytest.raises(NotImplementedError, match="Scoring method not implemented."): nsl.glm.PoissonGLM("BFGS", score_type=score_type) else: nsl.glm.PoissonGLM("BFGS", score_type=score_type) - def test_fit(self): - pass + ####################### + # Test model.fit + ####################### + @pytest.mark.parametrize("n_params", [0, 1, 2, 3]) + def test_fit_param_length(self, n_params, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.log(y.mean(axis=0)) + if n_params == 0: + init_params = tuple() + elif n_params == 1: + init_params = (init_w,) + elif n_params == 2: + init_params = (init_w, init_b) + else: + init_params = (init_w, init_b) + (init_w,) * (n_params - 2) + + raise_exception = n_params != 2 + if raise_exception: + with pytest.raises(ValueError, match="Params needs to be array-like of length two."): + model.fit(X, y, init_params=init_params) + else: + model.fit(X, y, init_params=init_params) + + @pytest.mark.parametrize("dim_weights", [0, 1, 2, 3]) + def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + if dim_weights == 0: + init_w = jnp.array([]) + elif dim_weights == 1: + init_w = jnp.zeros((n_neurons,)) + elif dim_weights == 2: + init_w = jnp.zeros((n_neurons, n_features)) + else: + init_w = jnp.zeros((n_neurons, n_features) + (1,) * (dim_weights - 2)) + init_b = jnp.log(y.mean(axis=0)) + raise_exception = dim_weights != 2 + if raise_exception: + with pytest.raises(ValueError, match="params\[0\] term must be of shape \(n_neurons, n_features\)"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("dim_intercepts", [0, 1, 2, 3]) + def test_fit_intercepts_dimensionality(self, dim_intercepts, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + + init_b = jnp.zeros((n_neurons,) * dim_intercepts) + init_w = jnp.zeros((n_neurons, n_features)) + raise_exception = dim_intercepts != 1 + if raise_exception: + with pytest.raises(ValueError, match="params\[1\] term must be of shape"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("init_params", + [dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), + [jnp.zeros((1, 5)), jnp.zeros((1,))], + dict(p1=jnp.zeros((1, 5)), p2=np.zeros((1,), dtype='U10')), + 0, + {0, 1}, + iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), + [jnp.zeros((1, 5)), ""], + ["", jnp.zeros((1,))]]) + def test_fit_init_params_type(self, init_params, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # check if parameter can be converted + try: + tuple(jnp.asarray(par, dtype=jnp.float32) for par in init_params) + # ensure that it's an array-like (for example excluding sets and iterators) + raise_exception = not hasattr(init_params, "__getitem__") + except(TypeError, ValueError): + raise_exception = True + + if raise_exception: + with pytest.raises(TypeError, match="Initial parameters must be array-like"): + model.fit(X, y, init_params=init_params) + else: + model.fit(X, y, init_params=init_params) + + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_fit_n_neuron_match_weights(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons + delta_n_neuron, n_features)) + init_b = jnp.zeros((n_neurons, )) + # model.basis_coeff_ = init_w + # model.baseline_link_fr_ = init_b + if raise_exception: + with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): + model.fit(X, y, init_params=(init_w, init_b)) + # with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): + # model.predict(X) + # with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): + # model.score(X, y) + else: + model.fit(X, y, init_params=(init_w, init_b)) + # model.predict(X) + # model.score(X, y) + + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons + delta_n_neuron,)) + + if raise_exception: + with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): + model.fit(X, y, init_params=(init_w, init_b)) + + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_fit_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_fit_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_fit_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + raise_exception = delta_dim != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + + if delta_dim == -1: + # remove a dimension + X = np.zeros((n_samples, n_neurons)) + elif delta_dim == 1: + # add a dimension + X = np.zeros((n_samples, n_neurons, n_features, 1)) + + if raise_exception: + with pytest.raises(ValueError, match="X must be three-dimensional"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_fit_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + raise_exception = delta_dim != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + + if delta_dim == -1: + # remove a dimension + y = np.zeros((n_samples, )) + elif delta_dim == 1: + # add a dimension + y = np.zeros((n_samples, n_neurons, 1)) + + if raise_exception: + with pytest.raises(ValueError, match="spike_data must be two-dimensional"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) + def test_fit_n_feature_consistency_weights(self, delta_n_features, poissonGLM_model_instantiation): + raise_exception = delta_n_features != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + # add/remove a feature from weights + init_w = jnp.zeros((n_neurons, n_features + delta_n_features)) + init_b = jnp.zeros((n_neurons,)) + + if raise_exception: + with pytest.raises(ValueError, match="Inconsistent number of features"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) + def test_fit_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + raise_exception = delta_n_features != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + + if delta_n_features == 1: + # add a feature + X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + elif delta_n_features == -1: + # remove a feature + X = X[..., :-1] + + if raise_exception: + with pytest.raises(ValueError, match="Inconsistent number of features"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + ####################### + # Test model.score + ####################### + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_score_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.score(X, y) + else: + model.score(X, y) + + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_score_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.score(X, y) + else: + model.score(X, y) + + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_score_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + raise_exception = delta_dim != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + + if delta_dim == -1: + # remove a dimension + X = np.zeros((n_samples, n_neurons)) + elif delta_dim == 1: + # add a dimension + X = np.zeros((n_samples, n_neurons, n_features, 1)) + + if raise_exception: + with pytest.raises(ValueError, match="X must be three-dimensional"): + model.score(X, y) + else: + model.score(X, y) + + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + raise_exception = delta_dim != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, _ = X.shape + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + + if delta_dim == -1: + # remove a dimension + y = np.zeros((n_samples,)) + elif delta_dim == 1: + # add a dimension + y = np.zeros((n_samples, n_neurons, 1)) + + if raise_exception: + with pytest.raises(ValueError, match="spike_data must be two-dimensional"): + model.score(X, y) + else: + model.score(X, y) + + @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) + def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + raise_exception = delta_n_features != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + if delta_n_features == 1: + # add a feature + X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + elif delta_n_features == -1: + # remove a feature + X = X[..., :-1] + + if raise_exception: + with pytest.raises(ValueError, match="Inconsistent number of features"): + model.score(X, y) + else: + model.score(X, y) + + def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + # get the rate + mean_firing = model.predict(X) + # compute the log-likelihood using jax.scipy + mean_ll_jax = jax.scipy.stats.poisson.logpmf(y, mean_firing).mean() + model_ll = model.score(X, y, score_type="log-likelihood") + if not np.allclose(mean_ll_jax, model_ll): + raise ValueError("Log-likelihood of PoissonModel does not match" + "that of jax.scipy!") + + + ####################### + # Test model.predict + ####################### + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_predict_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + raise_exception = delta_n_neuron != 0 + X, _, model, true_params, _ = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.predict(X) + else: + model.predict(X) + + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_predict_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + raise_exception = delta_dim != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + + if delta_dim == -1: + # remove a dimension + X = np.zeros((n_samples, n_neurons)) + elif delta_dim == 1: + # add a dimension + X = np.zeros((n_samples, n_neurons, n_features, 1)) + + if raise_exception: + with pytest.raises(ValueError, match="X must be three-dimensional"): + model.predict(X) + else: + model.predict(X) + + @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) + def test_predict_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + raise_exception = delta_n_features != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + if delta_n_features == 1: + # add a feature + X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + elif delta_n_features == -1: + # remove a feature + X = X[..., :-1] + + if raise_exception: + with pytest.raises(ValueError, match="Inconsistent number of features"): + model.predict(X) + else: + model.predict(X) + + ####################### + # Test model.simulate + ####################### + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_simulate_n_neuron_match_input(self, delta_n_neuron, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_n_neuron != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + n_neurons, n_features = model.basis_coeff_.shape + n_time_points, _, n_basis_input = feedforward_input.shape + if delta_n_neuron != 0: + feedforward_input = np.zeros((n_time_points, n_neurons+delta_n_neuron, n_basis_input)) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.simulate(random_key=random_key, + n_timesteps=n_time_points, + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=n_time_points, + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") - def test_score(self): - pass + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_score_input_dimensionality(self, delta_dim, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_dim != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + if delta_dim == -1: + # remove a dimension + feedforward_input = np.zeros(feedforward_input.shape[:2]) + elif delta_dim == 1: + # add a dimension + feedforward_input = np.zeros(feedforward_input.shape + (1,)) - def test_predict(self): - pass + if raise_exception: + with pytest.raises(ValueError, match="X must be three-dimensional"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) + def test_score_y_dimensionality(self, delta_dim, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_dim != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + n_samples, n_neurons = feedforward_input.shape[:2] + if delta_dim == -1: + # remove a dimension + init_spikes = np.zeros((n_samples,)) + elif delta_dim == 1: + # add a dimension + init_spikes = np.zeros((n_samples, n_neurons, 1)) + + if raise_exception: + with pytest.raises(ValueError, match="spike_data must be two-dimensional"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + # + # @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) + # def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + # raise_exception = delta_n_features != 0 + # X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # # set model coeff + # model.basis_coeff_ = true_params[0] + # model.baseline_link_fr_ = true_params[1] + # if delta_n_features == 1: + # # add a feature + # X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + # elif delta_n_features == -1: + # # remove a feature + # X = X[..., :-1] + # + # if raise_exception: + # with pytest.raises(ValueError, match="Inconsistent number of features"): + # model.score(X, y) + # else: + # model.score(X, y) - def test_simulate(self): - pass + def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + # get the rate + mean_firing = model.predict(X) + # compute the log-likelihood using jax.scipy + mean_ll_jax = jax.scipy.stats.poisson.logpmf(y, mean_firing).mean() + model_ll = model.score(X, y, score_type="log-likelihood") + if not np.allclose(mean_ll_jax, model_ll): + raise ValueError("Log-likelihood of PoissonModel does not match" + "that of jax.scipy!") - def test_compare_to_scikitlearn(self): - pass + def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set model coeff + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + # get the rate + dev = sm.families.Poisson().deviance(y, firing_rate) + dev_model = model._residual_deviance(firing_rate, y).sum() + if np.allclose(dev, dev_model): + raise ValueError("Deviance doesn't match statsmodels!") + def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + glm_sm = sm.GLM(endog=y[:, 0], + exog=sm.add_constant(X[:, 0]), + family=sm.families.Poisson()) + res_sm = glm_sm.fit() + fit_params_sm = res_sm.params + # use a second order method for precision, match non-linearity + model.set_params(inverse_link_function=jnp.exp, + solver_name="BFGS", + solver_kwargs={"tol": 10**-8}) + model.fit(X, y) + fit_params_model = jnp.hstack((model.baseline_link_fr_, + model.basis_coeff_.flatten())) + if not np.allclose(fit_params_sm, fit_params_model): + raise ValueError("Fitted parameters do not match that of statsmodels!") From d88ee96f61ad07d479ec2db889062364d9f1540b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 16:20:50 -0400 Subject: [PATCH 016/250] test completed, need docstrings --- src/neurostatslib/glm.py | 20 +- tests/simulate_coupled_neurons_params.yml | 6292 +++++++++++++++++++++ tests/test_glm.py | 316 +- 3 files changed, 6582 insertions(+), 46 deletions(-) create mode 100644 tests/simulate_coupled_neurons_params.yml diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index da9bf723..47078282 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -584,6 +584,8 @@ def simulate( target_device = jax.devices("gpu")[0] else: raise ValueError(f"Invalid device: {device}. Choose 'cpu' or 'gpu'.") + # check if the model is fit + self._check_is_fit() # Transfer data to the target device init_spikes = jax.device_put(init_spikes, target_device) @@ -597,17 +599,10 @@ def simulate( if feedforward_input is None: feedforward_input = jnp.zeros((n_timesteps, n_neurons, 0), dtype=jnp.float32) - - - self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ self._check_input_dimensionality(feedforward_input, init_spikes) - - self._check_input_and_params_consistency((Ws[:, n_basis_coupling*n_neurons:], bs), - X=feedforward_input, - spike_data=init_spikes) if ( feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] @@ -621,12 +616,17 @@ def simulate( f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " f"provided instead." ) + + self._check_input_and_params_consistency((Ws[:, n_basis_coupling*n_neurons:], bs), + X=feedforward_input, + spike_data=init_spikes) if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( - "init_spikes has the wrong number of time steps!" - f"init_spikes time steps: {init_spikes.shape[1]}, " - f"spike_basis_matrix window size: {coupling_basis_matrix.shape[1]}" + "`init_spikes` and `coupling_basis_matrix`" + " should have the same window size! " + f"`init_spikes` window size: {init_spikes.shape[1]}, " + f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" ) if feedforward_input.shape[0] != n_timesteps: diff --git a/tests/simulate_coupled_neurons_params.yml b/tests/simulate_coupled_neurons_params.yml new file mode 100644 index 00000000..1be6c9cc --- /dev/null +++ b/tests/simulate_coupled_neurons_params.yml @@ -0,0 +1,6292 @@ +baseline_link_fr_: +- -3.0 +- -3.0 +basis_coeff_: +- - -0.004372 + - -0.02786 + - -0.04582 + - -0.0588 + - -0.06539 + - -0.06396 + - -0.05328 + - -0.03192 + - 0.0002296 + - 0.04143 + - 0.08794 + - 0.1483 + - 0.2053 + - 0.2483 + - 0.2892 + - 0.3093 + - 0.2917 + - 0.2225 + - 0.07357 + - -0.2711 + - -0.006235 + - -0.01047 + - 0.02189 + - 0.058 + - 0.09002 + - 0.1118 + - 0.1209 + - 0.1167 + - 0.09909 + - 0.07044 + - 0.03448 + - -0.01565 + - -0.06823 + - -0.1128 + - -0.1655 + - -0.2176 + - -0.2621 + - -0.2982 + - -0.3255 + - -0.3449 + - 0.5 + - 0.5 +- - -0.004637 + - 0.02223 + - 0.07071 + - 0.09572 + - 0.1012 + - 0.08923 + - 0.06464 + - 0.03076 + - -0.007911 + - -0.04737 + - -0.08429 + - -0.1249 + - -0.1582 + - -0.1827 + - -0.2081 + - -0.23 + - -0.2473 + - -0.2616 + - -0.2741 + - -0.287 + - 0.01127 + - 0.04864 + - 0.0544 + - 0.05082 + - 0.03975 + - 0.02393 + - 0.004725 + - -0.01763 + - -0.04202 + - -0.06744 + - -0.09269 + - -0.1231 + - -0.1522 + - -0.1763 + - -0.2051 + - -0.2348 + - -0.2629 + - -0.2896 + - -0.3149 + - -0.3389 + - 0.5 + - 0.5 +coupling_basis: +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0024979173609873673 + - 0.9975020826390129 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.11451325277931029 + - 0.8854867472206909 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.25013898844998006 + - 0.7498610115500185 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.3122501403134024 + - 0.687749859686596 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.28176761370807446 + - 0.7182323862919272 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.17383844924397923 + - 0.8261615507560222 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.04364762794083282 + - 0.9563523720591665 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9912618171282106 + - 0.008738182871789013 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.7892946476427273 + - 0.21070535235727128 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.3531647741677867 + - 0.6468352258322151 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.011883820048045501 + - 0.9881161799519544 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.7841665801263835 + - 0.21583341987361648 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.17688067665784446 + - 0.8231193233421555 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9253003862638604 + - 0.0746996137361397 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.2549435480705588 + - 0.7450564519294413 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9205258993369989 + - 0.07947410066300109 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.16827351931758228 + - 0.8317264806824178 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.7835282009408713 + - 0.21647179905912872 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.019118847416525586 + - 0.9808811525834744 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.4372031242218587 + - 0.5627968757781414 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9120243919870162 + - 0.08797560801298382 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.044222034278324274 + - 0.9557779657216758 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.40793669708774605 + - 0.5920633029122541 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.8283923698925478 + - 0.17160763010745222 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9999802058373224 + - 1.9794162677666538e-05 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.1458111022283093 + - 0.8541888977716907 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.4778824971400245 + - 0.5221175028599756 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.803486827077907 + - 0.19651317292209308 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.9824675828481839 + - 0.017532417151816082 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.029720664099906924 + - 0.9702793359000932 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.19724020774947038 + - 0.8027597922505296 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.44389603578613035 + - 0.5561039642138698 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.6909694421867117 + - 0.30903055781328825 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.8804498633788072 + - 0.1195501366211929 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.9828262050955638 + - 0.017173794904436157 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.005816278861877466 + - 0.9941837211381226 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.07171948190677246 + - 0.9282805180932275 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.19211081158089233 + - 0.8078891884191077 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.3422365913893123 + - 0.6577634086106878 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.49997219806462273 + - 0.5000278019353773 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.6481581380891199 + - 0.3518418619108801 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.775227808426499 + - 0.22477219157350103 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.8747644272334134 + - 0.12523557276658664 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.9445228823471115 + - 0.05547711765288865 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.9852942394771702 + - 0.014705760522829736 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.9998405276097415 + - 0.00015947239025848603 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.00798856965539202 + - 0.9920114303446079 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.03392307742054024 + - 0.9660769225794598 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.07373523476821137 + - 0.9262647652317886 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.12352988337197751 + - 0.8764701166280225 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.17990211564285485 + - 0.8200978843571451 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.2399997347398921 + - 0.7600002652601079 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.3015222924967669 + - 0.6984777075032332 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.36268149196393995 + - 0.63731850803606 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.42214108290743424 + - 0.5778589170925659 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.47894873221112266 + - 0.5210512677888774 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.5324679173051469 + - 0.46753208269485313 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.5823146093533313 + - 0.4176853906466687 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.6283012081735033 + - 0.3716987918264968 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.6703886551778314 + - 0.32961134482216864 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.7086466881407022 + - 0.2913533118592979 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.7432216468423799 + - 0.25677835315762026 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.7743109612271127 + - 0.22568903877288732 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.802143356101582 + - 0.197856643898418 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.82696381862707 + - 0.17303618137292998 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.8490224486822571 + - 0.15097755131774288 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.8685664156253453 + - 0.13143358437465474 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.8858343578296817 + - 0.11416564217031833 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9010526715389762 + - 0.09894732846102389 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9144332365128198 + - 0.08556676348718023 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9261722145965264 + - 0.07382778540347357 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9364496329422705 + - 0.06355036705772948 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9454295266061546 + - 0.05457047339384541 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9532604668007324 + - 0.04673953319926766 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9600763426393057 + - 0.039923657360694254 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9659972972699125 + - 0.03400270273008754 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.971130745291511 + - 0.028869254708488945 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.975572418558468 + - 0.024427581441531954 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9794074030288873 + - 0.020592596971112653 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9827111411428311 + - 0.017288858857168965 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9855503831123861 + - 0.014449616887613925 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9879840771076767 + - 0.012015922892323394 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9900641931482845 + - 0.009935806851715523 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9918364789707291 + - 0.008163521029270815 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9933411485659462 + - 0.006658851434053759 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9946135057219054 + - 0.005386494278094567 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9956845059646938 + - 0.004315494035306178 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9965812609202838 + - 0.0034187390797163486 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.997327489436671 + - 0.002672510563328956 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9979439199017871 + - 0.002056080098212898 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9984486481342357 + - 0.0015513518657642722 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9988574550621354 + - 0.0011425449378646424 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9991840881776304 + - 0.0008159118223696749 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.999440510488429 + - 0.0005594895115710874 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9996371204027914 + - 0.00036287959720865404 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.999782945694725 + - 0.00021705430527496627 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9998858144113889 + - 0.00011418558861114869 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9999525053112863 + - 4.7494688713622946e-05 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.99998888016377 + - 1.1119836230089053e-05 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 1.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +feedforward_input: +- - - 0.0 + - 1.0 + - - 0.0 + - 1.0 +- - - 0.012578617838741058 + - 0.9999208860571255 + - - 0.012578617838741058 + - 0.9999208860571255 +- - - 0.025155245389375847 + - 0.9996835567465339 + - - 0.025155245389375847 + - 0.9996835567465339 +- - - 0.03772789267871718 + - 0.99928804962034 + - - 0.03772789267871718 + - 0.99928804962034 +- - - 0.05029457036336618 + - 0.9987344272588006 + - - 0.05029457036336618 + - 0.9987344272588006 +- - - 0.06285329004448194 + - 0.9980227772604111 + - - 0.06285329004448194 + - 0.9980227772604111 +- - - 0.07540206458240159 + - 0.9971532122280464 + - - 0.07540206458240159 + - 0.9971532122280464 +- - - 0.08793890841106125 + - 0.9961258697511429 + - - 0.08793890841106125 + - 0.9961258697511429 +- - - 0.10046183785216795 + - 0.9949409123839288 + - - 0.10046183785216795 + - 0.9949409123839288 +- - - 0.11296887142907283 + - 0.9935985276197029 + - - 0.11296887142907283 + - 0.9935985276197029 +- - - 0.12545803018029603 + - 0.9920989278611685 + - - 0.12545803018029603 + - 0.9920989278611685 +- - - 0.13792733797265358 + - 0.9904423503868246 + - - 0.13792733797265358 + - 0.9904423503868246 +- - - 0.1503748218139367 + - 0.9886290573134227 + - - 0.1503748218139367 + - 0.9886290573134227 +- - - 0.1627985121650943 + - 0.986659335554492 + - - 0.1627985121650943 + - 0.986659335554492 +- - - 0.17519644325186898 + - 0.984533496774942 + - - 0.17519644325186898 + - 0.984533496774942 +- - - 0.18756665337583714 + - 0.9822518773417481 + - - 0.18756665337583714 + - 0.9822518773417481 +- - - 0.19990718522480458 + - 0.9798148382707295 + - - 0.19990718522480458 + - 0.9798148382707295 +- - - 0.21221608618250787 + - 0.9772227651694256 + - - 0.21221608618250787 + - 0.9772227651694256 +- - - 0.22449140863757258 + - 0.9744760681760832 + - - 0.22449140863757258 + - 0.9744760681760832 +- - - 0.23673121029167973 + - 0.9715751818947602 + - - 0.23673121029167973 + - 0.9715751818947602 +- - - 0.2489335544668916 + - 0.9685205653265598 + - - 0.2489335544668916 + - 0.9685205653265598 +- - - 0.2610965104120882 + - 0.9653127017970033 + - - 0.2610965104120882 + - 0.9653127017970033 +- - - 0.27321815360846585 + - 0.9619520988795548 + - - 0.27321815360846585 + - 0.9619520988795548 +- - - 0.28529656607404974 + - 0.9584392883153087 + - - 0.28529656607404974 + - 0.9584392883153087 +- - - 0.2973298366671723 + - 0.9547748259288535 + - - 0.2973298366671723 + - 0.9547748259288535 +- - - 0.30931606138886886 + - 0.9509592915403253 + - - 0.30931606138886886 + - 0.9509592915403253 +- - - 0.32125334368414366 + - 0.9469932888736633 + - - 0.32125334368414366 + - 0.9469932888736633 +- - - 0.33313979474205757 + - 0.9428774454610842 + - - 0.33313979474205757 + - 0.9428774454610842 +- - - 0.34497353379459045 + - 0.9386124125437894 + - - 0.34497353379459045 + - 0.9386124125437894 +- - - 0.3567526884142317 + - 0.9341988649689198 + - - 0.3567526884142317 + - 0.9341988649689198 +- - - 0.3684753948102499 + - 0.9296375010827771 + - - 0.3684753948102499 + - 0.9296375010827771 +- - - 0.38013979812359666 + - 0.924929042620325 + - - 0.38013979812359666 + - 0.924929042620325 +- - - 0.3917440527203973 + - 0.9200742345909914 + - - 0.3917440527203973 + - 0.9200742345909914 +- - - 0.4032863224839812 + - 0.915073845160786 + - - 0.4032863224839812 + - 0.915073845160786 +- - - 0.41476478110540693 + - 0.9099286655307568 + - - 0.41476478110540693 + - 0.9099286655307568 +- - - 0.4261776123724353 + - 0.9046395098117981 + - - 0.4261776123724353 + - 0.9046395098117981 +- - - 0.4375230104569043 + - 0.8992072148958368 + - - 0.4375230104569043 + - 0.8992072148958368 +- - - 0.4487991802004621 + - 0.8936326403234123 + - - 0.4487991802004621 + - 0.8936326403234123 +- - - 0.46000433739861096 + - 0.887916668147673 + - - 0.46000433739861096 + - 0.887916668147673 +- - - 0.47113670908301786 + - 0.8820602027948115 + - - 0.47113670908301786 + - 0.8820602027948115 +- - - 0.4821945338020477 + - 0.8760641709209582 + - - 0.4821945338020477 + - 0.8760641709209582 +- - - 0.4931760618994744 + - 0.8699295212655597 + - - 0.4931760618994744 + - 0.8699295212655597 +- - - 0.5040795557913246 + - 0.8636572245012607 + - - 0.5040795557913246 + - 0.8636572245012607 +- - - 0.5149032902408126 + - 0.8572482730803168 + - - 0.5149032902408126 + - 0.8572482730803168 +- - - 0.5256455526313207 + - 0.8507036810775614 + - - 0.5256455526313207 + - 0.8507036810775614 +- - - 0.5363046432373825 + - 0.8440244840299503 + - - 0.5363046432373825 + - 0.8440244840299503 +- - - 0.5468788754936273 + - 0.8372117387727107 + - - 0.5468788754936273 + - 0.8372117387727107 +- - - 0.5573665762616421 + - 0.8302665232721208 + - - 0.5573665762616421 + - 0.8302665232721208 +- - - 0.5677660860947078 + - 0.8231899364549453 + - - 0.5677660860947078 + - 0.8231899364549453 +- - - 0.5780757595003707 + - 0.8159830980345546 + - - 0.5780757595003707 + - 0.8159830980345546 +- - - 0.588293965200805 + - 0.8086471483337551 + - - 0.588293965200805 + - 0.8086471483337551 +- - - 0.5984190863909268 + - 0.8011832481043575 + - - 0.5984190863909268 + - 0.8011832481043575 +- - - 0.608449520994217 + - 0.7935925783435149 + - - 0.608449520994217 + - 0.7935925783435149 +- - - 0.6183836819162153 + - 0.7858763401068549 + - - 0.6183836819162153 + - 0.7858763401068549 +- - - 0.6282199972956423 + - 0.7780357543184395 + - - 0.6282199972956423 + - 0.7780357543184395 +- - - 0.6379569107531118 + - 0.7700720615775812 + - - 0.6379569107531118 + - 0.7700720615775812 +- - - 0.647592881637394 + - 0.7619865219625451 + - - 0.647592881637394 + - 0.7619865219625451 +- - - 0.6571263852691885 + - 0.7537804148311695 + - - 0.6571263852691885 + - 0.7537804148311695 +- - - 0.666555913182372 + - 0.7454550386184362 + - - 0.666555913182372 + - 0.7454550386184362 +- - - 0.675879973362679 + - 0.7370117106310213 + - - 0.675879973362679 + - 0.7370117106310213 +- - - 0.6850970904837809 + - 0.7284517668388609 + - - 0.6850970904837809 + - 0.7284517668388609 +- - - 0.6942058061407225 + - 0.7197765616637636 + - - 0.6942058061407225 + - 0.7197765616637636 +- - - 0.7032046790806838 + - 0.7109874677651024 + - - 0.7032046790806838 + - 0.7109874677651024 +- - - 0.7120922854310254 + - 0.7020858758226226 + - - 0.7120922854310254 + - 0.7020858758226226 +- - - 0.720867218924585 + - 0.6930731943163971 + - - 0.720867218924585 + - 0.6930731943163971 +- - - 0.7295280911221884 + - 0.6839508493039657 + - - 0.7295280911221884 + - 0.6839508493039657 +- - - 0.7380735316323389 + - 0.6747202841946927 + - - 0.7380735316323389 + - 0.6747202841946927 +- - - 0.746502188328052 + - 0.6653829595213794 + - - 0.746502188328052 + - 0.6653829595213794 +- - - 0.7548127275607989 + - 0.6559403527091677 + - - 0.7548127275607989 + - 0.6559403527091677 +- - - 0.7630038343715272 + - 0.6463939578417693 + - - 0.7630038343715272 + - 0.6463939578417693 +- - - 0.7710742126987247 + - 0.6367452854250606 + - - 0.7710742126987247 + - 0.6367452854250606 +- - - 0.7790225855834911 + - 0.6269958621480786 + - - 0.7790225855834911 + - 0.6269958621480786 +- - - 0.7868476953715899 + - 0.6171472306414553 + - - 0.7868476953715899 + - 0.6171472306414553 +- - - 0.7945483039124437 + - 0.6072009492333317 + - - 0.7945483039124437 + - 0.6072009492333317 +- - - 0.8021231927550437 + - 0.5971585917027863 + - - 0.8021231927550437 + - 0.5971585917027863 +- - - 0.809571163340744 + - 0.5870217470308187 + - - 0.809571163340744 + - 0.5870217470308187 +- - - 0.8168910371929053 + - 0.5767920191489297 + - - 0.8168910371929053 + - 0.5767920191489297 +- - - 0.8240816561033644 + - 0.566471026685334 + - - 0.8240816561033644 + - 0.566471026685334 +- - - 0.8311418823156935 + - 0.5560604027088476 + - - 0.8311418823156935 + - 0.5560604027088476 +- - - 0.8380705987052264 + - 0.545561794470492 + - - 0.8380705987052264 + - 0.545561794470492 +- - - 0.8448667089558177 + - 0.5349768631428518 + - - 0.8448667089558177 + - 0.5349768631428518 +- - - 0.8515291377333112 + - 0.5243072835572319 + - - 0.8515291377333112 + - 0.5243072835572319 +- - - 0.8580568308556875 + - 0.5135547439386516 + - - 0.8580568308556875 + - 0.5135547439386516 +- - - 0.8644487554598649 + - 0.5027209456387218 + - - 0.8644487554598649 + - 0.5027209456387218 +- - - 0.8707039001651274 + - 0.4918076028664418 + - - 0.8707039001651274 + - 0.4918076028664418 +- - - 0.8768212752331536 + - 0.4808164424169648 + - - 0.8768212752331536 + - 0.4808164424169648 +- - - 0.8827999127246196 + - 0.4697492033983709 + - - 0.8827999127246196 + - 0.4697492033983709 +- - - 0.8886388666523558 + - 0.45860763695649104 + - - 0.8886388666523558 + - 0.45860763695649104 +- - - 0.8943372131310272 + - 0.4473935059978269 + - - 0.8943372131310272 + - 0.4473935059978269 +- - - 0.8998940505233182 + - 0.4361085849106111 + - - 0.8998940505233182 + - 0.4361085849106111 +- - - 0.9053084995825966 + - 0.42475465928404793 + - - 0.9053084995825966 + - 0.42475465928404793 +- - - 0.9105797035920355 + - 0.4133335256257842 + - - 0.9105797035920355 + - 0.4133335256257842 +- - - 0.9157068285001692 + - 0.4018469910776512 + - - 0.9157068285001692 + - 0.4018469910776512 +- - - 0.920689063052863 + - 0.3902968731297256 + - - 0.920689063052863 + - 0.3902968731297256 +- - - 0.9255256189216778 + - 0.3786849993327503 + - - 0.9255256189216778 + - 0.3786849993327503 +- - - 0.9302157308286042 + - 0.3670132070089654 + - - 0.9302157308286042 + - 0.3670132070089654 +- - - 0.934758656667151 + - 0.35528334296139374 + - - 0.934758656667151 + - 0.35528334296139374 +- - - 0.9391536776197676 + - 0.34349726318162344 + - - 0.9391536776197676 + - 0.34349726318162344 +- - - 0.9434000982715812 + - 0.3316568325561391 + - - 0.9434000982715812 + - 0.3316568325561391 +- - - 0.9474972467204298 + - 0.31976392457124536 + - - 0.9474972467204298 + - 0.31976392457124536 +- - - 0.9514444746831766 + - 0.30782042101662793 + - - 0.9514444746831766 + - 0.30782042101662793 +- - - 0.9552411575982869 + - 0.2958282116876025 + - - 0.9552411575982869 + - 0.2958282116876025 +- - - 0.9588866947246497 + - 0.28378919408609693 + - - 0.9588866947246497 + - 0.28378919408609693 +- - - 0.9623805092366334 + - 0.27170527312041276 + - - 0.9623805092366334 + - 0.27170527312041276 +- - - 0.9657220483153546 + - 0.25957836080381586 + - - 0.9657220483153546 + - 0.25957836080381586 +- - - 0.9689107832361495 + - 0.24741037595200252 + - - 0.9689107832361495 + - 0.24741037595200252 +- - - 0.9719462094522335 + - 0.23520324387949015 + - - 0.9719462094522335 + - 0.23520324387949015 +- - - 0.9748278466745341 + - 0.2229588960949774 + - - 0.9748278466745341 + - 0.2229588960949774 +- - - 0.9775552389476861 + - 0.21067926999572642 + - - 0.9775552389476861 + - 0.21067926999572642 +- - - 0.9801279547221765 + - 0.19836630856101303 + - - 0.9801279547221765 + - 0.19836630856101303 +- - - 0.9825455869226277 + - 0.18602196004469224 + - - 0.9825455869226277 + - 0.18602196004469224 +- - - 0.984807753012208 + - 0.17364817766693041 + - - 0.984807753012208 + - 0.17364817766693041 +- - - 0.98691409505316 + - 0.16124691930515242 + - - 0.98691409505316 + - 0.16124691930515242 +- - - 0.9888642797634357 + - 0.14882014718424924 + - - 0.9888642797634357 + - 0.14882014718424924 +- - - 0.9906579985694317 + - 0.1363698275661 + - - 0.9906579985694317 + - 0.1363698275661 +- - - 0.9922949676548136 + - 0.12389793043845522 + - - 0.9922949676548136 + - 0.12389793043845522 +- - - 0.9937749280054242 + - 0.11140642920322849 + - - 0.9937749280054242 + - 0.11140642920322849 +- - - 0.995097645450266 + - 0.09889730036424986 + - - 0.995097645450266 + - 0.09889730036424986 +- - - 0.9962629106985543 + - 0.08637252321452853 + - - 0.9962629106985543 + - 0.08637252321452853 +- - - 0.9972705393728327 + - 0.07383407952307214 + - - 0.9972705393728327 + - 0.07383407952307214 +- - - 0.9981203720381463 + - 0.06128395322131638 + - - 0.9981203720381463 + - 0.06128395322131638 +- - - 0.9988122742272691 + - 0.04872413008921228 + - - 0.9988122742272691 + - 0.04872413008921228 +- - - 0.9993461364619809 + - 0.036156597441019206 + - - 0.9993461364619809 + - 0.036156597441019206 +- - - 0.9997218742703887 + - 0.023583343810857166 + - - 0.9997218742703887 + - 0.023583343810857166 +- - - 0.9999394282002937 + - 0.011006358638064812 + - - 0.9999394282002937 + - 0.011006358638064812 +- - - 0.9999987638285974 + - -0.001572368047584414 + - - 0.9999987638285974 + - -0.001572368047584414 +- - - 0.9998998717667489 + - -0.014150845940761853 + - - 0.9998998717667489 + - -0.014150845940761853 +- - - 0.9996427676622299 + - -0.026727084775504745 + - - 0.9996427676622299 + - -0.026727084775504745 +- - - 0.9992274921960794 + - -0.03929909464013115 + - - 0.9992274921960794 + - -0.03929909464013115 +- - - 0.9986541110764565 + - -0.0518648862921008 + - - 0.9986541110764565 + - -0.0518648862921008 +- - - 0.9979227150282433 + - -0.06442247147276806 + - - 0.9979227150282433 + - -0.06442247147276806 +- - - 0.9970334197786902 + - -0.07696986322197923 + - - 0.9970334197786902 + - -0.07696986322197923 +- - - 0.9959863660391044 + - -0.08950507619246638 + - - 0.9959863660391044 + - -0.08950507619246638 +- - - 0.9947817194825853 + - -0.10202612696398403 + - - 0.9947817194825853 + - -0.10202612696398403 +- - - 0.9934196707178107 + - -0.11453103435714077 + - - 0.9934196707178107 + - -0.11453103435714077 +- - - 0.991900435258877 + - -0.12701781974687854 + - - 0.991900435258877 + - -0.12701781974687854 +- - - 0.9902242534911986 + - -0.1394845073755453 + - - 0.9902242534911986 + - -0.1394845073755453 +- - - 0.9883913906334728 + - -0.15192912466551547 + - - 0.9883913906334728 + - -0.15192912466551547 +- - - 0.9864021366957146 + - -0.16434970253130593 + - - 0.9864021366957146 + - -0.16434970253130593 +- - - 0.9842568064333687 + - -0.17674427569114137 + - - 0.9842568064333687 + - -0.17674427569114137 +- - - 0.9819557392975067 + - -0.18911088297791617 + - - 0.9819557392975067 + - -0.18911088297791617 +- - - 0.9794992993811165 + - -0.20144756764950503 + - - 0.9794992993811165 + - -0.20144756764950503 +- - - 0.9768878753614926 + - -0.21375237769837538 + - - 0.9768878753614926 + - -0.21375237769837538 +- - - 0.9741218804387363 + - -0.22602336616044894 + - - 0.9741218804387363 + - -0.22602336616044894 +- - - 0.9712017522703763 + - -0.23825859142316483 + - - 0.9712017522703763 + - -0.23825859142316483 +- - - 0.9681279529021188 + - -0.25045611753269825 + - - 0.9681279529021188 + - -0.25045611753269825 +- - - 0.9649009686947391 + - -0.2626140145002818 + - - 0.9649009686947391 + - -0.2626140145002818 +- - - 0.9615213102471255 + - -0.27473035860758266 + - - 0.9615213102471255 + - -0.27473035860758266 +- - - 0.9579895123154889 + - -0.28680323271109 + - - 0.9579895123154889 + - -0.28680323271109 +- - - 0.9543061337287488 + - -0.29883072654545967 + - - 0.9543061337287488 + - -0.29883072654545967 +- - - 0.9504717573001116 + - -0.310810937025771 + - - 0.9504717573001116 + - -0.310810937025771 +- - - 0.9464869897348526 + - -0.32274196854864906 + - - 0.9464869897348526 + - -0.32274196854864906 +- - - 0.9423524615343186 + - -0.33462193329220136 + - - 0.9423524615343186 + - -0.33462193329220136 +- - - 0.9380688268961659 + - -0.3464489515147234 + - - 0.9380688268961659 + - -0.3464489515147234 +- - - 0.9336367636108462 + - -0.3582211518521272 + - - 0.9336367636108462 + - -0.3582211518521272 +- - - 0.9290569729543628 + - -0.369936671614043 + - - 0.9290569729543628 + - -0.369936671614043 +- - - 0.9243301795773085 + - -0.38159365707854837 + - - 0.9243301795773085 + - -0.38159365707854837 +- - - 0.9194571313902055 + - -0.3931902637854787 + - - 0.9194571313902055 + - -0.3931902637854787 +- - - 0.9144385994451658 + - -0.40472465682827324 + - - 0.9144385994451658 + - -0.40472465682827324 +- - - 0.9092753778138886 + - -0.4161950111443075 + - - 0.9092753778138886 + - -0.4161950111443075 +- - - 0.9039682834620162 + - -0.42759951180366895 + - - 0.9039682834620162 + - -0.42759951180366895 +- - - 0.8985181561198674 + - -0.4389363542963303 + - - 0.8985181561198674 + - -0.4389363542963303 +- - - 0.8929258581495686 + - -0.450203744817673 + - - 0.8929258581495686 + - -0.450203744817673 +- - - 0.8871922744086043 + - -0.46139990055231683 + - - 0.8871922744086043 + - -0.46139990055231683 +- - - 0.881318312109807 + - -0.47252304995621186 + - - 0.881318312109807 + - -0.47252304995621186 +- - - 0.8753049006778131 + - -0.4835714330369443 + - - 0.8753049006778131 + - -0.4835714330369443 +- - - 0.869152991601999 + - -0.4945433016322186 + - - 0.869152991601999 + - -0.4945433016322186 +- - - 0.8628635582859312 + - -0.5054369196864643 + - - 0.8628635582859312 + - -0.5054369196864643 +- - - 0.856437595893346 + - -0.5162505635255284 + - - 0.856437595893346 + - -0.5162505635255284 +- - - 0.8498761211906867 + - -0.5269825221294092 + - - 0.8498761211906867 + - -0.5269825221294092 +- - - 0.8431801723862224 + - -0.5376310974029872 + - - 0.8431801723862224 + - -0.5376310974029872 +- - - 0.8363508089657762 + - -0.5481946044447097 + - - 0.8363508089657762 + - -0.5481946044447097 +- - - 0.8293891115250829 + - -0.5586713718131919 + - - 0.8293891115250829 + - -0.5586713718131919 +- - - 0.8222961815988096 + - -0.5690597417916836 + - - 0.8222961815988096 + - -0.5690597417916836 +- - - 0.8150731414862624 + - -0.5793580706503667 + - - 0.8150731414862624 + - -0.5793580706503667 +- - - 0.8077211340738071 + - -0.5895647289064391 + - - 0.8077211340738071 + - -0.5895647289064391 +- - - 0.800241322654032 + - -0.5996781015819448 + - - 0.800241322654032 + - -0.5996781015819448 +- - - 0.7926348907416848 + - -0.6096965884593069 + - - 0.7926348907416848 + - -0.6096965884593069 +- - - 0.7849030418864046 + - -0.6196186043345285 + - - 0.7849030418864046 + - -0.6196186043345285 +- - - 0.7770469994822886 + - -0.6294425792680156 + - - 0.7770469994822886 + - -0.6294425792680156 +- - - 0.769068006574317 + - -0.6391669588329847 + - - 0.769068006574317 + - -0.6391669588329847 +- - - 0.7609673256616678 + - -0.648790204361417 + - - 0.7609673256616678 + - -0.648790204361417 +- - - 0.7527462384979551 + - -0.6583107931875185 + - - 0.7527462384979551 + - -0.6583107931875185 +- - - 0.744406045888419 + - -0.6677272188886485 + - - 0.744406045888419 + - -0.6677272188886485 +- - - 0.7359480674841035 + - -0.6770379915236763 + - - 0.7359480674841035 + - -0.6770379915236763 +- - - 0.7273736415730488 + - -0.6862416378687335 + - - 0.7273736415730488 + - -0.6862416378687335 +- - - 0.7186841248685385 + - -0.6953367016503177 + - - 0.7186841248685385 + - -0.6953367016503177 +- - - 0.7098808922944289 + - -0.7043217437757161 + - - 0.7098808922944289 + - -0.7043217437757161 +- - - 0.7009653367675978 + - -0.7131953425607098 + - - 0.7009653367675978 + - -0.7131953425607098 +- - - 0.6919388689775463 + - -0.7219560939545244 + - - 0.6919388689775463 + - -0.7219560939545244 +- - - 0.6828029171631891 + - -0.7306026117619886 + - - 0.6828029171631891 + - -0.7306026117619886 +- - - 0.673558926886866 + - -0.739133527862871 + - - 0.673558926886866 + - -0.739133527862871 +- - - 0.6642083608056142 + - -0.7475474924283534 + - - 0.6642083608056142 + - -0.7475474924283534 +- - - 0.6547526984397353 + - -0.7558431741346118 + - - 0.6547526984397353 + - -0.7558431741346118 +- - - 0.6451934359386937 + - -0.764019260373469 + - - 0.6451934359386937 + - -0.764019260373469 +- - - 0.6355320858443845 + - -0.7720744574600859 + - - 0.6355320858443845 + - -0.7720744574600859 +- - - 0.6257701768518059 + - -0.7800074908376582 + - - 0.6257701768518059 + - -0.7800074908376582 +- - - 0.6159092535671797 + - -0.7878171052790867 + - - 0.6159092535671797 + - -0.7878171052790867 +- - - 0.6059508762635484 + - -0.7955020650855897 + - - 0.6059508762635484 + - -0.7955020650855897 +- - - 0.5958966206338979 + - -0.8030611542822255 + - - 0.5958966206338979 + - -0.8030611542822255 +- - - 0.5857480775418397 + - -0.8104931768102919 + - - 0.5857480775418397 + - -0.8104931768102919 +- - - 0.5755068527698903 + - -0.8177969567165775 + - - 0.5755068527698903 + - -0.8177969567165775 +- - - 0.5651745667653929 + - -0.8249713383394301 + - - 0.5651745667653929 + - -0.8249713383394301 +- - - 0.5547528543841173 + - -0.8320151864916135 + - - 0.5547528543841173 + - -0.8320151864916135 +- - - 0.5442433646315792 + - -0.8389273866399272 + - - 0.5442433646315792 + - -0.8389273866399272 +- - - 0.5336477604021226 + - -0.8457068450815559 + - - 0.5336477604021226 + - -0.8457068450815559 +- - - 0.5229677182158028 + - -0.8523524891171238 + - - 0.5229677182158028 + - -0.8523524891171238 +- - - 0.5122049279531147 + - -0.8588632672204258 + - - 0.5122049279531147 + - -0.8588632672204258 +- - - 0.5013610925876063 + - -0.865238149204808 + - - 0.5013610925876063 + - -0.865238149204808 +- - - 0.49043792791642066 + - -0.8714761263861723 + - - 0.49043792791642066 + - -0.8714761263861723 +- - - 0.47943716228880995 + - -0.8775762117425775 + - - 0.47943716228880995 + - -0.8775762117425775 +- - - 0.4683605363326608 + - -0.8835374400704151 + - - 0.4683605363326608 + - -0.8835374400704151 +- - - 0.4572098026790794 + - -0.8893588681371302 + - - 0.4572098026790794 + - -0.8893588681371302 +- - - 0.44598672568507636 + - -0.8950395748304677 + - - 0.44598672568507636 + - -0.8950395748304677 +- - - 0.4346930811543961 + - -0.9005786613042182 + - - 0.4346930811543961 + - -0.9005786613042182 +- - - 0.4233306560565345 + - -0.9059752511204399 + - - 0.4233306560565345 + - -0.9059752511204399 +- - - 0.4119012482439928 + - -0.9112284903881356 + - - 0.4119012482439928 + - -0.9112284903881356 +- - - 0.40040666616780407 + - -0.916337547898363 + - - 0.40040666616780407 + - -0.916337547898363 +- - - 0.3888487285913878 + - -0.9213016152557539 + - - 0.3888487285913878 + - -0.9213016152557539 +- - - 0.37722926430277026 + - -0.9261199070064258 + - - 0.37722926430277026 + - -0.9261199070064258 +- - - 0.36555011182521946 + - -0.9307916607622618 + - - 0.36555011182521946 + - -0.9307916607622618 +- - - 0.3538131191263388 + - -0.9353161373215428 + - - 0.3538131191263388 + - -0.9353161373215428 +- - - 0.3420201433256689 + - -0.9396926207859083 + - - 0.3420201433256689 + - -0.9396926207859083 +- - - 0.330173050400837 + - -0.9439204186736329 + - - 0.330173050400837 + - -0.9439204186736329 +- - - 0.3182737148923088 + - -0.9479988620291954 + - - 0.3182737148923088 + - -0.9479988620291954 +- - - 0.3063240196067838 + - -0.9519273055291264 + - - 0.3063240196067838 + - -0.9519273055291264 +- - - 0.29432585531928224 + - -0.9557051275841167 + - - 0.29432585531928224 + - -0.9557051275841167 +- - - 0.2822811204739722 + - -0.9593317304373701 + - - 0.2822811204739722 + - -0.9593317304373701 +- - - 0.27019172088378224 + - -0.9628065402591843 + - - 0.27019172088378224 + - -0.9628065402591843 +- - - 0.25805956942885044 + - -0.9661290072377479 + - - 0.25805956942885044 + - -0.9661290072377479 +- - - 0.24588658575385056 + - -0.9692986056661355 + - - 0.24588658575385056 + - -0.9692986056661355 +- - - 0.23367469596425278 + - -0.9723148340254889 + - - 0.23367469596425278 + - -0.9723148340254889 +- - - 0.22142583232155955 + - -0.975177215064372 + - - 0.22142583232155955 + - -0.975177215064372 +- - - 0.20914193293756786 + - -0.977885295874285 + - - 0.20914193293756786 + - -0.977885295874285 +- - - 0.19682494146770554 + - -0.9804386479613267 + - - 0.19682494146770554 + - -0.9804386479613267 +- - - 0.18447680680349254 + - -0.9828368673139948 + - - 0.18447680680349254 + - -0.9828368673139948 +- - - 0.17209948276416928 + - -0.9850795744671115 + - - 0.17209948276416928 + - -0.9850795744671115 +- - - 0.15969492778754976 + - -0.9871664145618657 + - - 0.15969492778754976 + - -0.9871664145618657 +- - - 0.14726510462014156 + - -0.9890970574019613 + - - 0.14726510462014156 + - -0.9890970574019613 +- - - 0.1348119800065847 + - -0.9908711975058636 + - - 0.1348119800065847 + - -0.9908711975058636 +- - - 0.12233752437845731 + - -0.992488554155135 + - - 0.12233752437845731 + - -0.992488554155135 +- - - 0.1098437115425002 + - -0.9939488714388522 + - - 0.1098437115425002 + - -0.9939488714388522 +- - - 0.09733251836830287 + - -0.9952519182940991 + - - 0.09733251836830287 + - -0.9952519182940991 +- - - 0.0848059244755095 + - -0.9963974885425265 + - - 0.0848059244755095 + - -0.9963974885425265 +- - - 0.07226591192058739 + - -0.9973854009229761 + - - 0.07226591192058739 + - -0.9973854009229761 +- - - 0.05971446488321034 + - -0.9982154991201608 + - - 0.05971446488321034 + - -0.9982154991201608 +- - - 0.04715356935230619 + - -0.9988876517893978 + - - 0.04715356935230619 + - -0.9988876517893978 +- - - 0.034585212811817465 + - -0.9994017525773913 + - - 0.034585212811817465 + - -0.9994017525773913 +- - - 0.022011383926227784 + - -0.9997577201390606 + - - 0.022011383926227784 + - -0.9997577201390606 +- - - 0.009434072225897046 + - -0.999955498150411 + - - 0.009434072225897046 + - -0.999955498150411 +- - - -0.0031447322077359985 + - -0.9999950553174459 + - - -0.0031447322077359985 + - -0.9999950553174459 +- - - -0.015723039057040564 + - -0.9998763853811183 + - - -0.015723039057040564 + - -0.9998763853811183 +- - - -0.02829885808311759 + - -0.9995995071183217 + - - -0.02829885808311759 + - -0.9995995071183217 +- - - -0.04087019944071145 + - -0.9991644643389178 + - - -0.04087019944071145 + - -0.9991644643389178 +- - - -0.053435073993057226 + - -0.9985713258788059 + - - -0.053435073993057226 + - -0.9985713258788059 +- - - -0.06599149362662023 + - -0.9978201855890307 + - - -0.06599149362662023 + - -0.9978201855890307 +- - - -0.07853747156566927 + - -0.996911162320932 + - - -0.07853747156566927 + - -0.996911162320932 +- - - -0.09107102268664041 + - -0.9958443999073396 + - - -0.09107102268664041 + - -0.9958443999073396 +- - - -0.10359016383223883 + - -0.9946200671398149 + - - -0.10359016383223883 + - -0.9946200671398149 +- - - -0.11609291412522968 + - -0.993238357741943 + - - -0.11609291412522968 + - -0.993238357741943 +- - - -0.12857729528186848 + - -0.9916994903386808 + - - -0.12857729528186848 + - -0.9916994903386808 +- - - -0.14104133192491908 + - -0.9900037084217639 + - - -0.14104133192491908 + - -0.9900037084217639 +- - - -0.15348305189621594 + - -0.9881512803111796 + - - -0.15348305189621594 + - -0.9881512803111796 +- - - -0.16590048656871298 + - -0.9861424991127116 + - - -0.16590048656871298 + - -0.9861424991127116 +- - - -0.1782916711579755 + - -0.9839776826715616 + - - -0.1782916711579755 + - -0.9839776826715616 +- - - -0.19065464503306404 + - -0.9816571735220583 + - - -0.19065464503306404 + - -0.9816571735220583 +- - - -0.20298745202676116 + - -0.979181338833458 + - - -0.20298745202676116 + - -0.979181338833458 +- - - -0.2152881407450901 + - -0.9765505703518493 + - - -0.2152881407450901 + - -0.9765505703518493 +- - - -0.2275547648760821 + - -0.9737652843381669 + - - -0.2275547648760821 + - -0.9737652843381669 +- - - -0.23978538349773562 + - -0.9708259215023277 + - - -0.23978538349773562 + - -0.9708259215023277 +- - - -0.25197806138512474 + - -0.967732946933499 + - - -0.25197806138512474 + - -0.967732946933499 +- - - -0.2641308693166058 + - -0.9644868500265071 + - - -0.2641308693166058 + - -0.9644868500265071 +- - - -0.2762418843790738 + - -0.9610881444044029 + - - -0.2762418843790738 + - -0.9610881444044029 +- - - -0.2883091902722216 + - -0.9575373678371909 + - - -0.2883091902722216 + - -0.9575373678371909 +- - - -0.3003308776117502 + - -0.9538350821567405 + - - -0.3003308776117502 + - -0.9538350821567405 +- - - -0.31230504423148914 + - -0.9499818731678872 + - - -0.31230504423148914 + - -0.9499818731678872 +- - - -0.32422979548437053 + - -0.9459783505557425 + - - -0.32422979548437053 + - -0.9459783505557425 +- - - -0.33610324454221563 + - -0.9418251477892251 + - - -0.33610324454221563 + - -0.9418251477892251 +- - - -0.34792351269428334 + - -0.9375229220208277 + - - -0.34792351269428334 + - -0.9375229220208277 +- - - -0.3596887296445355 + - -0.9330723539826374 + - - -0.3596887296445355 + - -0.9330723539826374 +- - - -0.3713970338075679 + - -0.9284741478786258 + - - -0.3713970338075679 + - -0.9284741478786258 +- - - -0.3830465726031674 + - -0.9237290312732227 + - - -0.3830465726031674 + - -0.9237290312732227 +- - - -0.3946355027494405 + - -0.9188377549761962 + - - -0.3946355027494405 + - -0.9188377549761962 +- - - -0.406161990554472 + - -0.9138010929238535 + - - -0.406161990554472 + - -0.9138010929238535 +- - - -0.41762421220646645 + - -0.9086198420565822 + - - -0.41762421220646645 + - -0.9086198420565822 +- - - -0.4290203540623263 + - -0.9032948221927524 + - - -0.4290203540623263 + - -0.9032948221927524 +- - - -0.44034861293461913 + - -0.8978268758989992 + - - -0.44034861293461913 + - -0.8978268758989992 +- - - -0.4516071963768948 + - -0.892216868356904 + - - -0.4516071963768948 + - -0.892216868356904 +- - - -0.46279432296729867 + - -0.8864656872260989 + - - -0.46279432296729867 + - -0.8864656872260989 +- - - -0.47390822259044274 + - -0.8805742425038149 + - - -0.47390822259044274 + - -0.8805742425038149 +- - - -0.4849471367174873 + - -0.8745434663808944 + - - -0.4849471367174873 + - -0.8745434663808944 +- - - -0.495909318684389 + - -0.8683743130942929 + - - -0.495909318684389 + - -0.8683743130942929 +- - - -0.5067930339682724 + - -0.8620677587760915 + - - -0.5067930339682724 + - -0.8620677587760915 +- - - -0.5175965604618782 + - -0.8556248012990468 + - - -0.5175965604618782 + - -0.8556248012990468 +- - - -0.5283181887460511 + - -0.849046460118698 + - - -0.5283181887460511 + - -0.849046460118698 +- - - -0.538956222360216 + - -0.842333776112062 + - - -0.538956222360216 + - -0.842333776112062 +- - - -0.5495089780708056 + - -0.8354878114129367 + - - -0.5495089780708056 + - -0.8354878114129367 +- - - -0.5599747861375949 + - -0.8285096492438424 + - - -0.5599747861375949 + - -0.8285096492438424 +- - - -0.5703519905779012 + - -0.8214003937446254 + - - -0.5703519905779012 + - -0.8214003937446254 +- - - -0.5806389494286053 + - -0.814161169797753 + - - -0.5806389494286053 + - -0.814161169797753 +- - - -0.5908340350059578 + - -0.8067931228503245 + - - -0.5908340350059578 + - -0.8067931228503245 +- - - -0.6009356341631226 + - -0.7992974187328304 + - - -0.6009356341631226 + - -0.7992974187328304 +- - - -0.6109421485454225 + - -0.7916752434746857 + - - -0.6109421485454225 + - -0.7916752434746857 +- - - -0.6208519948432432 + - -0.7839278031165661 + - - -0.6208519948432432 + - -0.7839278031165661 +- - - -0.630663605042557 + - -0.7760563235195791 + - - -0.630663605042557 + - -0.7760563235195791 +- - - -0.6403754266730258 + - -0.7680620501712998 + - - -0.6403754266730258 + - -0.7680620501712998 +- - - -0.6499859230536464 + - -0.7599462479886977 + - - -0.6499859230536464 + - -0.7599462479886977 +- - - -0.6594935735358957 + - -0.7517102011179935 + - - -0.6594935735358957 + - -0.7517102011179935 +- - - -0.6688968737443391 + - -0.7433552127314704 + - - -0.6688968737443391 + - -0.7433552127314704 +- - - -0.6781943358146659 + - -0.7348826048212762 + - - -0.6781943358146659 + - -0.7348826048212762 +- - - -0.6873844886291098 + - -0.7262937179902474 + - - -0.6873844886291098 + - -0.7262937179902474 +- - - -0.6964658780492216 + - -0.717589911239788 + - - -0.6964658780492216 + - -0.717589911239788 +- - - -0.7054370671459529 + - -0.7087725617548385 + - - -0.7054370671459529 + - -0.7087725617548385 +- - - -0.7142966364270207 + - -0.6998430646859656 + - - -0.7142966364270207 + - -0.6998430646859656 +- - - -0.723043184061509 + - -0.6908028329286112 + - - -0.723043184061509 + - -0.6908028329286112 +- - - -0.731675326101678 + - -0.6816532968995332 + - - -0.731675326101678 + - -0.6816532968995332 +- - - -0.7401916967019432 + - -0.6723959043104729 + - - -0.7401916967019432 + - -0.6723959043104729 +- - - -0.7485909483349904 + - -0.6630321199390868 + - - -0.7485909483349904 + - -0.6630321199390868 +- - - -0.7568717520049916 + - -0.6535634253971795 + - - -0.7568717520049916 + - -0.6535634253971795 +- - - -0.7650327974578898 + - -0.6439913188962686 + - - -0.7650327974578898 + - -0.6439913188962686 +- - - -0.7730727933887175 + - -0.634317315010528 + - - -0.7730727933887175 + - -0.634317315010528 +- - - -0.7809904676459172 + - -0.6245429444371393 + - - -0.7809904676459172 + - -0.6245429444371393 +- - - -0.788784567432631 + - -0.6146697537540928 + - - -0.788784567432631 + - -0.6146697537540928 +- - - -0.7964538595049286 + - -0.6046993051754759 + - - -0.7964538595049286 + - -0.6046993051754759 +- - - -0.8039971303669401 + - -0.5946331763042871 + - - -0.8039971303669401 + - -0.5946331763042871 +- - - -0.8114131864628653 + - -0.5844729598828156 + - - -0.8114131864628653 + - -0.5844729598828156 +- - - -0.8187008543658276 + - -0.5742202635406243 + - - -0.8187008543658276 + - -0.5742202635406243 +- - - -0.825858980963543 + - -0.5638767095401779 + - - -0.825858980963543 + - -0.5638767095401779 +- - - -0.8328864336407734 + - -0.5534439345201586 + - - -0.8328864336407734 + - -0.5534439345201586 +- - - -0.8397821004585396 + - -0.5429235892364995 + - - -0.8397821004585396 + - -0.5429235892364995 +- - - -0.8465448903300604 + - -0.5323173383011922 + - - -0.8465448903300604 + - -0.5323173383011922 +- - - -0.8531737331933926 + - -0.521626859918898 + - - -0.8531737331933926 + - -0.521626859918898 +- - - -0.8596675801807451 + - -0.5108538456214089 + - - -0.8596675801807451 + - -0.5108538456214089 +- - - -0.8660254037844384 + - -0.5000000000000004 + - - -0.8660254037844384 + - -0.5000000000000004 +- - - -0.872246198019486 + - -0.4890670404357173 + - - -0.872246198019486 + - -0.4890670404357173 +- - - -0.8783289785827684 + - -0.4780566968276366 + - - -0.8783289785827684 + - -0.4780566968276366 +- - - -0.8842727830087774 + - -0.46697071131914863 + - - -0.8842727830087774 + - -0.46697071131914863 +- - - -0.8900766708219056 + - -0.4558108380223019 + - - -0.8900766708219056 + - -0.4558108380223019 +- - - -0.895739723685255 + - -0.4445788427402534 + - - -0.895739723685255 + - -0.4445788427402534 +- - - -0.9012610455459443 + - -0.4332765026878693 + - - -0.9012610455459443 + - -0.4332765026878693 +- - - -0.9066397627768893 + - -0.4219056062105194 + - - -0.9066397627768893 + - -0.4219056062105194 +- - - -0.9118750243150336 + - -0.410467952501114 + - - -0.9118750243150336 + - -0.410467952501114 +- - - -0.9169660017960133 + - -0.39896535131541655 + - - -0.9169660017960133 + - -0.39896535131541655 +- - - -0.921911889685225 + - -0.38739962268569333 + - - -0.921911889685225 + - -0.38739962268569333 +- - - -0.9267119054052849 + - -0.37577259663273255 + - - -0.9267119054052849 + - -0.37577259663273255 +- - - -0.931365289459854 + - -0.3640861128762842 + - - -0.931365289459854 + - -0.3640861128762842 +- - - -0.9358713055538119 + - -0.3523420205439648 + - - -0.9358713055538119 + - -0.3523420205439648 +- - - -0.9402292407097588 + - -0.3405421778786742 + - - -0.9402292407097588 + - -0.3405421778786742 +- - - -0.9444384053808287 + - -0.32868845194456947 + - - -0.9444384053808287 + - -0.32868845194456947 +- - - -0.948498133559795 + - -0.3167827183316434 + - - -0.948498133559795 + - -0.3167827183316434 +- - - -0.9524077828844512 + - -0.30482686085895394 + - - -0.9524077828844512 + - -0.30482686085895394 +- - - -0.9561667347392507 + - -0.2928227712765512 + - - -0.9561667347392507 + - -0.2928227712765512 +- - - -0.959774394353189 + - -0.28077234896614933 + - - -0.959774394353189 + - -0.28077234896614933 +- - - -0.9632301908939126 + - -0.26867750064059465 + - - -0.9632301908939126 + - -0.26867750064059465 +- - - -0.9665335775580413 + - -0.25654014004216524 + - - -0.9665335775580413 + - -0.25654014004216524 +- - - -0.9696840316576876 + - -0.2443621876397672 + - - -0.9696840316576876 + - -0.2443621876397672 +- - - -0.97268105470316 + - -0.2321455703250619 + - - -0.97268105470316 + - -0.2321455703250619 +- - - -0.9755241724818386 + - -0.21989222110757806 + - - -0.9755241724818386 + - -0.21989222110757806 +- - - -0.9782129351332083 + - -0.2076040788088557 + - - -0.9782129351332083 + - -0.2076040788088557 +- - - -0.9807469172200395 + - -0.19528308775567055 + - - -0.9807469172200395 + - -0.19528308775567055 +- - - -0.9831257177957041 + - -0.18293119747238726 + - - -0.9831257177957041 + - -0.18293119747238726 +- - - -0.9853489604676163 + - -0.17055036237249038 + - - -0.9853489604676163 + - -0.17055036237249038 +- - - -0.9874162934567888 + - -0.15814254144934156 + - - -0.9874162934567888 + - -0.15814254144934156 +- - - -0.9893273896534934 + - -0.14570969796621222 + - - -0.9893273896534934 + - -0.14570969796621222 +- - - -0.9910819466690195 + - -0.1332537991456406 + - - -0.9910819466690195 + - -0.1332537991456406 +- - - -0.9926796868835203 + - -0.1207768158581612 + - - -0.9926796868835203 + - -0.1207768158581612 +- - - -0.9941203574899392 + - -0.10828072231046196 + - - -0.9941203574899392 + - -0.10828072231046196 +- - - -0.9954037305340125 + - -0.09576749573300417 + - - -0.9954037305340125 + - -0.09576749573300417 +- - - -0.9965296029503367 + - -0.08323911606717305 + - - -0.9965296029503367 + - -0.08323911606717305 +- - - -0.9974977965944997 + - -0.070697565651995 + - - -0.9974977965944997 + - -0.070697565651995 +- - - -0.9983081582712682 + - -0.05814482891047624 + - - -0.9983081582712682 + - -0.05814482891047624 +- - - -0.9989605597588274 + - -0.04558289203561173 + - - -0.9989605597588274 + - -0.04558289203561173 +- - - -0.9994548978290693 + - -0.0330137426761141 + - - -0.9994548978290693 + - -0.0330137426761141 +- - - -0.9997910942639261 + - -0.020439369621912166 + - - -0.9997910942639261 + - -0.020439369621912166 +- - - -0.9999690958677468 + - -0.007861762489468911 + - - -0.9999690958677468 + - -0.007861762489468911 +- - - -0.999988874475714 + - 0.004717088593031313 + - - -0.999988874475714 + - 0.004717088593031313 +- - - -0.9998504269583004 + - 0.01729519330057657 + - - -0.9998504269583004 + - 0.01729519330057657 +- - - -0.9995537752217639 + - 0.029870561426252256 + - - -0.9995537752217639 + - 0.029870561426252256 +- - - -0.9990989662046815 + - 0.04244120319614822 + - - -0.9990989662046815 + - 0.04244120319614822 +- - - -0.9984860718705224 + - 0.055005129584192916 + - - -0.9984860718705224 + - 0.055005129584192916 +- - - -0.9977151891962615 + - 0.06756035262687816 + - - -0.9977151891962615 + - 0.06756035262687816 +- - - -0.9967864401570343 + - 0.08010488573780679 + - - -0.9967864401570343 + - 0.08010488573780679 +- - - -0.9956999717068378 + - 0.09263674402202696 + - - -0.9956999717068378 + - 0.09263674402202696 +- - - -0.9944559557552776 + - 0.10515394459009784 + - - -0.9944559557552776 + - 0.10515394459009784 +- - - -0.9930545891403677 + - 0.11765450687183807 + - - -0.9930545891403677 + - 0.11765450687183807 +- - - -0.9914960935973849 + - 0.1301364529297071 + - - -0.9914960935973849 + - 0.1301364529297071 +- - - -0.9897807157237836 + - 0.1425978077717702 + - - -0.9897807157237836 + - 0.1425978077717702 +- - - -0.9879087269401782 + - 0.1550365996641971 + - - -0.9879087269401782 + - 0.1550365996641971 +- - - -0.9858804234473959 + - 0.16745086044324545 + - - -0.9858804234473959 + - 0.16745086044324545 +- - - -0.9836961261796103 + - 0.17983862582667898 + - - -0.9836961261796103 + - 0.17983862582667898 +- - - -0.9813561807535597 + - 0.19219793572457194 + - - -0.9813561807535597 + - 0.19219793572457194 +- - - -0.9788609574138615 + - 0.20452683454945075 + - - -0.9788609574138615 + - 0.20452683454945075 +- - - -0.9762108509744296 + - 0.21682337152571898 + - - -0.9762108509744296 + - 0.21682337152571898 +- - - -0.9734062807560028 + - 0.22908560099832972 + - - -0.9734062807560028 + - 0.22908560099832972 +- - - -0.9704476905197971 + - 0.24131158274063894 + - - -0.9704476905197971 + - 0.24131158274063894 +- - - -0.9673355483972903 + - 0.25349938226140434 + - - -0.9673355483972903 + - 0.25349938226140434 +- - - -0.9640703468161508 + - 0.2656470711108758 + - - -0.9640703468161508 + - 0.2656470711108758 +- - - -0.9606526024223212 + - 0.27775272718593 + - - -0.9606526024223212 + - 0.27775272718593 +- - - -0.957082855998271 + - 0.28981443503420057 + - - -0.957082855998271 + - 0.28981443503420057 +- - - -0.9533616723774295 + - 0.30183028615715607 + - - -0.9533616723774295 + - 0.30183028615715607 +- - - -0.9494896403548136 + - 0.31379837931207794 + - - -0.9494896403548136 + - 0.31379837931207794 +- - - -0.9454673725938637 + - 0.3257168208128897 + - - -0.9454673725938637 + - 0.3257168208128897 +- - - -0.9412955055295036 + - 0.33758372482979143 + - - -0.9412955055295036 + - 0.33758372482979143 +- - - -0.9369746992674384 + - 0.34939721368765 + - - -0.9369746992674384 + - 0.34939721368765 +- - - -0.9325056374797075 + - 0.361155418163101 + - - -0.9325056374797075 + - 0.361155418163101 +- - - -0.9278890272965095 + - 0.3728564777803084 + - - -0.9278890272965095 + - 0.3728564777803084 +- - - -0.9231255991943125 + - 0.3844985411053488 + - - -0.9231255991943125 + - 0.3844985411053488 +- - - -0.9182161068802741 + - 0.3960797660391565 + - - -0.9182161068802741 + - 0.3960797660391565 +- - - -0.9131613271729835 + - 0.4075983201089958 + - - -0.9131613271729835 + - 0.4075983201089958 +- - - -0.9079620598795464 + - 0.41905238075840945 + - - -0.9079620598795464 + - 0.41905238075840945 +- - - -0.9026191276690343 + - 0.4304401356355976 + - - -0.9026191276690343 + - 0.4304401356355976 +- - - -0.8971333759423143 + - 0.4417597828801825 + - - -0.8971333759423143 + - 0.4417597828801825 +- - - -0.8915056726982842 + - 0.4530095314083134 + - - -0.8915056726982842 + - 0.4530095314083134 +- - - -0.8857369083965297 + - 0.4641876011960654 + - - -0.8857369083965297 + - 0.4641876011960654 +- - - -0.8798279958164298 + - 0.4752922235610892 + - - -0.8798279958164298 + - 0.4752922235610892 +- - - -0.873779869912729 + - 0.486321641442466 + - - -0.873779869912729 + - 0.486321641442466 +- - - -0.8675934876676018 + - 0.49727410967872326 + - - -0.8675934876676018 + - 0.49727410967872326 +- - - -0.8612698279392309 + - 0.5081478952839691 + - - -0.8612698279392309 + - 0.5081478952839691 +- - - -0.8548098913069261 + - 0.5189412777220956 + - - -0.8548098913069261 + - 0.5189412777220956 +- - - -0.8482146999128025 + - 0.5296525491790203 + - - -0.8482146999128025 + - 0.5296525491790203 +- - - -0.8414852973000504 + - 0.5402800148329067 + - - -0.8414852973000504 + - 0.5402800148329067 +- - - -0.8346227482478176 + - 0.5508219931223336 + - - -0.8346227482478176 + - 0.5508219931223336 +- - - -0.8276281386027314 + - 0.5612768160123647 + - - -0.8276281386027314 + - 0.5612768160123647 +- - - -0.8205025751070878 + - 0.5716428292584782 + - - -0.8205025751070878 + - 0.5716428292584782 +- - - -0.8132471852237334 + - 0.5819183926683146 + - - -0.8132471852237334 + - 0.5819183926683146 +- - - -0.8058631169576695 + - 0.5921018803612005 + - - -0.8058631169576695 + - 0.5921018803612005 +- - - -0.7983515386744064 + - 0.6021916810254089 + - - -0.7983515386744064 + - 0.6021916810254089 +- - - -0.7907136389150943 + - 0.6121861981731129 + - - -0.7907136389150943 + - 0.6121861981731129 +- - - -0.7829506262084637 + - 0.6220838503929953 + - - -0.7829506262084637 + - 0.6220838503929953 +- - - -0.7750637288796017 + - 0.6318830716004721 + - - -0.7750637288796017 + - 0.6318830716004721 +- - - -0.7670541948555989 + - 0.6415823112854881 + - - -0.7670541948555989 + - 0.6415823112854881 +- - - -0.7589232914680891 + - 0.6511800347578556 + - - -0.7589232914680891 + - 0.6511800347578556 +- - - -0.7506723052527245 + - 0.6606747233900812 + - - -0.7506723052527245 + - 0.6606747233900812 +- - - -0.7423025417456096 + - 0.670064874857657 + - - -0.7423025417456096 + - 0.670064874857657 +- - - -0.7338153252767281 + - 0.6793490033767694 + - - -0.7338153252767281 + - 0.6793490033767694 +- - - -0.7252119987603977 + - 0.6885256399393918 + - - -0.7252119987603977 + - 0.6885256399393918 +- - - -0.7164939234827836 + - 0.6975933325457224 + - - -0.7164939234827836 + - 0.6975933325457224 +- - - -0.7076624788865049 + - 0.706550646433932 + - - -0.7076624788865049 + - 0.706550646433932 +- - - -0.698719062352368 + - 0.7153961643071813 + - - -0.698719062352368 + - 0.7153961643071813 +- - - -0.6896650889782625 + - 0.7241284865578796 + - - -0.6896650889782625 + - 0.7241284865578796 +- - - -0.6805019913552531 + - 0.7327462314891391 + - - -0.6805019913552531 + - 0.7327462314891391 +- - - -0.6712312193409035 + - 0.7412480355333995 + - - -0.6712312193409035 + - 0.7412480355333995 +- - - -0.6618542398298681 + - 0.7496325534681825 + - - -0.6618542398298681 + - 0.7496325534681825 +- - - -0.6523725365217912 + - 0.7578984586289408 + - - -0.6523725365217912 + - 0.7578984586289408 +- - - -0.6427876096865396 + - 0.7660444431189778 + - - -0.6427876096865396 + - 0.7660444431189778 +- - - -0.6331009759268216 + - 0.7740692180163904 + - - -0.6331009759268216 + - 0.7740692180163904 +- - - -0.623314167938217 + - 0.7819715135780128 + - - -0.623314167938217 + - 0.7819715135780128 +- - - -0.6134287342666622 + - 0.7897500794403256 + - - -0.6134287342666622 + - 0.7897500794403256 +- - - -0.6034462390634266 + - 0.7974036848172986 + - - -0.6034462390634266 + - 0.7974036848172986 +- - - -0.5933682618376209 + - 0.8049311186951345 + - - -0.5933682618376209 + - 0.8049311186951345 +- - - -0.5831963972062739 + - 0.8123311900238854 + - - -0.5831963972062739 + - 0.8123311900238854 +- - - -0.5729322546420206 + - 0.819602727905911 + - - -0.5729322546420206 + - 0.819602727905911 +- - - -0.5625774582184379 + - 0.826744581781146 + - - -0.5625774582184379 + - 0.826744581781146 +- - - -0.552133646353071 + - 0.8337556216091511 + - - -0.552133646353071 + - 0.8337556216091511 +- - - -0.541602471548191 + - 0.8406347380479176 + - - -0.541602471548191 + - 0.8406347380479176 +- - - -0.5309856001293205 + - 0.8473808426293961 + - - -0.5309856001293205 + - 0.8473808426293961 +- - - -0.5202847119815792 + - 0.8539928679317206 + - - -0.5202847119815792 + - 0.8539928679317206 +- - - -0.5095015002838734 + - 0.8604697677481075 + - - -0.5095015002838734 + - 0.8604697677481075 +- - - -0.4986376712409919 + - 0.8668105172523927 + - - -0.4986376712409919 + - 0.8668105172523927 +- - - -0.487694943813635 + - 0.8730141131611879 + - - -0.487694943813635 + - 0.8730141131611879 +- - - -0.47667504944642797 + - 0.8790795738926286 + - - -0.47667504944642797 + - 0.8790795738926286 +- - - -0.4655797317939577 + - 0.8850059397216871 + - - -0.4655797317939577 + - 0.8850059397216871 +- - - -0.45441074644487806 + - 0.890792272932028 + - - -0.45441074644487806 + - 0.890792272932028 +- - - -0.4431698606441268 + - 0.8964376579643814 + - - -0.4431698606441268 + - 0.8964376579643814 +- - - -0.4318588530132981 + - 0.9019412015614092 + - - -0.4318588530132981 + - 0.9019412015614092 +- - - -0.4204795132692152 + - 0.907302032909044 + - - -0.4204795132692152 + - 0.907302032909044 +- - - -0.4090336419407468 + - 0.9125193037742757 + - - -0.4090336419407468 + - 0.9125193037742757 +- - - -0.3975230500839139 + - 0.9175921886393661 + - - -0.3975230500839139 + - 0.9175921886393661 +- - - -0.38594955899532896 + - 0.9225198848324686 + - - -0.38594955899532896 + - 0.9225198848324686 +- - - -0.3743149999240192 + - 0.9273016126546322 + - - -0.3743149999240192 + - 0.9273016126546322 +- - - -0.3626212137816673 + - 0.9319366155031737 + - - -0.3626212137816673 + - 0.9319366155031737 +- - - -0.35087005085133094 + - 0.9364241599913922 + - - -0.35087005085133094 + - 0.9364241599913922 +- - - -0.3390633704946757 + - 0.9407635360646108 + - - -0.3390633704946757 + - 0.9407635360646108 +- - - -0.3272030408577722 + - 0.9449540571125281 + - - -0.3272030408577722 + - 0.9449540571125281 +- - - -0.3152909385755031 + - 0.9489950600778585 + - - -0.3152909385755031 + - 0.9489950600778585 +- - - -0.3033289484746273 + - 0.9528859055612465 + - - -0.3033289484746273 + - 0.9528859055612465 +- - - -0.29131896327554796 + - 0.9566259779224375 + - - -0.29131896327554796 + - 0.9566259779224375 +- - - -0.2792628832928309 + - 0.9602146853776892 + - - -0.2792628832928309 + - 0.9602146853776892 +- - - -0.26716261613452225 + - 0.9636514600934084 + - - -0.26716261613452225 + - 0.9636514600934084 +- - - -0.25502007640031144 + - 0.9669357582759981 + - - -0.25502007640031144 + - 0.9669357582759981 +- - - -0.24283718537858734 + - 0.9700670602579007 + - - -0.24283718537858734 + - 0.9700670602579007 +- - - -0.23061587074244044 + - 0.9730448705798238 + - - -0.23061587074244044 + - 0.9730448705798238 +- - - -0.21835806624464577 + - 0.975868718069136 + - - -0.21835806624464577 + - 0.975868718069136 +- - - -0.20606571141169297 + - 0.9785381559144195 + - - -0.20606571141169297 + - 0.9785381559144195 +- - - -0.19374075123689813 + - 0.981052761736168 + - - -0.19374075123689813 + - 0.981052761736168 +- - - -0.18138513587265162 + - 0.9834121376536186 + - - -0.18138513587265162 + - 0.9834121376536186 +- - - -0.16900082032184968 + - 0.9856159103477083 + - - -0.16900082032184968 + - 0.9856159103477083 +- - - -0.15658976412855838 + - 0.9876637311201432 + - - -0.15658976412855838 + - 0.9876637311201432 +- - - -0.14415393106795907 + - 0.9895552759485718 + - - -0.14415393106795907 + - 0.9895552759485718 +- - - -0.13169528883562445 + - 0.9912902455378553 + - - -0.13169528883562445 + - 0.9912902455378553 +- - - -0.11921580873617425 + - 0.9928683653674237 + - - -0.11921580873617425 + - 0.9928683653674237 +- - - -0.10671746537135988 + - 0.9942893857347128 + - - -0.10671746537135988 + - 0.9942893857347128 +- - - -0.0942022363276273 + - 0.9955530817946745 + - - -0.0942022363276273 + - 0.9955530817946745 +- - - -0.08167210186320688 + - 0.9966592535953529 + - - -0.08167210186320688 + - 0.9966592535953529 +- - - -0.06912904459478485 + - 0.9976077261095226 + - - -0.06912904459478485 + - 0.9976077261095226 +- - - -0.056575049183792726 + - 0.998398349262383 + - - -0.056575049183792726 + - 0.998398349262383 +- - - -0.04401210202238211 + - 0.9990309979553044 + - - -0.04401210202238211 + - 0.9990309979553044 +- - - -0.031442190919121114 + - 0.9995055720856215 + - - -0.031442190919121114 + - 0.9995055720856215 +- - - -0.018867304784467676 + - 0.9998219965624732 + - - -0.018867304784467676 + - 0.9998219965624732 +- - - -0.006289433316068405 + - 0.9999802213186832 + - - -0.006289433316068405 + - 0.9999802213186832 +- - - 0.006289433316067026 + - 0.9999802213186832 + - - 0.006289433316067026 + - 0.9999802213186832 +- - - 0.0188673047844663 + - 0.9998219965624732 + - - 0.0188673047844663 + - 0.9998219965624732 +- - - 0.03144219091911974 + - 0.9995055720856215 + - - 0.03144219091911974 + - 0.9995055720856215 +- - - 0.04401210202238073 + - 0.9990309979553045 + - - 0.04401210202238073 + - 0.9990309979553045 +- - - 0.056575049183791346 + - 0.9983983492623831 + - - 0.056575049183791346 + - 0.9983983492623831 +- - - 0.06912904459478347 + - 0.9976077261095226 + - - 0.06912904459478347 + - 0.9976077261095226 +- - - 0.08167210186320639 + - 0.9966592535953529 + - - 0.08167210186320639 + - 0.9966592535953529 +- - - 0.09420223632762592 + - 0.9955530817946746 + - - 0.09420223632762592 + - 0.9955530817946746 +- - - 0.10671746537135851 + - 0.994289385734713 + - - 0.10671746537135851 + - 0.994289385734713 +- - - 0.11921580873617288 + - 0.9928683653674238 + - - 0.11921580873617288 + - 0.9928683653674238 +- - - 0.13169528883562306 + - 0.9912902455378555 + - - 0.13169528883562306 + - 0.9912902455378555 +- - - 0.14415393106795768 + - 0.9895552759485721 + - - 0.14415393106795768 + - 0.9895552759485721 +- - - 0.15658976412855702 + - 0.9876637311201434 + - - 0.15658976412855702 + - 0.9876637311201434 +- - - 0.16900082032184832 + - 0.9856159103477086 + - - 0.16900082032184832 + - 0.9856159103477086 +- - - 0.18138513587265026 + - 0.9834121376536189 + - - 0.18138513587265026 + - 0.9834121376536189 +- - - 0.19374075123689677 + - 0.9810527617361683 + - - 0.19374075123689677 + - 0.9810527617361683 +- - - 0.2060657114116916 + - 0.9785381559144198 + - - 0.2060657114116916 + - 0.9785381559144198 +- - - 0.21835806624464443 + - 0.9758687180691363 + - - 0.21835806624464443 + - 0.9758687180691363 +- - - 0.2306158707424391 + - 0.9730448705798241 + - - 0.2306158707424391 + - 0.9730448705798241 +- - - 0.24283718537858687 + - 0.9700670602579009 + - - 0.24283718537858687 + - 0.9700670602579009 +- - - 0.2550200764003101 + - 0.9669357582759984 + - - 0.2550200764003101 + - 0.9669357582759984 +- - - 0.2671626161345209 + - 0.9636514600934087 + - - 0.2671626161345209 + - 0.9636514600934087 +- - - 0.2792628832928296 + - 0.9602146853776896 + - - 0.2792628832928296 + - 0.9602146853776896 +- - - 0.2913189632755466 + - 0.956625977922438 + - - 0.2913189632755466 + - 0.956625977922438 +- - - 0.30332894847462605 + - 0.952885905561247 + - - 0.30332894847462605 + - 0.952885905561247 +- - - 0.3152909385755018 + - 0.9489950600778589 + - - 0.3152909385755018 + - 0.9489950600778589 +- - - 0.3272030408577709 + - 0.9449540571125286 + - - 0.3272030408577709 + - 0.9449540571125286 +- - - 0.33906337049467444 + - 0.9407635360646113 + - - 0.33906337049467444 + - 0.9407635360646113 +- - - 0.3508700508513296 + - 0.9364241599913926 + - - 0.3508700508513296 + - 0.9364241599913926 +- - - 0.36262121378166595 + - 0.9319366155031743 + - - 0.36262121378166595 + - 0.9319366155031743 +- - - 0.3743149999240179 + - 0.9273016126546327 + - - 0.3743149999240179 + - 0.9273016126546327 +- - - 0.3859495589953277 + - 0.9225198848324692 + - - 0.3859495589953277 + - 0.9225198848324692 +- - - 0.39752305008391264 + - 0.9175921886393666 + - - 0.39752305008391264 + - 0.9175921886393666 +- - - 0.40903364194074554 + - 0.9125193037742763 + - - 0.40903364194074554 + - 0.9125193037742763 +- - - 0.4204795132692139 + - 0.9073020329090445 + - - 0.4204795132692139 + - 0.9073020329090445 +- - - 0.4318588530132969 + - 0.9019412015614098 + - - 0.4318588530132969 + - 0.9019412015614098 +- - - 0.44316986064412556 + - 0.896437657964382 + - - 0.44316986064412556 + - 0.896437657964382 +- - - 0.45441074644487683 + - 0.8907922729320287 + - - 0.45441074644487683 + - 0.8907922729320287 +- - - 0.46557973179395645 + - 0.8850059397216877 + - - 0.46557973179395645 + - 0.8850059397216877 +- - - 0.47667504944642675 + - 0.8790795738926293 + - - 0.47667504944642675 + - 0.8790795738926293 +- - - 0.48769494381363376 + - 0.8730141131611886 + - - 0.48769494381363376 + - 0.8730141131611886 +- - - 0.4986376712409907 + - 0.8668105172523933 + - - 0.4986376712409907 + - 0.8668105172523933 +- - - 0.5095015002838723 + - 0.8604697677481082 + - - 0.5095015002838723 + - 0.8604697677481082 +- - - 0.520284711981578 + - 0.8539928679317214 + - - 0.520284711981578 + - 0.8539928679317214 +- - - 0.5309856001293194 + - 0.8473808426293968 + - - 0.5309856001293194 + - 0.8473808426293968 +- - - 0.5416024715481897 + - 0.8406347380479183 + - - 0.5416024715481897 + - 0.8406347380479183 +- - - 0.5521336463530699 + - 0.8337556216091518 + - - 0.5521336463530699 + - 0.8337556216091518 +- - - 0.5625774582184366 + - 0.8267445817811466 + - - 0.5625774582184366 + - 0.8267445817811466 +- - - 0.5729322546420195 + - 0.8196027279059118 + - - 0.5729322546420195 + - 0.8196027279059118 +- - - 0.5831963972062728 + - 0.8123311900238863 + - - 0.5831963972062728 + - 0.8123311900238863 +- - - 0.5933682618376198 + - 0.8049311186951352 + - - 0.5933682618376198 + - 0.8049311186951352 +- - - 0.6034462390634255 + - 0.7974036848172994 + - - 0.6034462390634255 + - 0.7974036848172994 +- - - 0.6134287342666611 + - 0.7897500794403265 + - - 0.6134287342666611 + - 0.7897500794403265 +- - - 0.6233141679382159 + - 0.7819715135780135 + - - 0.6233141679382159 + - 0.7819715135780135 +- - - 0.6331009759268206 + - 0.7740692180163913 + - - 0.6331009759268206 + - 0.7740692180163913 +- - - 0.6427876096865385 + - 0.7660444431189787 + - - 0.6427876096865385 + - 0.7660444431189787 +- - - 0.6523725365217901 + - 0.7578984586289417 + - - 0.6523725365217901 + - 0.7578984586289417 +- - - 0.6618542398298678 + - 0.7496325534681827 + - - 0.6618542398298678 + - 0.7496325534681827 +- - - 0.6712312193409025 + - 0.7412480355334005 + - - 0.6712312193409025 + - 0.7412480355334005 +- - - 0.6805019913552521 + - 0.7327462314891401 + - - 0.6805019913552521 + - 0.7327462314891401 +- - - 0.6896650889782615 + - 0.7241284865578805 + - - 0.6896650889782615 + - 0.7241284865578805 +- - - 0.698719062352367 + - 0.7153961643071823 + - - 0.698719062352367 + - 0.7153961643071823 +- - - 0.7076624788865039 + - 0.7065506464339328 + - - 0.7076624788865039 + - 0.7065506464339328 +- - - 0.7164939234827827 + - 0.6975933325457234 + - - 0.7164939234827827 + - 0.6975933325457234 +- - - 0.7252119987603968 + - 0.6885256399393928 + - - 0.7252119987603968 + - 0.6885256399393928 +- - - 0.7338153252767271 + - 0.6793490033767704 + - - 0.7338153252767271 + - 0.6793490033767704 +- - - 0.7423025417456087 + - 0.670064874857658 + - - 0.7423025417456087 + - 0.670064874857658 +- - - 0.7506723052527237 + - 0.6606747233900823 + - - 0.7506723052527237 + - 0.6606747233900823 +- - - 0.7589232914680881 + - 0.6511800347578566 + - - 0.7589232914680881 + - 0.6511800347578566 +- - - 0.767054194855598 + - 0.6415823112854891 + - - 0.767054194855598 + - 0.6415823112854891 +- - - 0.7750637288796014 + - 0.6318830716004724 + - - 0.7750637288796014 + - 0.6318830716004724 +- - - 0.7829506262084629 + - 0.6220838503929964 + - - 0.7829506262084629 + - 0.6220838503929964 +- - - 0.7907136389150935 + - 0.612186198173114 + - - 0.7907136389150935 + - 0.612186198173114 +- - - 0.7983515386744056 + - 0.60219168102541 + - - 0.7983515386744056 + - 0.60219168102541 +- - - 0.8058631169576688 + - 0.5921018803612016 + - - 0.8058631169576688 + - 0.5921018803612016 +- - - 0.8132471852237325 + - 0.5819183926683157 + - - 0.8132471852237325 + - 0.5819183926683157 +- - - 0.820502575107087 + - 0.5716428292584793 + - - 0.820502575107087 + - 0.5716428292584793 +- - - 0.8276281386027308 + - 0.5612768160123658 + - - 0.8276281386027308 + - 0.5612768160123658 +- - - 0.8346227482478168 + - 0.5508219931223347 + - - 0.8346227482478168 + - 0.5508219931223347 +- - - 0.8414852973000496 + - 0.5402800148329078 + - - 0.8414852973000496 + - 0.5402800148329078 +- - - 0.8482146999128017 + - 0.5296525491790214 + - - 0.8482146999128017 + - 0.5296525491790214 +- - - 0.8548098913069254 + - 0.5189412777220967 + - - 0.8548098913069254 + - 0.5189412777220967 +- - - 0.8612698279392301 + - 0.5081478952839703 + - - 0.8612698279392301 + - 0.5081478952839703 +- - - 0.8675934876676011 + - 0.49727410967872443 + - - 0.8675934876676011 + - 0.49727410967872443 +- - - 0.8737798699127283 + - 0.48632164144246715 + - - 0.8737798699127283 + - 0.48632164144246715 +- - - 0.8798279958164291 + - 0.4752922235610904 + - - 0.8798279958164291 + - 0.4752922235610904 +- - - 0.8857369083965291 + - 0.4641876011960666 + - - 0.8857369083965291 + - 0.4641876011960666 +- - - 0.8915056726982836 + - 0.4530095314083147 + - - 0.8915056726982836 + - 0.4530095314083147 +- - - 0.8971333759423138 + - 0.4417597828801838 + - - 0.8971333759423138 + - 0.4417597828801838 +- - - 0.9026191276690336 + - 0.43044013563559885 + - - 0.9026191276690336 + - 0.43044013563559885 +- - - 0.9079620598795458 + - 0.4190523807584107 + - - 0.9079620598795458 + - 0.4190523807584107 +- - - 0.9131613271729829 + - 0.4075983201089971 + - - 0.9131613271729829 + - 0.4075983201089971 +- - - 0.9182161068802737 + - 0.39607976603915773 + - - 0.9182161068802737 + - 0.39607976603915773 +- - - 0.9231255991943119 + - 0.3844985411053501 + - - 0.9231255991943119 + - 0.3844985411053501 +- - - 0.9278890272965089 + - 0.37285647778030967 + - - 0.9278890272965089 + - 0.37285647778030967 +- - - 0.932505637479707 + - 0.36115541816310226 + - - 0.932505637479707 + - 0.36115541816310226 +- - - 0.9369746992674379 + - 0.3493972136876513 + - - 0.9369746992674379 + - 0.3493972136876513 +- - - 0.9412955055295031 + - 0.3375837248297927 + - - 0.9412955055295031 + - 0.3375837248297927 +- - - 0.9454673725938633 + - 0.32571682081289105 + - - 0.9454673725938633 + - 0.32571682081289105 +- - - 0.9494896403548132 + - 0.3137983793120792 + - - 0.9494896403548132 + - 0.3137983793120792 +- - - 0.9533616723774291 + - 0.3018302861571574 + - - 0.9533616723774291 + - 0.3018302861571574 +- - - 0.9570828559982706 + - 0.2898144350342019 + - - 0.9570828559982706 + - 0.2898144350342019 +- - - 0.9606526024223209 + - 0.27775272718593136 + - - 0.9606526024223209 + - 0.27775272718593136 +- - - 0.9640703468161504 + - 0.26564707111087715 + - - 0.9640703468161504 + - 0.26564707111087715 +- - - 0.96733554839729 + - 0.25349938226140567 + - - 0.96733554839729 + - 0.25349938226140567 +- - - 0.9704476905197967 + - 0.24131158274064027 + - - 0.9704476905197967 + - 0.24131158274064027 +- - - 0.9734062807560024 + - 0.22908560099833106 + - - 0.9734062807560024 + - 0.22908560099833106 +- - - 0.9762108509744293 + - 0.21682337152572034 + - - 0.9762108509744293 + - 0.21682337152572034 +- - - 0.9788609574138614 + - 0.20452683454945125 + - - 0.9788609574138614 + - 0.20452683454945125 +- - - 0.9813561807535595 + - 0.1921979357245733 + - - 0.9813561807535595 + - 0.1921979357245733 +- - - 0.98369612617961 + - 0.17983862582668034 + - - 0.98369612617961 + - 0.17983862582668034 +- - - 0.9858804234473957 + - 0.1674508604432468 + - - 0.9858804234473957 + - 0.1674508604432468 +- - - 0.987908726940178 + - 0.15503659966419847 + - - 0.987908726940178 + - 0.15503659966419847 +- - - 0.9897807157237833 + - 0.14259780777177156 + - - 0.9897807157237833 + - 0.14259780777177156 +- - - 0.9914960935973847 + - 0.13013645292970846 + - - 0.9914960935973847 + - 0.13013645292970846 +- - - 0.9930545891403676 + - 0.11765450687183943 + - - 0.9930545891403676 + - 0.11765450687183943 +- - - 0.9944559557552775 + - 0.1051539445900992 + - - 0.9944559557552775 + - 0.1051539445900992 +- - - 0.9956999717068375 + - 0.09263674402202833 + - - 0.9956999717068375 + - 0.09263674402202833 +- - - 0.9967864401570342 + - 0.08010488573780816 + - - 0.9967864401570342 + - 0.08010488573780816 +- - - 0.9977151891962615 + - 0.06756035262687954 + - - 0.9977151891962615 + - 0.06756035262687954 +- - - 0.9984860718705224 + - 0.05500512958419429 + - - 0.9984860718705224 + - 0.05500512958419429 +- - - 0.9990989662046814 + - 0.042441203196148705 + - - 0.9990989662046814 + - 0.042441203196148705 +- - - 0.9995537752217638 + - 0.029870561426253633 + - - 0.9995537752217638 + - 0.029870561426253633 +- - - 0.9998504269583004 + - 0.01729519330057795 + - - 0.9998504269583004 + - 0.01729519330057795 +- - - 0.999988874475714 + - 0.004717088593032691 + - - 0.999988874475714 + - 0.004717088593032691 +- - - 0.999969095867747 + - -0.007861762489467534 + - - 0.999969095867747 + - -0.007861762489467534 +- - - 0.9997910942639262 + - -0.020439369621910786 + - - 0.9997910942639262 + - -0.020439369621910786 +- - - 0.9994548978290694 + - -0.03301374267611272 + - - 0.9994548978290694 + - -0.03301374267611272 +- - - 0.9989605597588275 + - -0.045582892035610355 + - - 0.9989605597588275 + - -0.045582892035610355 +- - - 0.9983081582712683 + - -0.058144828910474865 + - - 0.9983081582712683 + - -0.058144828910474865 +- - - 0.9974977965944998 + - -0.07069756565199363 + - - 0.9974977965944998 + - -0.07069756565199363 +- - - 0.9965296029503368 + - -0.08323911606717167 + - - 0.9965296029503368 + - -0.08323911606717167 +- - - 0.9954037305340127 + - -0.09576749573300279 + - - 0.9954037305340127 + - -0.09576749573300279 +- - - 0.9941203574899394 + - -0.1082807223104606 + - - 0.9941203574899394 + - -0.1082807223104606 +- - - 0.9926796868835203 + - -0.12077681585816072 + - - 0.9926796868835203 + - -0.12077681585816072 +- - - 0.9910819466690197 + - -0.1332537991456392 + - - 0.9910819466690197 + - -0.1332537991456392 +- - - 0.9893273896534936 + - -0.14570969796621086 + - - 0.9893273896534936 + - -0.14570969796621086 +- - - 0.9874162934567892 + - -0.1581425414493393 + - - 0.9874162934567892 + - -0.1581425414493393 +- - - 0.9853489604676167 + - -0.17055036237248902 + - - 0.9853489604676167 + - -0.17055036237248902 +- - - 0.9831257177957046 + - -0.18293119747238504 + - - 0.9831257177957046 + - -0.18293119747238504 +- - - 0.9807469172200398 + - -0.1952830877556692 + - - 0.9807469172200398 + - -0.1952830877556692 +- - - 0.9782129351332084 + - -0.2076040788088552 + - - 0.9782129351332084 + - -0.2076040788088552 +- - - 0.9755241724818389 + - -0.2198922211075767 + - - 0.9755241724818389 + - -0.2198922211075767 +- - - 0.9726810547031601 + - -0.23214557032506142 + - - 0.9726810547031601 + - -0.23214557032506142 +- - - 0.9696840316576879 + - -0.24436218763976586 + - - 0.9696840316576879 + - -0.24436218763976586 +- - - 0.9665335775580415 + - -0.25654014004216474 + - - 0.9665335775580415 + - -0.25654014004216474 +- - - 0.9632301908939129 + - -0.2686775006405933 + - - 0.9632301908939129 + - -0.2686775006405933 +- - - 0.9597743943531892 + - -0.2807723489661489 + - - 0.9597743943531892 + - -0.2807723489661489 +- - - 0.9561667347392514 + - -0.29282277127654904 + - - 0.9561667347392514 + - -0.29282277127654904 +- - - 0.9524077828844516 + - -0.3048268608589526 + - - 0.9524077828844516 + - -0.3048268608589526 +- - - 0.9484981335597957 + - -0.3167827183316413 + - - 0.9484981335597957 + - -0.3167827183316413 +- - - 0.9444384053808291 + - -0.32868845194456814 + - - 0.9444384053808291 + - -0.32868845194456814 +- - - 0.9402292407097596 + - -0.340542177878672 + - - 0.9402292407097596 + - -0.340542177878672 +- - - 0.9358713055538124 + - -0.3523420205439635 + - - 0.9358713055538124 + - -0.3523420205439635 +- - - 0.9313652894598542 + - -0.36408611287628373 + - - 0.9313652894598542 + - -0.36408611287628373 +- - - 0.9267119054052854 + - -0.37577259663273127 + - - 0.9267119054052854 + - -0.37577259663273127 +- - - 0.9219118896852252 + - -0.38739962268569283 + - - 0.9219118896852252 + - -0.38739962268569283 +- - - 0.9169660017960138 + - -0.3989653513154153 + - - 0.9169660017960138 + - -0.3989653513154153 +- - - 0.9118750243150339 + - -0.4104679525011135 + - - 0.9118750243150339 + - -0.4104679525011135 +- - - 0.9066397627768898 + - -0.4219056062105182 + - - 0.9066397627768898 + - -0.4219056062105182 +- - - 0.901261045545945 + - -0.4332765026878681 + - - 0.901261045545945 + - -0.4332765026878681 +- - - 0.895739723685256 + - -0.44457884274025133 + - - 0.895739723685256 + - -0.44457884274025133 +- - - 0.8900766708219062 + - -0.45581083802230066 + - - 0.8900766708219062 + - -0.45581083802230066 +- - - 0.8842727830087785 + - -0.46697071131914664 + - - 0.8842727830087785 + - -0.46697071131914664 +- - - 0.878328978582769 + - -0.47805669682763535 + - - 0.878328978582769 + - -0.47805669682763535 +- - - 0.8722461980194871 + - -0.48906704043571536 + - - 0.8722461980194871 + - -0.48906704043571536 +- - - 0.8660254037844392 + - -0.4999999999999992 + - - 0.8660254037844392 + - -0.4999999999999992 +- - - 0.8596675801807453 + - -0.5108538456214086 + - - 0.8596675801807453 + - -0.5108538456214086 +- - - 0.8531737331933934 + - -0.5216268599188969 + - - 0.8531737331933934 + - -0.5216268599188969 +- - - 0.8465448903300608 + - -0.5323173383011919 + - - 0.8465448903300608 + - -0.5323173383011919 +- - - 0.8397821004585404 + - -0.5429235892364983 + - - 0.8397821004585404 + - -0.5429235892364983 +- - - 0.8328864336407736 + - -0.5534439345201582 + - - 0.8328864336407736 + - -0.5534439345201582 +- - - 0.8258589809635439 + - -0.5638767095401768 + - - 0.8258589809635439 + - -0.5638767095401768 +- - - 0.8187008543658284 + - -0.5742202635406232 + - - 0.8187008543658284 + - -0.5742202635406232 +- - - 0.8114131864628666 + - -0.5844729598828138 + - - 0.8114131864628666 + - -0.5844729598828138 +- - - 0.803997130366941 + - -0.5946331763042861 + - - 0.803997130366941 + - -0.5946331763042861 +- - - 0.7964538595049301 + - -0.6046993051754741 + - - 0.7964538595049301 + - -0.6046993051754741 +- - - 0.7887845674326319 + - -0.6146697537540917 + - - 0.7887845674326319 + - -0.6146697537540917 +- - - 0.7809904676459185 + - -0.6245429444371375 + - - 0.7809904676459185 + - -0.6245429444371375 +- - - 0.7730727933887184 + - -0.6343173150105269 + - - 0.7730727933887184 + - -0.6343173150105269 +- - - 0.76503279745789 + - -0.6439913188962683 + - - 0.76503279745789 + - -0.6439913188962683 +- - - 0.7568717520049925 + - -0.6535634253971785 + - - 0.7568717520049925 + - -0.6535634253971785 +- - - 0.7485909483349908 + - -0.6630321199390865 + - - 0.7485909483349908 + - -0.6630321199390865 +- - - 0.7401916967019444 + - -0.6723959043104716 + - - 0.7401916967019444 + - -0.6723959043104716 +- - - 0.7316753261016786 + - -0.6816532968995326 + - - 0.7316753261016786 + - -0.6816532968995326 +- - - 0.7230431840615102 + - -0.69080283292861 + - - 0.7230431840615102 + - -0.69080283292861 +- - - 0.7142966364270213 + - -0.6998430646859649 + - - 0.7142966364270213 + - -0.6998430646859649 +- - - 0.7054370671459542 + - -0.7087725617548373 + - - 0.7054370671459542 + - -0.7087725617548373 +- - - 0.6964658780492222 + - -0.7175899112397874 + - - 0.6964658780492222 + - -0.7175899112397874 +- - - 0.6873844886291115 + - -0.7262937179902459 + - - 0.6873844886291115 + - -0.7262937179902459 +- - - 0.678194335814667 + - -0.7348826048212753 + - - 0.678194335814667 + - -0.7348826048212753 +- - - 0.6688968737443408 + - -0.7433552127314689 + - - 0.6688968737443408 + - -0.7433552127314689 +- - - 0.6594935735358967 + - -0.7517102011179926 + - - 0.6594935735358967 + - -0.7517102011179926 +- - - 0.6499859230536468 + - -0.7599462479886974 + - - 0.6499859230536468 + - -0.7599462479886974 +- - - 0.6403754266730268 + - -0.7680620501712988 + - - 0.6403754266730268 + - -0.7680620501712988 +- - - 0.6306636050425575 + - -0.7760563235195788 + - - 0.6306636050425575 + - -0.7760563235195788 +- - - 0.6208519948432446 + - -0.7839278031165648 + - - 0.6208519948432446 + - -0.7839278031165648 +- - - 0.6109421485454233 + - -0.7916752434746851 + - - 0.6109421485454233 + - -0.7916752434746851 +- - - 0.600935634163124 + - -0.7992974187328293 + - - 0.600935634163124 + - -0.7992974187328293 +- - - 0.5908340350059585 + - -0.8067931228503239 + - - 0.5908340350059585 + - -0.8067931228503239 +- - - 0.5806389494286068 + - -0.8141611697977519 + - - 0.5806389494286068 + - -0.8141611697977519 +- - - 0.570351990577902 + - -0.8214003937446248 + - - 0.570351990577902 + - -0.8214003937446248 +- - - 0.5599747861375968 + - -0.8285096492438412 + - - 0.5599747861375968 + - -0.8285096492438412 +- - - 0.5495089780708068 + - -0.8354878114129359 + - - 0.5495089780708068 + - -0.8354878114129359 +- - - 0.5389562223602165 + - -0.8423337761120617 + - - 0.5389562223602165 + - -0.8423337761120617 +- - - 0.5283181887460523 + - -0.8490464601186973 + - - 0.5283181887460523 + - -0.8490464601186973 +- - - 0.5175965604618786 + - -0.8556248012990465 + - - 0.5175965604618786 + - -0.8556248012990465 +- - - 0.5067930339682736 + - -0.8620677587760909 + - - 0.5067930339682736 + - -0.8620677587760909 +- - - 0.49590931868438975 + - -0.8683743130942925 + - - 0.49590931868438975 + - -0.8683743130942925 +- - - 0.4849471367174889 + - -0.8745434663808935 + - - 0.4849471367174889 + - -0.8745434663808935 +- - - 0.4739082225904436 + - -0.8805742425038144 + - - 0.4739082225904436 + - -0.8805742425038144 +- - - 0.4627943229673003 + - -0.886465687226098 + - - 0.4627943229673003 + - -0.886465687226098 +- - - 0.4516071963768956 + - -0.8922168683569035 + - - 0.4516071963768956 + - -0.8922168683569035 +- - - 0.44034861293462074 + - -0.8978268758989985 + - - 0.44034861293462074 + - -0.8978268758989985 +- - - 0.42902035406232714 + - -0.903294822192752 + - - 0.42902035406232714 + - -0.903294822192752 +- - - 0.4176242122064685 + - -0.9086198420565812 + - - 0.4176242122064685 + - -0.9086198420565812 +- - - 0.4061619905544733 + - -0.9138010929238529 + - - 0.4061619905544733 + - -0.9138010929238529 +- - - 0.3946355027494409 + - -0.918837754976196 + - - 0.3946355027494409 + - -0.918837754976196 +- - - 0.38304657260316866 + - -0.9237290312732221 + - - 0.38304657260316866 + - -0.9237290312732221 +- - - 0.37139703380756833 + - -0.9284741478786256 + - - 0.37139703380756833 + - -0.9284741478786256 +- - - 0.3596887296445368 + - -0.9330723539826369 + - - 0.3596887296445368 + - -0.9330723539826369 +- - - 0.34792351269428423 + - -0.9375229220208273 + - - 0.34792351269428423 + - -0.9375229220208273 +- - - 0.3361032445422173 + - -0.9418251477892244 + - - 0.3361032445422173 + - -0.9418251477892244 +- - - 0.3242297954843714 + - -0.9459783505557422 + - - 0.3242297954843714 + - -0.9459783505557422 +- - - 0.31230504423149086 + - -0.9499818731678866 + - - 0.31230504423149086 + - -0.9499818731678866 +- - - 0.3003308776117511 + - -0.9538350821567402 + - - 0.3003308776117511 + - -0.9538350821567402 +- - - 0.28830919027222335 + - -0.9575373678371905 + - - 0.28830919027222335 + - -0.9575373678371905 +- - - 0.27624188437907515 + - -0.9610881444044025 + - - 0.27624188437907515 + - -0.9610881444044025 +- - - 0.264130869316608 + - -0.9644868500265066 + - - 0.264130869316608 + - -0.9644868500265066 +- - - 0.2519780613851261 + - -0.9677329469334987 + - - 0.2519780613851261 + - -0.9677329469334987 +- - - 0.2397853834977361 + - -0.9708259215023276 + - - 0.2397853834977361 + - -0.9708259215023276 +- - - 0.22755476487608342 + - -0.9737652843381666 + - - 0.22755476487608342 + - -0.9737652843381666 +- - - 0.2152881407450906 + - -0.9765505703518492 + - - 0.2152881407450906 + - -0.9765505703518492 +- - - 0.20298745202676252 + - -0.9791813388334577 + - - 0.20298745202676252 + - -0.9791813388334577 +- - - 0.19065464503306495 + - -0.9816571735220581 + - - 0.19065464503306495 + - -0.9816571735220581 +- - - 0.17829167115797728 + - -0.9839776826715613 + - - 0.17829167115797728 + - -0.9839776826715613 +- - - 0.1659004865687139 + - -0.9861424991127113 + - - 0.1659004865687139 + - -0.9861424991127113 +- - - 0.15348305189621775 + - -0.9881512803111794 + - - 0.15348305189621775 + - -0.9881512803111794 +- - - 0.14104133192492 + - -0.9900037084217637 + - - 0.14104133192492 + - -0.9900037084217637 +- - - 0.12857729528187029 + - -0.9916994903386805 + - - 0.12857729528187029 + - -0.9916994903386805 +- - - 0.11609291412523105 + - -0.9932383577419429 + - - 0.11609291412523105 + - -0.9932383577419429 +- - - 0.10359016383224108 + - -0.9946200671398147 + - - 0.10359016383224108 + - -0.9946200671398147 +- - - 0.09107102268664179 + - -0.9958443999073395 + - - 0.09107102268664179 + - -0.9958443999073395 +- - - 0.07853747156566976 + - -0.996911162320932 + - - 0.07853747156566976 + - -0.996911162320932 +- - - 0.0659914936266216 + - -0.9978201855890306 + - - 0.0659914936266216 + - -0.9978201855890306 +- - - 0.05343507399305771 + - -0.9985713258788059 + - - 0.05343507399305771 + - -0.9985713258788059 +- - - 0.04087019944071283 + - -0.9991644643389177 + - - 0.04087019944071283 + - -0.9991644643389177 +- - - 0.028298858083118522 + - -0.9995995071183216 + - - 0.028298858083118522 + - -0.9995995071183216 +- - - 0.01572303905704239 + - -0.9998763853811183 + - - 0.01572303905704239 + - -0.9998763853811183 +- - - 0.003144732207736932 + - -0.9999950553174458 + - - 0.003144732207736932 + - -0.9999950553174458 +- - - -0.009434072225895224 + - -0.999955498150411 + - - -0.009434072225895224 + - -0.999955498150411 +- - - -0.02201138392622685 + - -0.9997577201390606 + - - -0.02201138392622685 + - -0.9997577201390606 +- - - -0.03458521281181564 + - -0.9994017525773914 + - - -0.03458521281181564 + - -0.9994017525773914 +- - - -0.04715356935230482 + - -0.9988876517893979 + - - -0.04715356935230482 + - -0.9988876517893979 +- - - -0.05971446488320808 + - -0.9982154991201609 + - - -0.05971446488320808 + - -0.9982154991201609 +- - - -0.07226591192058601 + - -0.9973854009229762 + - - -0.07226591192058601 + - -0.9973854009229762 +- - - -0.08480592447550901 + - -0.9963974885425265 + - - -0.08480592447550901 + - -0.9963974885425265 +- - - -0.0973325183683015 + - -0.9952519182940992 + - - -0.0973325183683015 + - -0.9952519182940992 +- - - -0.1098437115424997 + - -0.9939488714388522 + - - -0.1098437115424997 + - -0.9939488714388522 +- - - -0.12233752437845594 + - -0.9924885541551351 + - - -0.12233752437845594 + - -0.9924885541551351 +- - - -0.13481198000658376 + - -0.9908711975058637 + - - -0.13481198000658376 + - -0.9908711975058637 +- - - -0.14726510462013975 + - -0.9890970574019616 + - - -0.14726510462013975 + - -0.9890970574019616 +- - - -0.15969492778754882 + - -0.9871664145618658 + - - -0.15969492778754882 + - -0.9871664145618658 +- - - -0.17209948276416748 + - -0.9850795744671118 + - - -0.17209948276416748 + - -0.9850795744671118 +- - - -0.18447680680349163 + - -0.9828368673139949 + - - -0.18447680680349163 + - -0.9828368673139949 +- - - -0.19682494146770374 + - -0.9804386479613271 + - - -0.19682494146770374 + - -0.9804386479613271 +- - - -0.2091419329375665 + - -0.9778852958742853 + - - -0.2091419329375665 + - -0.9778852958742853 +- - - -0.22142583232155733 + - -0.9751772150643726 + - - -0.22142583232155733 + - -0.9751772150643726 +- - - -0.23367469596425144 + - -0.9723148340254892 + - - -0.23367469596425144 + - -0.9723148340254892 +- - - -0.24588658575385006 + - -0.9692986056661356 + - - -0.24588658575385006 + - -0.9692986056661356 +- - - -0.2580595694288491 + - -0.9661290072377483 + - - -0.2580595694288491 + - -0.9661290072377483 +- - - -0.2701917208837818 + - -0.9628065402591844 + - - -0.2701917208837818 + - -0.9628065402591844 +- - - -0.2822811204739704 + - -0.9593317304373705 + - - -0.2822811204739704 + - -0.9593317304373705 +- - - -0.29432585531928135 + - -0.9557051275841171 + - - -0.29432585531928135 + - -0.9557051275841171 +- - - -0.30632401960678207 + - -0.951927305529127 + - - -0.30632401960678207 + - -0.951927305529127 +- - - -0.31827371489230794 + - -0.9479988620291956 + - - -0.31827371489230794 + - -0.9479988620291956 +- - - -0.3301730504008353 + - -0.9439204186736335 + - - -0.3301730504008353 + - -0.9439204186736335 +- - - -0.342020143325668 + - -0.9396926207859086 + - - -0.342020143325668 + - -0.9396926207859086 +- - - -0.35381311912633706 + - -0.9353161373215435 + - - -0.35381311912633706 + - -0.9353161373215435 +- - - -0.3655501118252182 + - -0.9307916607622624 + - - -0.3655501118252182 + - -0.9307916607622624 +- - - -0.37722926430276815 + - -0.9261199070064267 + - - -0.37722926430276815 + - -0.9261199070064267 +- - - -0.3888487285913865 + - -0.9213016152557545 + - - -0.3888487285913865 + - -0.9213016152557545 +- - - -0.4004066661678036 + - -0.9163375478983632 + - - -0.4004066661678036 + - -0.9163375478983632 +- - - -0.4119012482439916 + - -0.9112284903881362 + - - -0.4119012482439916 + - -0.9112284903881362 +- - - -0.4233306560565341 + - -0.9059752511204401 + - - -0.4233306560565341 + - -0.9059752511204401 +- - - -0.4346930811543944 + - -0.9005786613042189 + - - -0.4346930811543944 + - -0.9005786613042189 +- - - -0.4459867256850755 + - -0.8950395748304681 + - - -0.4459867256850755 + - -0.8950395748304681 +- - - -0.4572098026790778 + - -0.8893588681371309 + - - -0.4572098026790778 + - -0.8893588681371309 +- - - -0.46836053633265995 + - -0.8835374400704156 + - - -0.46836053633265995 + - -0.8835374400704156 +- - - -0.47943716228880834 + - -0.8775762117425784 + - - -0.47943716228880834 + - -0.8775762117425784 +- - - -0.4904379279164198 + - -0.8714761263861728 + - - -0.4904379279164198 + - -0.8714761263861728 +- - - -0.5013610925876044 + - -0.8652381492048091 + - - -0.5013610925876044 + - -0.8652381492048091 +- - - -0.5122049279531135 + - -0.8588632672204265 + - - -0.5122049279531135 + - -0.8588632672204265 +- - - -0.5229677182158008 + - -0.852352489117125 + - - -0.5229677182158008 + - -0.852352489117125 +- - - -0.5336477604021214 + - -0.8457068450815567 + - - -0.5336477604021214 + - -0.8457068450815567 +- - - -0.5442433646315787 + - -0.8389273866399275 + - - -0.5442433646315787 + - -0.8389273866399275 +- - - -0.5547528543841161 + - -0.8320151864916143 + - - -0.5547528543841161 + - -0.8320151864916143 +- - - -0.5651745667653925 + - -0.8249713383394304 + - - -0.5651745667653925 + - -0.8249713383394304 +- - - -0.5755068527698889 + - -0.8177969567165786 + - - -0.5755068527698889 + - -0.8177969567165786 +- - - -0.5857480775418389 + - -0.8104931768102923 + - - -0.5857480775418389 + - -0.8104931768102923 +- - - -0.5958966206338965 + - -0.8030611542822266 + - - -0.5958966206338965 + - -0.8030611542822266 +- - - -0.6059508762635476 + - -0.7955020650855904 + - - -0.6059508762635476 + - -0.7955020650855904 +- - - -0.6159092535671783 + - -0.7878171052790878 + - - -0.6159092535671783 + - -0.7878171052790878 +- - - -0.6257701768518052 + - -0.7800074908376589 + - - -0.6257701768518052 + - -0.7800074908376589 +- - - -0.6355320858443827 + - -0.7720744574600873 + - - -0.6355320858443827 + - -0.7720744574600873 +- - - -0.6451934359386927 + - -0.76401926037347 + - - -0.6451934359386927 + - -0.76401926037347 +- - - -0.6547526984397336 + - -0.7558431741346133 + - - -0.6547526984397336 + - -0.7558431741346133 +- - - -0.6642083608056132 + - -0.7475474924283543 + - - -0.6642083608056132 + - -0.7475474924283543 +- - - -0.6735589268868657 + - -0.7391335278628713 + - - -0.6735589268868657 + - -0.7391335278628713 +- - - -0.6828029171631881 + - -0.7306026117619896 + - - -0.6828029171631881 + - -0.7306026117619896 +- - - -0.6919388689775459 + - -0.7219560939545248 + - - -0.6919388689775459 + - -0.7219560939545248 +- - - -0.7009653367675964 + - -0.7131953425607112 + - - -0.7009653367675964 + - -0.7131953425607112 +- - - -0.7098808922944282 + - -0.7043217437757168 + - - -0.7098808922944282 + - -0.7043217437757168 +- - - -0.7186841248685372 + - -0.695336701650319 + - - -0.7186841248685372 + - -0.695336701650319 +- - - -0.7273736415730482 + - -0.6862416378687342 + - - -0.7273736415730482 + - -0.6862416378687342 +- - - -0.7359480674841022 + - -0.6770379915236775 + - - -0.7359480674841022 + - -0.6770379915236775 +- - - -0.7444060458884184 + - -0.6677272188886492 + - - -0.7444060458884184 + - -0.6677272188886492 +- - - -0.7527462384979536 + - -0.6583107931875202 + - - -0.7527462384979536 + - -0.6583107931875202 +- - - -0.7609673256616669 + - -0.648790204361418 + - - -0.7609673256616669 + - -0.648790204361418 +- - - -0.7690680065743155 + - -0.6391669588329865 + - - -0.7690680065743155 + - -0.6391669588329865 +- - - -0.7770469994822877 + - -0.6294425792680167 + - - -0.7770469994822877 + - -0.6294425792680167 +- - - -0.7849030418864043 + - -0.619618604334529 + - - -0.7849030418864043 + - -0.619618604334529 +- - - -0.7926348907416839 + - -0.609696588459308 + - - -0.7926348907416839 + - -0.609696588459308 +- - - -0.8002413226540318 + - -0.5996781015819452 + - - -0.8002413226540318 + - -0.5996781015819452 +- - - -0.807721134073806 + - -0.5895647289064406 + - - -0.807721134073806 + - -0.5895647289064406 +- - - -0.8150731414862619 + - -0.5793580706503675 + - - -0.8150731414862619 + - -0.5793580706503675 +- - - -0.8222961815988086 + - -0.5690597417916851 + - - -0.8222961815988086 + - -0.5690597417916851 +- - - -0.8293891115250823 + - -0.5586713718131927 + - - -0.8293891115250823 + - -0.5586713718131927 +- - - -0.8363508089657752 + - -0.5481946044447112 + - - -0.8363508089657752 + - -0.5481946044447112 +- - - -0.8431801723862219 + - -0.537631097402988 + - - -0.8431801723862219 + - -0.537631097402988 +- - - -0.8498761211906855 + - -0.5269825221294112 + - - -0.8498761211906855 + - -0.5269825221294112 +- - - -0.8564375958933453 + - -0.5162505635255297 + - - -0.8564375958933453 + - -0.5162505635255297 +- - - -0.8628635582859301 + - -0.5054369196864662 + - - -0.8628635582859301 + - -0.5054369196864662 +- - - -0.8691529916019983 + - -0.49454330163221977 + - - -0.8691529916019983 + - -0.49454330163221977 +- - - -0.8753049006778127 + - -0.4835714330369447 + - - -0.8753049006778127 + - -0.4835714330369447 +- - - -0.8813183121098064 + - -0.4725230499562131 + - - -0.8813183121098064 + - -0.4725230499562131 +- - - -0.8871922744086038 + - -0.46139990055231767 + - - -0.8871922744086038 + - -0.46139990055231767 +- - - -0.8929258581495678 + - -0.4502037448176746 + - - -0.8929258581495678 + - -0.4502037448176746 +- - - -0.898518156119867 + - -0.43893635429633115 + - - -0.898518156119867 + - -0.43893635429633115 +- - - -0.9039682834620154 + - -0.42759951180367056 + - - -0.9039682834620154 + - -0.42759951180367056 +- - - -0.9092753778138881 + - -0.4161950111443084 + - - -0.9092753778138881 + - -0.4161950111443084 +- - - -0.914438599445165 + - -0.40472465682827513 + - - -0.914438599445165 + - -0.40472465682827513 +- - - -0.919457131390205 + - -0.39319026378547983 + - - -0.919457131390205 + - -0.39319026378547983 +- - - -0.9243301795773077 + - -0.38159365707855025 + - - -0.9243301795773077 + - -0.38159365707855025 +- - - -0.9290569729543624 + - -0.36993667161404425 + - - -0.9290569729543624 + - -0.36993667161404425 +- - - -0.9336367636108461 + - -0.3582211518521277 + - - -0.9336367636108461 + - -0.3582211518521277 +- - - -0.9380688268961654 + - -0.34644895151472466 + - - -0.9380688268961654 + - -0.34644895151472466 +- - - -0.9423524615343185 + - -0.3346219332922018 + - - -0.9423524615343185 + - -0.3346219332922018 +- - - -0.946486989734852 + - -0.32274196854865056 + - - -0.946486989734852 + - -0.32274196854865056 +- - - -0.9504717573001114 + - -0.31081093702577167 + - - -0.9504717573001114 + - -0.31081093702577167 +- - - -0.9543061337287484 + - -0.2988307265454612 + - - -0.9543061337287484 + - -0.2988307265454612 +- - - -0.9579895123154887 + - -0.2868032327110909 + - - -0.9579895123154887 + - -0.2868032327110909 +- - - -0.9615213102471251 + - -0.27473035860758444 + - - -0.9615213102471251 + - -0.27473035860758444 +- - - -0.9649009686947388 + - -0.2626140145002827 + - - -0.9649009686947388 + - -0.2626140145002827 +- - - -0.9681279529021183 + - -0.25045611753270025 + - - -0.9681279529021183 + - -0.25045611753270025 +- - - -0.9712017522703761 + - -0.23825859142316594 + - - -0.9712017522703761 + - -0.23825859142316594 +- - - -0.9741218804387358 + - -0.22602336616045093 + - - -0.9741218804387358 + - -0.22602336616045093 +- - - -0.9768878753614922 + - -0.21375237769837674 + - - -0.9768878753614922 + - -0.21375237769837674 +- - - -0.9794992993811164 + - -0.2014475676495055 + - - -0.9794992993811164 + - -0.2014475676495055 +- - - -0.9819557392975065 + - -0.18911088297791753 + - - -0.9819557392975065 + - -0.18911088297791753 +- - - -0.9842568064333685 + - -0.17674427569114207 + - - -0.9842568064333685 + - -0.17674427569114207 +- - - -0.9864021366957143 + - -0.1643497025313075 + - - -0.9864021366957143 + - -0.1643497025313075 +- - - -0.9883913906334727 + - -0.1519291246655162 + - - -0.9883913906334727 + - -0.1519291246655162 +- - - -0.9902242534911982 + - -0.1394845073755471 + - - -0.9902242534911982 + - -0.1394845073755471 +- - - -0.9919004352588768 + - -0.12701781974687945 + - - -0.9919004352588768 + - -0.12701781974687945 +- - - -0.9934196707178105 + - -0.11453103435714257 + - - -0.9934196707178105 + - -0.11453103435714257 +- - - -0.9947817194825852 + - -0.10202612696398496 + - - -0.9947817194825852 + - -0.10202612696398496 +- - - -0.9959863660391042 + - -0.08950507619246842 + - - -0.9959863660391042 + - -0.08950507619246842 +- - - -0.9970334197786901 + - -0.07696986322198038 + - - -0.9970334197786901 + - -0.07696986322198038 +- - - -0.9979227150282431 + - -0.0644224714727701 + - - -0.9979227150282431 + - -0.0644224714727701 +- - - -0.9986541110764564 + - -0.051864886292102175 + - - -0.9986541110764564 + - -0.051864886292102175 +- - - -0.9992274921960794 + - -0.03929909464013164 + - - -0.9992274921960794 + - -0.03929909464013164 +- - - -0.9996427676622299 + - -0.026727084775506123 + - - -0.9996427676622299 + - -0.026727084775506123 +- - - -0.9998998717667489 + - -0.014150845940762564 + - - -0.9998998717667489 + - -0.014150845940762564 +- - - -0.9999987638285974 + - -0.001572368047586014 + - - -0.9999987638285974 + - -0.001572368047586014 +- - - -0.9999394282002937 + - 0.0110063586380641 + - - -0.9999394282002937 + - 0.0110063586380641 +- - - -0.9997218742703887 + - 0.02358334381085534 + - - -0.9997218742703887 + - 0.02358334381085534 +- - - -0.9993461364619809 + - 0.036156597441018276 + - - -0.9993461364619809 + - 0.036156597441018276 +- - - -0.9988122742272693 + - 0.04872413008921046 + - - -0.9988122742272693 + - 0.04872413008921046 +- - - -0.9981203720381463 + - 0.06128395322131545 + - - -0.9981203720381463 + - 0.06128395322131545 +- - - -0.9972705393728328 + - 0.0738340795230701 + - - -0.9972705393728328 + - 0.0738340795230701 +- - - -0.9962629106985544 + - 0.08637252321452737 + - - -0.9962629106985544 + - 0.08637252321452737 +- - - -0.9950976454502662 + - 0.09889730036424782 + - - -0.9950976454502662 + - 0.09889730036424782 +- - - -0.9937749280054243 + - 0.11140642920322712 + - - -0.9937749280054243 + - 0.11140642920322712 +- - - -0.9922949676548137 + - 0.12389793043845473 + - - -0.9922949676548137 + - 0.12389793043845473 +- - - -0.9906579985694319 + - 0.1363698275660986 + - - -0.9906579985694319 + - 0.1363698275660986 +- - - -0.9888642797634358 + - 0.14882014718424852 + - - -0.9888642797634358 + - 0.14882014718424852 +- - - -0.9869140950531602 + - 0.16124691930515087 + - - -0.9869140950531602 + - 0.16124691930515087 +- - - -0.9848077530122081 + - 0.17364817766692972 + - - -0.9848077530122081 + - 0.17364817766692972 +- - - -0.9825455869226281 + - 0.18602196004469043 + - - -0.9825455869226281 + - 0.18602196004469043 +- - - -0.9801279547221767 + - 0.19836630856101212 + - - -0.9801279547221767 + - 0.19836630856101212 +- - - -0.9775552389476866 + - 0.21067926999572462 + - - -0.9775552389476866 + - 0.21067926999572462 +- - - -0.9748278466745344 + - 0.2229588960949763 + - - -0.9748278466745344 + - 0.2229588960949763 +- - - -0.9719462094522341 + - 0.23520324387948816 + - - -0.9719462094522341 + - 0.23520324387948816 +- - - -0.9689107832361499 + - 0.24741037595200138 + - - -0.9689107832361499 + - 0.24741037595200138 +- - - -0.9657220483153551 + - 0.25957836080381363 + - - -0.9657220483153551 + - 0.25957836080381363 +- - - -0.9623805092366339 + - 0.27170527312041143 + - - -0.9623805092366339 + - 0.27170527312041143 +- - - -0.9588866947246498 + - 0.2837891940860965 + - - -0.9588866947246498 + - 0.2837891940860965 +- - - -0.9552411575982872 + - 0.29582821168760115 + - - -0.9552411575982872 + - 0.29582821168760115 +- - - -0.9514444746831768 + - 0.30782042101662727 + - - -0.9514444746831768 + - 0.30782042101662727 +- - - -0.9474972467204302 + - 0.31976392457124386 + - - -0.9474972467204302 + - 0.31976392457124386 +- - - -0.9434000982715814 + - 0.3316568325561384 + - - -0.9434000982715814 + - 0.3316568325561384 +- - - -0.9391536776197683 + - 0.3434972631816217 + - - -0.9391536776197683 + - 0.3434972631816217 +- - - -0.9347586566671513 + - 0.35528334296139286 + - - -0.9347586566671513 + - 0.35528334296139286 +- - - -0.9302157308286049 + - 0.3670132070089637 + - - -0.9302157308286049 + - 0.3670132070089637 +- - - -0.9255256189216783 + - 0.3786849993327492 + - - -0.9255256189216783 + - 0.3786849993327492 +- - - -0.9206890630528639 + - 0.3902968731297237 + - - -0.9206890630528639 + - 0.3902968731297237 +- - - -0.9157068285001696 + - 0.40184699107765015 + - - -0.9157068285001696 + - 0.40184699107765015 +- - - -0.9105797035920364 + - 0.41333352562578207 + - - -0.9105797035920364 + - 0.41333352562578207 +- - - -0.9053084995825972 + - 0.4247546592840467 + - - -0.9053084995825972 + - 0.4247546592840467 +- - - -0.8998940505233184 + - 0.4361085849106107 + - - -0.8998940505233184 + - 0.4361085849106107 +- - - -0.8943372131310279 + - 0.4473935059978257 + - - -0.8943372131310279 + - 0.4473935059978257 +- - - -0.8886388666523561 + - 0.45860763695649037 + - - -0.8886388666523561 + - 0.45860763695649037 +- - - -0.8827999127246203 + - 0.4697492033983695 + - - -0.8827999127246203 + - 0.4697492033983695 +- - - -0.8768212752331539 + - 0.48081644241696414 + - - -0.8768212752331539 + - 0.48081644241696414 +- - - -0.8707039001651283 + - 0.49180760286644026 + - - -0.8707039001651283 + - 0.49180760286644026 +- - - -0.8644487554598653 + - 0.502720945638721 + - - -0.8644487554598653 + - 0.502720945638721 +- - - -0.8580568308556884 + - 0.5135547439386501 + - - -0.8580568308556884 + - 0.5135547439386501 +- - - -0.8515291377333118 + - 0.5243072835572309 + - - -0.8515291377333118 + - 0.5243072835572309 +- - - -0.8448667089558188 + - 0.53497686314285 + - - -0.8448667089558188 + - 0.53497686314285 +- - - -0.838070598705227 + - 0.5455617944704909 + - - -0.838070598705227 + - 0.5455617944704909 +- - - -0.8311418823156947 + - 0.5560604027088458 + - - -0.8311418823156947 + - 0.5560604027088458 +- - - -0.8240816561033651 + - 0.5664710266853329 + - - -0.8240816561033651 + - 0.5664710266853329 +- - - -0.8168910371929057 + - 0.5767920191489293 + - - -0.8168910371929057 + - 0.5767920191489293 +- - - -0.8095711633407447 + - 0.5870217470308176 + - - -0.8095711633407447 + - 0.5870217470308176 +- - - -0.8021231927550442 + - 0.5971585917027857 + - - -0.8021231927550442 + - 0.5971585917027857 +- - - -0.7945483039124446 + - 0.6072009492333305 + - - -0.7945483039124446 + - 0.6072009492333305 +- - - -0.7868476953715905 + - 0.6171472306414546 + - - -0.7868476953715905 + - 0.6171472306414546 +- - - -0.7790225855834922 + - 0.6269958621480771 + - - -0.7790225855834922 + - 0.6269958621480771 +- - - -0.7710742126987252 + - 0.6367452854250599 + - - -0.7710742126987252 + - 0.6367452854250599 +- - - -0.7630038343715285 + - 0.6463939578417678 + - - -0.7630038343715285 + - 0.6463939578417678 +- - - -0.7548127275607995 + - 0.6559403527091668 + - - -0.7548127275607995 + - 0.6559403527091668 +- - - -0.7465021883280534 + - 0.6653829595213779 + - - -0.7465021883280534 + - 0.6653829595213779 +- - - -0.7380735316323398 + - 0.6747202841946918 + - - -0.7380735316323398 + - 0.6747202841946918 +- - - -0.7295280911221899 + - 0.6839508493039641 + - - -0.7295280911221899 + - 0.6839508493039641 +- - - -0.7208672189245859 + - 0.6930731943163961 + - - -0.7208672189245859 + - 0.6930731943163961 +- - - -0.7120922854310258 + - 0.7020858758226223 + - - -0.7120922854310258 + - 0.7020858758226223 +- - - -0.703204679080685 + - 0.7109874677651012 + - - -0.703204679080685 + - 0.7109874677651012 +- - - -0.694205806140723 + - 0.719776561663763 + - - -0.694205806140723 + - 0.719776561663763 +- - - -0.685097090483782 + - 0.7284517668388598 + - - -0.685097090483782 + - 0.7284517668388598 +- - - -0.6758799733626797 + - 0.7370117106310208 + - - -0.6758799733626797 + - 0.7370117106310208 +- - - -0.6665559131823733 + - 0.745455038618435 + - - -0.6665559131823733 + - 0.745455038618435 +- - - -0.6571263852691893 + - 0.7537804148311689 + - - -0.6571263852691893 + - 0.7537804148311689 +- - - -0.6475928816373955 + - 0.7619865219625438 + - - -0.6475928816373955 + - 0.7619865219625438 +- - - -0.6379569107531127 + - 0.7700720615775806 + - - -0.6379569107531127 + - 0.7700720615775806 +- - - -0.6282199972956439 + - 0.7780357543184383 + - - -0.6282199972956439 + - 0.7780357543184383 +- - - -0.6183836819162163 + - 0.7858763401068541 + - - -0.6183836819162163 + - 0.7858763401068541 +- - - -0.6084495209942188 + - 0.7935925783435136 + - - -0.6084495209942188 + - 0.7935925783435136 +- - - -0.5984190863909279 + - 0.8011832481043567 + - - -0.5984190863909279 + - 0.8011832481043567 +- - - -0.5882939652008056 + - 0.8086471483337546 + - - -0.5882939652008056 + - 0.8086471483337546 +- - - -0.5780757595003719 + - 0.8159830980345537 + - - -0.5780757595003719 + - 0.8159830980345537 +- - - -0.5677660860947084 + - 0.8231899364549449 + - - -0.5677660860947084 + - 0.8231899364549449 +- - - -0.5573665762616435 + - 0.8302665232721198 + - - -0.5573665762616435 + - 0.8302665232721198 +- - - -0.546878875493628 + - 0.8372117387727103 + - - -0.546878875493628 + - 0.8372117387727103 +- - - -0.5363046432373839 + - 0.8440244840299495 + - - -0.5363046432373839 + - 0.8440244840299495 +- - - -0.5256455526313215 + - 0.850703681077561 + - - -0.5256455526313215 + - 0.850703681077561 +- - - -0.5149032902408143 + - 0.8572482730803158 + - - -0.5149032902408143 + - 0.8572482730803158 +- - - -0.5040795557913256 + - 0.86365722450126 + - - -0.5040795557913256 + - 0.86365722450126 +- - - -0.49317606189947616 + - 0.8699295212655587 + - - -0.49317606189947616 + - 0.8699295212655587 +- - - -0.4821945338020488 + - 0.8760641709209576 + - - -0.4821945338020488 + - 0.8760641709209576 +- - - -0.4711367090830182 + - 0.8820602027948112 + - - -0.4711367090830182 + - 0.8820602027948112 +- - - -0.46000433739861224 + - 0.8879166681476723 + - - -0.46000433739861224 + - 0.8879166681476723 +- - - -0.44879918020046267 + - 0.893632640323412 + - - -0.44879918020046267 + - 0.893632640323412 +- - - -0.43752301045690567 + - 0.8992072148958361 + - - -0.43752301045690567 + - 0.8992072148958361 +- - - -0.4261776123724359 + - 0.9046395098117977 + - - -0.4261776123724359 + - 0.9046395098117977 +- - - -0.4147647811054085 + - 0.909928665530756 + - - -0.4147647811054085 + - 0.909928665530756 +- - - -0.403286322483982 + - 0.9150738451607857 + - - -0.403286322483982 + - 0.9150738451607857 +- - - -0.39174405272039897 + - 0.9200742345909907 + - - -0.39174405272039897 + - 0.9200742345909907 +- - - -0.3801397981235976 + - 0.9249290426203247 + - - -0.3801397981235976 + - 0.9249290426203247 +- - - -0.3684753948102517 + - 0.9296375010827764 + - - -0.3684753948102517 + - 0.9296375010827764 +- - - -0.3567526884142328 + - 0.9341988649689195 + - - -0.3567526884142328 + - 0.9341988649689195 +- - - -0.34497353379459245 + - 0.9386124125437886 + - - -0.34497353379459245 + - 0.9386124125437886 +- - - -0.33313979474205874 + - 0.9428774454610838 + - - -0.33313979474205874 + - 0.9428774454610838 +- - - -0.3212533436841441 + - 0.9469932888736632 + - - -0.3212533436841441 + - 0.9469932888736632 +- - - -0.30931606138887024 + - 0.9509592915403249 + - - -0.30931606138887024 + - 0.9509592915403249 +- - - -0.2973298366671729 + - 0.9547748259288534 + - - -0.2973298366671729 + - 0.9547748259288534 +- - - -0.28529656607405124 + - 0.9584392883153082 + - - -0.28529656607405124 + - 0.9584392883153082 +- - - -0.2732181536084666 + - 0.9619520988795546 + - - -0.2732181536084666 + - 0.9619520988795546 +- - - -0.26109651041208987 + - 0.9653127017970029 + - - -0.26109651041208987 + - 0.9653127017970029 +- - - -0.24893355446689247 + - 0.9685205653265596 + - - -0.24893355446689247 + - 0.9685205653265596 +- - - -0.2367312102916815 + - 0.9715751818947599 + - - -0.2367312102916815 + - 0.9715751818947599 +- - - -0.22449140863757358 + - 0.974476068176083 + - - -0.22449140863757358 + - 0.974476068176083 +- - - -0.2122160861825098 + - 0.9772227651694252 + - - -0.2122160861825098 + - 0.9772227651694252 +- - - -0.19990718522480572 + - 0.9798148382707292 + - - -0.19990718522480572 + - 0.9798148382707292 +- - - -0.1875666533758392 + - 0.9822518773417477 + - - -0.1875666533758392 + - 0.9822518773417477 +- - - -0.17519644325187023 + - 0.9845334967749417 + - - -0.17519644325187023 + - 0.9845334967749417 +- - - -0.16279851216509478 + - 0.9866593355544919 + - - -0.16279851216509478 + - 0.9866593355544919 +- - - -0.1503748218139381 + - 0.9886290573134224 + - - -0.1503748218139381 + - 0.9886290573134224 +- - - -0.1379273379726542 + - 0.9904423503868245 + - - -0.1379273379726542 + - 0.9904423503868245 +- - - -0.12545803018029758 + - 0.9920989278611683 + - - -0.12545803018029758 + - 0.9920989278611683 +- - - -0.11296887142907358 + - 0.9935985276197029 + - - -0.11296887142907358 + - 0.9935985276197029 +- - - -0.10046183785216964 + - 0.9949409123839287 + - - -0.10046183785216964 + - 0.9949409123839287 +- - - -0.08793890841106214 + - 0.9961258697511428 + - - -0.08793890841106214 + - 0.9961258697511428 +- - - -0.07540206458240344 + - 0.9971532122280462 + - - -0.07540206458240344 + - 0.9971532122280462 +- - - -0.06285329004448297 + - 0.9980227772604111 + - - -0.06285329004448297 + - 0.9980227772604111 +- - - -0.05029457036336817 + - 0.9987344272588005 + - - -0.05029457036336817 + - 0.9987344272588005 +- - - -0.037727892678718344 + - 0.99928804962034 + - - -0.037727892678718344 + - 0.99928804962034 +- - - -0.025155245389377974 + - 0.9996835567465338 + - - -0.025155245389377974 + - 0.9996835567465338 +- - - -0.012578617838742366 + - 0.9999208860571255 + - - -0.012578617838742366 + - 0.9999208860571255 +- - - -4.898587196589413e-16 + - 1.0 + - - -4.898587196589413e-16 + - 1.0 +init_spikes: +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +n_neurons: 2 diff --git a/tests/test_glm.py b/tests/test_glm.py index 695ed537..df0f4815 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -283,6 +283,34 @@ def test_fit_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_in else: model.fit(X, y, init_params=(init_w, init_b)) + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + raise_exception = delta_tp != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + X = jnp.zeros((X.shape[0] + delta_tp, ) + X.shape[1:]) + if raise_exception: + with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): + raise_exception = delta_tp != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) + if raise_exception: + with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + ####################### # Test model.score ####################### @@ -380,6 +408,61 @@ def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_ else: model.score(X, y) + @pytest.mark.parametrize("is_fit", [True, False]) + def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): + raise_exception = not is_fit + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + if is_fit: + model.fit(X, y) + + if raise_exception: + with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): + model.score(X, y) + else: + model.score(X, y) + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + raise_exception = delta_tp != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) + if raise_exception: + with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + model.fit(X, y, init_params=(init_w, init_b)) + else: + model.fit(X, y, init_params=(init_w, init_b)) + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + raise_exception = delta_tp != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + + X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) + if raise_exception: + with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + model.score(X, y) + else: + model.score(X, y) + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): + raise_exception = delta_tp != 0 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + + y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) + if raise_exception: + with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + model.score(X, y) + else: + model.score(X, y) + def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff @@ -455,6 +538,19 @@ def test_predict_n_feature_consistency_x(self, delta_n_features, poissonGLM_mode else: model.predict(X) + @pytest.mark.parametrize("is_fit", [True, False]) + def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): + raise_exception = not is_fit + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + if is_fit: + model.fit(X, y) + + if raise_exception: + with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): + model.predict(X) + else: + model.predict(X) + ####################### # Test model.simulate ####################### @@ -486,7 +582,7 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_score_input_dimensionality(self, delta_dim, + def test_simulate_input_dimensionality(self, delta_dim, poissonGLM_coupled_model_config_simulate): raise_exception = delta_dim != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ @@ -515,7 +611,7 @@ def test_score_input_dimensionality(self, delta_dim, device="cpu") @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_score_y_dimensionality(self, delta_dim, + def test_simulate_y_dimensionality(self, delta_dim, poissonGLM_coupled_model_config_simulate): raise_exception = delta_dim != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ @@ -543,50 +639,198 @@ def test_score_y_dimensionality(self, delta_dim, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") - # - # @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) - # def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): - # raise_exception = delta_n_features != 0 - # X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - # # set model coeff - # model.basis_coeff_ = true_params[0] - # model.baseline_link_fr_ = true_params[1] - # if delta_n_features == 1: - # # add a feature - # X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) - # elif delta_n_features == -1: - # # remove a feature - # X = X[..., :-1] - # - # if raise_exception: - # with pytest.raises(ValueError, match="Inconsistent number of features"): - # model.score(X, y) - # else: - # model.score(X, y) - def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] - # get the rate - mean_firing = model.predict(X) - # compute the log-likelihood using jax.scipy - mean_ll_jax = jax.scipy.stats.poisson.logpmf(y, mean_firing).mean() - model_ll = model.score(X, y, score_type="log-likelihood") - if not np.allclose(mean_ll_jax, model_ll): - raise ValueError("Log-likelihood of PoissonModel does not match" - "that of jax.scipy!") + @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) + def test_simulate_n_neuron_match_y(self, delta_n_neuron, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_n_neuron != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + n_samples, n_neurons = feedforward_input.shape[:2] + + init_spikes = jnp.zeros((init_spikes.shape[0], n_neurons + delta_n_neuron)) + if raise_exception: + with pytest.raises(ValueError, match="The number of neuron in the model parameters"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("is_fit", [True, False]) + def test_simulate_is_fit(self, is_fit, + poissonGLM_coupled_model_config_simulate): + raise_exception = not is_fit + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + + if not is_fit: + model.baseline_link_fr_ = None + + if raise_exception: + with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_simulate_time_point_match_y(self, delta_tp, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_tp != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + + init_spikes = jnp.zeros((init_spikes.shape[0]+delta_tp, + init_spikes.shape[1])) + + if raise_exception: + with pytest.raises(ValueError, match="`init_spikes` and `coupling_basis_matrix`"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_simulate_time_point_match_coupling_basis(self, delta_tp, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_tp != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + + coupling_basis = jnp.zeros((coupling_basis.shape[0] + delta_tp,) + + coupling_basis.shape[1:]) + + if raise_exception: + with pytest.raises(ValueError, match="`init_spikes` and `coupling_basis_matrix`"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("delta_features", [-1, 0, 1]) + def test_simulate_feature_consistency_input(self, delta_features, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_features != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + feedforward_input = jnp.zeros((feedforward_input.shape[0], + feedforward_input.shape[1], + feedforward_input.shape[2]+delta_features)) + if raise_exception: + with pytest.raises(ValueError, match="The number of feed forward input features"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("delta_features", [-1, 0, 1]) + def test_simulate_feature_consistency_coupling_basis(self, delta_features, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_features != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + coupling_basis = jnp.zeros((coupling_basis.shape[0], + coupling_basis.shape[1] + delta_features)) + if raise_exception: + with pytest.raises(ValueError, match="The number of feed forward input features"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) + def test_simulate_input_timepoints(self, delta_tp, + poissonGLM_coupled_model_config_simulate): + raise_exception = delta_tp != 0 + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + n_timesteps = feedforward_input.shape[0] + feedforward_input = jnp.zeros((feedforward_input.shape[0] + delta_tp, + feedforward_input.shape[1], + feedforward_input.shape[2])) + if raise_exception: + with pytest.raises(ValueError, match="`feedforward_input` must be of length"): + model.simulate(random_key=random_key, + n_timesteps=n_timesteps, + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + else: + model.simulate(random_key=random_key, + n_timesteps=n_timesteps, + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + ####################################### + # Compare with standard implementation + ####################################### def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] + model.set_params(inverse_link_function=jnp.exp) # get the rate dev = sm.families.Poisson().deviance(y, firing_rate) dev_model = model._residual_deviance(firing_rate, y).sum() - if np.allclose(dev, dev_model): + if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiation): From f40b1e13a953994416eebc38b85801fc783b08c4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 16:23:03 -0400 Subject: [PATCH 017/250] linted and removed dandi test --- tests/test_dandi.py | 87 --------------------------------------------- tests/test_glm.py | 9 +++-- 2 files changed, 4 insertions(+), 92 deletions(-) delete mode 100644 tests/test_dandi.py diff --git a/tests/test_dandi.py b/tests/test_dandi.py deleted file mode 100644 index 1bbab8cb..00000000 --- a/tests/test_dandi.py +++ /dev/null @@ -1,87 +0,0 @@ -import pynwb - -from pynwb import NWBHDF5IO, TimeSeries - -from dandi.dandiapi import DandiAPIClient -import pynapple as nap -import numpy as np -import jax.numpy as jnp -import fsspec -from fsspec.implementations.cached import CachingFileSystem - -import pynwb -import h5py - -from matplotlib.pylab import * - -##################################### -# Dandi -##################################### - -# ecephys, Buzsaki Lab (15.2 GB) -dandiset_id, filepath = "000582", "sub-10073/sub-10073_ses-17010302_behavior+ecephys.nwb" - - -with DandiAPIClient() as client: - asset = client.get_dandiset(dandiset_id, "draft").get_asset_by_path(filepath) - s3_url = asset.get_content_url(follow_redirects=1, strip_query=True) - - - - -# first, create a virtual filesystem based on the http protocol -fs=fsspec.filesystem("http") - -# create a cache to save downloaded data to disk (optional) -fs = CachingFileSystem( - fs=fs, - cache_storage="nwb-cache", # Local folder for the cache -) - -# next, open the file -file = h5py.File(fs.open(s3_url, "rb")) -io = pynwb.NWBHDF5IO(file=file, load_namespaces=True) - - -##################################### -# Pynapple -##################################### - -nwb = nap.NWBFile(io.read()) - -units = nwb["units"] - -position = nwb["SpatialSeriesLED1"] - -tc, binsxy = nap.compute_2d_tuning_curves(units, position, 15) - - -figure() -for i in tc.keys(): - subplot(3,3,i+1) - imshow(tc[i]) -#show() - -figure() -for i in units.keys(): - subplot(3,3,i+1) - plot(position['x'], position['y']) - spk_pos = units[i].value_from(position) - plot(spk_pos["x"], spk_pos["y"], 'o', color = 'red', markersize = 1, alpha = 0.5) - -show() - - -##################################### -# GLM -##################################### -# create the binning -t0 = position.time_support.start[0] -tend = position.time_support.end[0] -ts = np.arange(t0-0.01, tend+0.01, 0.02) -binning = nap.IntervalSet(start=ts[:-1], end=ts[1:], time_units='s') - -# bin and convert to jax array -counts = jnp.asarray(units.count(ep=binning)) -position_binned = jnp.asarray(position.restrict(binning)) - diff --git a/tests/test_glm.py b/tests/test_glm.py index df0f4815..ad60d3c6 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,10 +1,10 @@ -import pytest - -from typing import Literal, Callable +from typing import Callable, Literal -import jax, jaxopt +import jax import jax.numpy as jnp +import jaxopt import numpy as np +import pytest import statsmodels.api as sm import neurostatslib as nsl @@ -849,4 +849,3 @@ def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiatio model.basis_coeff_.flatten())) if not np.allclose(fit_params_sm, fit_params_model): raise ValueError("Fitted parameters do not match that of statsmodels!") - From 07c9e56bdad41dc1063bc258294c222ebf8078e2 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 16:25:28 -0400 Subject: [PATCH 018/250] linted src --- src/neurostatslib/glm.py | 165 ++++++++++++++++++-------------- src/neurostatslib/model_base.py | 19 ++-- 2 files changed, 101 insertions(+), 83 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 47078282..32284021 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -4,10 +4,10 @@ import inspect from typing import Callable, Literal, Optional, Tuple, Union -import jax, jaxlib +import jax import jax.numpy as jnp import jaxopt -from numpy.typing import NDArray, ArrayLike +from numpy.typing import ArrayLike, NDArray from sklearn.exceptions import NotFittedError from .model_base import Model @@ -39,12 +39,12 @@ class PoissonGLMBase(Model, abc.ABC): """ def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - **kwargs, + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + **kwargs, ): self.solver_name = solver_name try: @@ -56,7 +56,9 @@ def __init__( undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) if undefined_kwargs: - raise NameError(f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!") + raise NameError( + f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" + ) if score_type not in ["log-likelihood", "pseudo-r2"]: raise NotImplementedError( @@ -78,7 +80,7 @@ def __init__( self.basis_coeff_ = None def _predict( - self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray + self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray ) -> jnp.ndarray: """ Predict firing rates given predictors and parameters. @@ -99,10 +101,10 @@ def _predict( return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) def _score( - self, - X: jnp.ndarray, - target_spikes: jnp.ndarray, - params: Tuple[jnp.ndarray, jnp.ndarray], + self, + X: jnp.ndarray, + target_spikes: jnp.ndarray, + params: Tuple[jnp.ndarray, jnp.ndarray], ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. @@ -144,7 +146,7 @@ def _score( """ # Avoid the edge-case of 0*log(0), much faster than # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) x = target_spikes * jnp.log(predicted_firing_rates) # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) @@ -207,11 +209,11 @@ def _pseudo_r2(self, params, X, y): """ mu = self._predict(params, X) res_dev_t = self._residual_deviance(mu, y) - resid_deviance = jnp.sum(res_dev_t ** 2) + resid_deviance = jnp.sum(res_dev_t**2) null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() null_dev_t = self._residual_deviance(null_mu, y) - null_deviance = jnp.sum(null_dev_t ** 2) + null_deviance = jnp.sum(null_dev_t**2) return (null_deviance - resid_deviance) / null_deviance @@ -223,9 +225,7 @@ def _check_is_fit(self): ) @staticmethod - def _check_and_convert_params( - params: ArrayLike - ) -> Tuple[jnp.ndarray, jnp.ndarray]: + def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Validate the dimensions and consistency of parameters and data. @@ -239,8 +239,10 @@ def _check_and_convert_params( try: params = tuple(jnp.asarray(par, dtype=jnp.float32) for par in params) except ValueError: - raise TypeError("Initial parameters must be array-like of array-like objects" - "with numeric data-type!") + raise TypeError( + "Initial parameters must be array-like of array-like objects" + "with numeric data-type!" + ) if len(params) != 2: raise ValueError("Params needs to be array-like of length two.") @@ -258,8 +260,9 @@ def _check_and_convert_params( return params @staticmethod - def _check_input_dimensionality(X: Optional[jnp.ndarray] = None, - spike_data: Optional[jnp.ndarray] = None): + def _check_input_dimensionality( + X: Optional[jnp.ndarray] = None, spike_data: Optional[jnp.ndarray] = None + ): if not (spike_data is None): if spike_data.ndim != 2: raise ValueError( @@ -272,9 +275,11 @@ def _check_input_dimensionality(X: Optional[jnp.ndarray] = None, ) @staticmethod - def _check_input_and_params_consistency(params: Tuple[jnp.ndarray, jnp.ndarray], - X: Optional[jnp.ndarray] = None, - spike_data: Optional[jnp.ndarray] = None): + def _check_input_and_params_consistency( + params: Tuple[jnp.ndarray, jnp.ndarray], + X: Optional[jnp.ndarray] = None, + spike_data: Optional[jnp.ndarray] = None, + ): """ Validate the number of neurons in model parameters and input arguments. @@ -320,11 +325,13 @@ def _check_input_and_params_consistency(params: Tuple[jnp.ndarray, jnp.ndarray], ) @staticmethod - def _check_input_n_timepoints(X: jnp.ndarray, spike_data:jnp.ndarray): + def _check_input_n_timepoints(X: jnp.ndarray, spike_data: jnp.ndarray): if X.shape[0] != spike_data.shape[0]: - raise ValueError("The number of time-points in X and spike_data must agree. " - f"X has {X.shape[0]} time-points, " - f"spike_data has {spike_data.shape[0]} instead!") + raise ValueError( + "The number of time-points in X and spike_data must agree. " + f"X has {X.shape[0]} time-points, " + f"spike_data has {spike_data.shape[0]} instead!" + ) def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: """Predict firing rates based on fit parameters. @@ -366,13 +373,15 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: self._check_input_dimensionality(X=X) # check consistency between X and params self._check_input_and_params_consistency((Ws, bs), X=X) - X, = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) + (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) return self._predict((Ws, bs), X) - def score(self, - X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], - score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None) -> jnp.ndarray: + def score( + self, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], + score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None, + ) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the @@ -457,16 +466,17 @@ def score(self, self._check_input_n_timepoints(X, spike_data) self._check_input_and_params_consistency((Ws, bs), X=X, spike_data=spike_data) - X, spike_data = self._convert_to_jnp_ndarray(X, spike_data, - data_type=jnp.float32) + X, spike_data = self._convert_to_jnp_ndarray( + X, spike_data, data_type=jnp.float32 + ) if score_type is None: score_type = self.score_type if score_type == "log-likelihood": score = -( - self._score(X, spike_data, (Ws, bs)) - + jax.scipy.special.gammaln(spike_data + 1).mean() + self._score(X, spike_data, (Ws, bs)) + + jax.scipy.special.gammaln(spike_data + 1).mean() ) elif score_type == "pseudo-r2": score = self._pseudo_r2((Ws, bs), X, spike_data) @@ -479,10 +489,10 @@ def score(self, @abc.abstractmethod def fit( - self, - X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, + self, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, ): """Fit GLM to spiking data. @@ -513,13 +523,13 @@ def fit( pass def simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_spikes: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray], - feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: str = "cpu", + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_spikes: Union[NDArray, jnp.ndarray], + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + device: str = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using the GLM as a recurrent network. @@ -597,16 +607,18 @@ def simulate( # add an empty input (simulate with coupling-only) if feedforward_input is None: - feedforward_input = jnp.zeros((n_timesteps, n_neurons, 0), dtype=jnp.float32) + feedforward_input = jnp.zeros( + (n_timesteps, n_neurons, 0), dtype=jnp.float32 + ) Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - + self._check_input_dimensionality(feedforward_input, init_spikes) if ( - feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] - != Ws.shape[1] + feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] + != Ws.shape[1] ): raise ValueError( "The number of feed forward input features" @@ -616,10 +628,12 @@ def simulate( f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " f"provided instead." ) - - self._check_input_and_params_consistency((Ws[:, n_basis_coupling*n_neurons:], bs), - X=feedforward_input, - spike_data=init_spikes) + + self._check_input_and_params_consistency( + (Ws[:, n_basis_coupling * n_neurons :], bs), + X=feedforward_input, + spike_data=init_spikes, + ) if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( @@ -630,13 +644,15 @@ def simulate( ) if feedforward_input.shape[0] != n_timesteps: - raise ValueError("`feedforward_input` must be of length `n_timesteps`. " - f"`feedforward_input` has length {len(feedforward_input)}, " - f"`n_timesteps` is {n_timesteps} instead!") + raise ValueError( + "`feedforward_input` must be of length `n_timesteps`. " + f"`feedforward_input` has length {len(feedforward_input)}, " + f"`n_timesteps` is {n_timesteps} instead!" + ) subkeys = jax.random.split(random_key, num=n_timesteps) def scan_fn( - data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray + data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray ) -> Tuple[Tuple[NDArray, int], NDArray]: """Function to scan over time steps and simulate spikes and firing rates. @@ -706,11 +722,11 @@ class PoissonGLM(PoissonGLMBase): """ def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + self, + solver_name: str = "GradientDescent", + solver_kwargs: dict = dict(), + inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", ): super().__init__( solver_name=solver_name, @@ -720,10 +736,10 @@ def __init__( ) def fit( - self, - X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + self, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): """Fit GLM to spiking data. @@ -760,8 +776,9 @@ def fit( self._check_input_n_timepoints(X, spike_data) # convert to jnp.ndarray of floats - X, spike_data = self._convert_to_jnp_ndarray(X, spike_data, - data_type=jnp.float32) + X, spike_data = self._convert_to_jnp_ndarray( + X, spike_data, data_type=jnp.float32 + ) _, n_neurons = spike_data.shape n_features = X.shape[2] diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py index 05829dd3..60a67f0b 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/model_base.py @@ -137,8 +137,7 @@ def _get_param_names(cls): return sorted([p.name for p in parameters]) @abc.abstractmethod - def fit(self, X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray]): + def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): pass @abc.abstractmethod @@ -146,12 +145,15 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass @abc.abstractmethod - def score(self, X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + def score( + self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray] + ) -> jnp.ndarray: pass @abc.abstractmethod - def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.ndarray: + def _predict( + self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray + ) -> jnp.ndarray: pass @abc.abstractmethod @@ -164,8 +166,7 @@ def _score( pass @staticmethod - def _convert_to_jnp_ndarray(*args: Union[NDArray, jnp.ndarray], - data_type: jnp.dtype = jnp.float32) \ - -> Tuple[jnp.ndarray, ...]: + def _convert_to_jnp_ndarray( + *args: Union[NDArray, jnp.ndarray], data_type: jnp.dtype = jnp.float32 + ) -> Tuple[jnp.ndarray, ...]: return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) - From bcd7cf942ab1dfb0e14c1d25d34eea250d647a2e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 16:36:06 -0400 Subject: [PATCH 019/250] improved description of simulate --- src/neurostatslib/glm.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 32284021..5a97eb59 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -535,9 +535,10 @@ def simulate( Simulate spike trains using the GLM as a recurrent network. This function projects spike trains into the future, employing the fitted - parameters of the GLM. While the default computation device is the CPU, - users can opt for GPU; however, it may not provide substantial speed-up due - to the inherently sequential nature of certain computations. + parameters of the GLM. It is capable of simulating spike trains based on a combination + of historical spike activity and external feedforward inputs like convolved currents, light + intensities, etc. + Parameters ---------- @@ -585,8 +586,11 @@ def simulate( Notes ----- - The sum of n_basis_input and n_basis_coupling should equal `self.basis_coeff_.shape[1]` to ensure + The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure consistency in the model's input feature dimensionality. + + The first `n_basis_coupling * n_neurons` in `self.basis_coeff_` are interpreted as the weights of + the coupling filters. """ if device == "cpu": target_device = jax.devices("cpu")[0] From 5fafb5d5d0d023beb4bd51da8e7a7c8c66659348 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 16:39:58 -0400 Subject: [PATCH 020/250] improved description of simulate --- src/neurostatslib/glm.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 5a97eb59..e8c7e865 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -553,8 +553,9 @@ def simulate( Basis matrix for coupling, representing inter-neuron effects and auto-correlations. Expected shape: (window_size, n_basis_coupling). feedforward_input : - External input matrix, representing factors like convolved currents, - light intensities, etc. Expected shape: (n_timesteps, n_neurons, n_basis_input). + External input matrix to the model, representing factors like convolved currents, + light intensities, etc. When not provided, the simulation is done with coupling-only. + Expected shape: (n_timesteps, n_neurons, n_basis_input). device : Computation device to use ('cpu' or 'gpu'). Default is 'cpu'. @@ -586,11 +587,13 @@ def simulate( Notes ----- - The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure - consistency in the model's input feature dimensionality. + The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients + (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. + The remaining coefficients correspond to the weights for the feed-forward input. - The first `n_basis_coupling * n_neurons` in `self.basis_coeff_` are interpreted as the weights of - the coupling filters. + + The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` + to ensure consistency in the model's input feature dimensionality. """ if device == "cpu": target_device = jax.devices("cpu")[0] From 28db00c9eca62025624b9ceadeb41fc6296ce73b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 23 Aug 2023 16:47:54 -0400 Subject: [PATCH 021/250] bugfixed config --- tests/basic_test.py | 13 ++++++------- tests/conftest.py | 15 +++++++++------ tests/test_basis.py | 1 - tests/test_convolution_1d.py | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tests/basic_test.py b/tests/basic_test.py index e08b85b1..a503f0ae 100644 --- a/tests/basic_test.py +++ b/tests/basic_test.py @@ -1,13 +1,12 @@ -from neurostatslib.glm import PoissonGLM -from sklearn.linear_model import PoissonRegressor -import numpy as np +import jax.numpy as jnp import matplotlib.pylab as plt -from scipy.optimize import minimize -from jax import grad +import numpy as np import scipy.stats as sts -import jax.numpy as jnp - +from jax import grad +from scipy.optimize import minimize +from sklearn.linear_model import PoissonRegressor +from neurostatslib.glm import PoissonGLM np.random.seed(100) diff --git a/tests/conftest.py b/tests/conftest.py index f2a2dc4b..6d1b3727 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,11 @@ -import pytest +import inspect +import os -import yaml import jax - -import numpy as np import jax.numpy as jnp +import numpy as np +import pytest +import yaml import neurostatslib as nsl @@ -22,8 +23,10 @@ def poissonGLM_model_instantiation(): @pytest.fixture def poissonGLM_coupled_model_config_simulate(): - - with open("simulate_coupled_neurons_params.yml", "r") as fh: + current_file = inspect.getfile(inspect.currentframe()) + test_dir = os.path.dirname(os.path.abspath(current_file)) + with open(os.path.join(test_dir, + "simulate_coupled_neurons_params.yml"), "r") as fh: config_dict = yaml.safe_load(fh) model = nsl.glm.PoissonGLM(inverse_link_function=jax.numpy.exp) diff --git a/tests/test_basis.py b/tests/test_basis.py index 52f14771..4d3d023a 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -7,7 +7,6 @@ import neurostatslib.basis as basis - # automatic define user accessible basis and check the methods diff --git a/tests/test_convolution_1d.py b/tests/test_convolution_1d.py index 18059c70..e0616e11 100644 --- a/tests/test_convolution_1d.py +++ b/tests/test_convolution_1d.py @@ -1,5 +1,5 @@ -import pytest import numpy as np +import pytest import neurostatslib.utils as utils From 405b17f237ad0963e3569699a9972c38f8f56549 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 24 Aug 2023 07:10:51 -0400 Subject: [PATCH 022/250] improved logic of fit, add check nans --- src/neurostatslib/glm.py | 95 +++++++++++++++++++++------------ src/neurostatslib/model_base.py | 15 ++++++ src/neurostatslib/utils.py | 15 ++++++ tests/test_glm.py | 70 +++++++++++++++++++++++- 4 files changed, 159 insertions(+), 36 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index e8c7e865..35b08e18 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -2,6 +2,7 @@ """ import abc import inspect +import warnings from typing import Callable, Literal, Optional, Tuple, Union import jax @@ -11,7 +12,7 @@ from sklearn.exceptions import NotFittedError from .model_base import Model -from .utils import convolve_1d_trials +from .utils import convolve_1d_trials, has_local_device class PoissonGLMBase(Model, abc.ABC): @@ -62,9 +63,8 @@ def __init__( if score_type not in ["log-likelihood", "pseudo-r2"]: raise NotImplementedError( - "Scoring method not implemented. " - f"score_type must be either 'log-likelihood', or 'pseudo-r2'." - f" {score_type} provided instead." + f"Scoring method {score_type} not implemented! " + f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." ) self.score_type = score_type self.solver_kwargs = solver_kwargs @@ -483,7 +483,8 @@ def score( else: # this should happen only if one manually set score_type raise NotImplementedError( - f"Scoring method {self.score_type} not implemented!" + f"Scoring method {score_type} not implemented! " + f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." ) return score @@ -492,7 +493,7 @@ def fit( self, X: Union[NDArray, jnp.ndarray], spike_data: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, + init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None ): """Fit GLM to spiking data. @@ -522,6 +523,48 @@ def fit( """ pass + def _preprocess_fit( + self, + X: Union[NDArray, jnp.ndarray], + spike_data: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None + ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: + + # check input dimensionality + self._check_input_dimensionality(X, spike_data) + self._check_input_n_timepoints(X, spike_data) + + # convert to jnp.ndarray of floats + X, spike_data = self._convert_to_jnp_ndarray( + X, spike_data, data_type=jnp.float32 + ) + + if self._has_invalid_entry(X): + raise ValueError("Input X contains a NaNs or Infs!") + elif self._has_invalid_entry(spike_data): + raise ValueError("Input spike_data contains a NaNs or Infs!") + + _, n_neurons = spike_data.shape + n_features = X.shape[2] + + # Initialize parameters + if init_params is None: + # Ws, spike basis coeffs + init_params = ( + jnp.zeros((n_neurons, n_features)), + # bs, bias terms + jnp.log(jnp.mean(spike_data, axis=0)), + ) + else: + # check parameter length, shape and dimensionality, convert to jnp.ndarray. + init_params = self._check_and_convert_params(init_params) + + # check that the inputs and the parameters has consistent sizes + self._check_input_and_params_consistency(init_params, X, spike_data) + + return X, spike_data, init_params + + def simulate( self, random_key: jax.random.PRNGKeyArray, @@ -529,7 +572,7 @@ def simulate( init_spikes: Union[NDArray, jnp.ndarray], coupling_basis_matrix: Union[NDArray, jnp.ndarray], feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: str = "cpu", + device: Literal["cpu", "gpu", "tpu"] = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using the GLM as a recurrent network. @@ -596,11 +639,16 @@ def simulate( to ensure consistency in the model's input feature dimensionality. """ if device == "cpu": - target_device = jax.devices("cpu")[0] - elif device == "gpu": - target_device = jax.devices("gpu")[0] + target_device = jax.devices(device)[0] + elif (device == "gpu") or (device == "tpu"): + if has_local_device(device): + # assume for now 1 gpu/tpu (no further parallelization) + target_device = jax.devices(device)[0] + else: + warnings.warn(f"No {device.upper()} found! Falling back to CPU") + target_device = jax.devices("cpu")[0] else: - raise ValueError(f"Invalid device: {device}. Choose 'cpu' or 'gpu'.") + raise ValueError(f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`.") # check if the model is fit self._check_is_fit() @@ -778,31 +826,8 @@ def fit( - If `init_params[i]` cannot be converted to jnp.ndarray for all i """ - # check input dimensionality - self._check_input_dimensionality(X, spike_data) - self._check_input_n_timepoints(X, spike_data) - - # convert to jnp.ndarray of floats - X, spike_data = self._convert_to_jnp_ndarray( - X, spike_data, data_type=jnp.float32 - ) - - _, n_neurons = spike_data.shape - n_features = X.shape[2] - # Initialize parameters - if init_params is None: - # Ws, spike basis coeffs - init_params = ( - jnp.zeros((n_neurons, n_features)), - # bs, bias terms - jnp.log(jnp.mean(spike_data, axis=0)), - ) - else: - # check parameter length, shape and dimensionality, convert to jnp.ndarray. - init_params = self._check_and_convert_params(init_params) - # check that the inputs and the parameters has consistent sizes - self._check_input_and_params_consistency(init_params, X, spike_data) + X, spike_data, init_params = self._preprocess_fit(X, spike_data, init_params) def loss(params, X, y): return self._score(X, y, params) diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py index 60a67f0b..9041e093 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/model_base.py @@ -170,3 +170,18 @@ def _convert_to_jnp_ndarray( *args: Union[NDArray, jnp.ndarray], data_type: jnp.dtype = jnp.float32 ) -> Tuple[jnp.ndarray, ...]: return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) + @staticmethod + def _has_invalid_entry(array: jnp.ndarray) -> bool: + """Check if the array has nans or infs. + + Parameters + ---------- + array: + The array to be checked. + + Returns + ------- + True if a nan or an inf is present, False otherwise + + """ + return (jnp.isinf(array) | jnp.isnan(array)).any() diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index ef87d04e..46c7c43a 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -383,3 +383,18 @@ def row_wise_kron(a, c): K = K.T return K + +def has_local_device(device_type: str) -> bool: + """ + Scan for local device availability. + + Parameters + ---------- + device_type: + The the device type in lower-case, e.g. `gpu`, `tpu`... + Returns + ------- + True if the jax finds the device, False otherwise. + + """ + return any(device_type in device.device_kind.lower() for device in jax.local_devices()) \ No newline at end of file diff --git a/tests/test_glm.py b/tests/test_glm.py index ad60d3c6..a7518a4a 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -52,7 +52,7 @@ def test_init_callable(self, func: Callable[[jnp.ndarray], jnp.ndarray]): @pytest.mark.parametrize("score_type", [1, "ll", "log-likelihood", "pseudo-r2"]) def test_init_score_type(self, score_type: Literal["log-likelihood", "pseudo-r2"]): if score_type not in ["log-likelihood", "pseudo-r2"]: - with pytest.raises(NotImplementedError, match="Scoring method not implemented."): + with pytest.raises(NotImplementedError, match=f"Scoring method {score_type} not implemented"): nsl.glm.PoissonGLM("BFGS", score_type=score_type) else: nsl.glm.PoissonGLM("BFGS", score_type=score_type) @@ -82,6 +82,25 @@ def test_fit_param_length(self, n_params, poissonGLM_model_instantiation): else: model.fit(X, y, init_params=init_params) + @pytest.mark.parametrize("add_entry", [0, np.nan, np.inf]) + @pytest.mark.parametrize("add_to", ["X", "y"]) + def test_fit_param_length(self, add_entry, add_to, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + if add_to == "X": + idx = np.unravel_index(np.random.choice(X.size), X.shape) + X[idx] = add_entry + elif add_to == "y": + idx = np.unravel_index(np.random.choice(y.size), y.shape) + y = np.asarray(y, dtype=np.float32) + y[idx] = add_entry + + raise_exception = jnp.isnan(add_entry) or jnp.isinf(add_entry) + if raise_exception: + with pytest.raises(ValueError, match="Input (X|spike_data) contains a NaNs or Infs"): + model.fit(X, y, init_params=true_params) + else: + model.fit(X, y, init_params=true_params) + @pytest.mark.parametrize("dim_weights", [0, 1, 2, 3]) def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -463,6 +482,19 @@ def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): else: model.score(X, y) + @pytest.mark.parametrize("score_type", ["pseudo-r2", "log-likelihood", "not-implemented"]) + def test_score_type_r2(self, score_type, poissonGLM_model_instantiation): + raise_exception = score_type not in ["pseudo-r2", "log-likelihood"] + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + model.basis_coeff_ = true_params[0] + model.baseline_link_fr_ = true_params[1] + + if raise_exception: + with pytest.raises(NotImplementedError, match=f"Scoring method {score_type} not implemented"): + model.score(X, y, score_type=score_type) + else: + model.score(X, y, score_type=score_type) + def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff @@ -818,6 +850,42 @@ def test_simulate_input_timepoints(self, delta_tp, feedforward_input=feedforward_input, device="cpu") + @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none"]) + def test_simulate_device_tspec(self, device_spec, + poissonGLM_coupled_model_config_simulate): + + raise_exception = not (device_spec in ["cpu", "tpu", "gpu"]) + print(device_spec, raise_exception) + raise_warning = all(device_spec != device.device_kind.lower() + for device in jax.local_devices()) + raise_warning = raise_warning and (not raise_exception) + + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + + if raise_exception: + with pytest.raises(ValueError, match=f"Invalid device specification: {device_spec}"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device=device_spec) + elif raise_warning: + with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device=device_spec) + else: + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_spikes=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device=device_spec) ####################################### # Compare with standard implementation ####################################### From ba07cffedf8d1b5a3f75b486816089f2b3c2f165 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 24 Aug 2023 08:34:03 -0400 Subject: [PATCH 023/250] added short docstrings to tests --- tests/conftest.py | 52 ++++++++++++ tests/test_glm.py | 196 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 233 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6d1b3727..ba0d16ad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,28 @@ +""" +Testing configurations for the `neurostatslib` library. + +This module contains test fixtures required to set up and verify the functionality +of the modules of the `neurostatslib` library. + +Dependencies: + - jax: Used for efficient numerical computing. + - jax.numpy: JAX's version of NumPy, used for matrix operations. + - numpy: Standard Python numerical computing library. + - pytest: Testing framework. + - yaml: For parsing and loading YAML configuration files. + +Functions: + - poissonGLM_model_instantiation: Sets up a Poisson GLM, instantiating its parameters + with random values and returning a set of test data and expected output. + + - poissonGLM_coupled_model_config_simulate: Reads from a YAML configuration file, + sets up a Poisson GLM with predefined parameters, and returns the initialized model + along with other related parameters. + +Note: + This module primarily serves as a utility for test configurations, setting up initial conditions, + and loading predefined parameters for testing various functionalities of the `neurostatslib` library. +""" import inspect import os @@ -12,6 +37,20 @@ @pytest.fixture def poissonGLM_model_instantiation(): + """Set up a Poisson GLM for testing purposes. + + This fixture initializes a Poisson GLM with random parameters, simulates its response, and + returns the test data, expected output, the model instance, true parameters, and the rate + of response. + + Returns: + tuple: A tuple containing: + - X (numpy.ndarray): Simulated input data. + - np.random.poisson(rate) (numpy.ndarray): Simulated spike responses. + - model (nsl.glm.PoissonGLM): Initialized model instance. + - (w_true, b_true) (tuple): True weight and bias parameters. + - rate (jax.numpy.ndarray): Simulated rate of response. + """ np.random.seed(123) X = np.random.normal(size=(100, 1, 5)) b_true = np.zeros((1, )) @@ -23,6 +62,19 @@ def poissonGLM_model_instantiation(): @pytest.fixture def poissonGLM_coupled_model_config_simulate(): + """Set up a Poisson GLM from a predefined configuration in a YAML file. + + This fixture reads parameters for a Poisson GLM from a YAML configuration file, initializes + the model accordingly, and returns the model instance with other related parameters. + + Returns: + tuple: A tuple containing: + - model (nsl.glm.PoissonGLM): Initialized model instance. + - coupling_basis (jax.numpy.ndarray): Coupling basis values from the config. + - feedforward_input (jax.numpy.ndarray): Feedforward input values from the config. + - init_spikes (jax.numpy.ndarray): Initial spike values from the config. + - jax.random.PRNGKey(123) (jax.random.PRNGKey): A pseudo-random number generator key. + """ current_file = inspect.getfile(inspect.currentframe()) test_dir = os.path.dirname(os.path.abspath(current_file)) with open(os.path.join(test_dir, diff --git a/tests/test_glm.py b/tests/test_glm.py index a7518a4a..d0d47a18 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -11,11 +11,18 @@ class TestPoissonGLM: + """ + Unit tests for the PoissonGLM class. + """ ####################### # Test model.__init__ ####################### @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize", "NotPresent"]) def test_init_solver_name(self, solver_name: str): + """ + Test initialization with different solver names. Check if an appropriate exception is raised + when the solver name is not present in jaxopt. + """ try: getattr(jaxopt, solver_name) raise_exception = False @@ -32,6 +39,10 @@ def test_init_solver_name(self, solver_name: str): {"tol": 1, "verbose": 1, "maxiter": 1}, {"tol": 1, "maxiter": 1}]) def test_init_solver_kwargs(self, solver_name, solver_kwargs): + """ + Test the initialization of solver with different solver keyword arguments. Check for invalid arguments + combination. + """ raise_exception = (solver_name == "ScipyMinimize") & ("verbose" in solver_kwargs) if raise_exception: with pytest.raises(NameError, match="kwargs {'[a-z]+'} in solver_kwargs not a kwarg"): @@ -43,6 +54,10 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): @pytest.mark.parametrize("func", [1, "string", lambda x: x, jnp.exp]) def test_init_callable(self, func: Callable[[jnp.ndarray], jnp.ndarray]): + """ + Test the initialization with different types of inverse_link_function. Check if a ValueError is raised + when the provided function is not callable. + """ if not callable(func): with pytest.raises(ValueError, match="inverse_link_function must be a callable"): nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) @@ -51,6 +66,10 @@ def test_init_callable(self, func: Callable[[jnp.ndarray], jnp.ndarray]): @pytest.mark.parametrize("score_type", [1, "ll", "log-likelihood", "pseudo-r2"]) def test_init_score_type(self, score_type: Literal["log-likelihood", "pseudo-r2"]): + """ + Test the initialization with different scoring methods. Check if a NotImplementedError is raised + for unsupported scoring methods. + """ if score_type not in ["log-likelihood", "pseudo-r2"]: with pytest.raises(NotImplementedError, match=f"Scoring method {score_type} not implemented"): nsl.glm.PoissonGLM("BFGS", score_type=score_type) @@ -62,6 +81,10 @@ def test_init_score_type(self, score_type: Literal["log-likelihood", "pseudo-r2" ####################### @pytest.mark.parametrize("n_params", [0, 1, 2, 3]) def test_fit_param_length(self, n_params, poissonGLM_model_instantiation): + """ + Test the `fit` method with different numbers of initial parameters. + Check for correct number of parameters. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape init_w = jnp.zeros((n_neurons, n_features)) @@ -84,7 +107,10 @@ def test_fit_param_length(self, n_params, poissonGLM_model_instantiation): @pytest.mark.parametrize("add_entry", [0, np.nan, np.inf]) @pytest.mark.parametrize("add_to", ["X", "y"]) - def test_fit_param_length(self, add_entry, add_to, poissonGLM_model_instantiation): + def test_fit_param_values(self, add_entry, add_to, poissonGLM_model_instantiation): + """ + Test the `fit` method with altered X or y values. Ensure the method raises exceptions for NaN or Inf values. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if add_to == "X": idx = np.unravel_index(np.random.choice(X.size), X.shape) @@ -103,6 +129,10 @@ def test_fit_param_length(self, add_entry, add_to, poissonGLM_model_instantiatio @pytest.mark.parametrize("dim_weights", [0, 1, 2, 3]) def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instantiation): + """ + Test the `fit` method with weight matrices of different dimensionalities. + Check for correct dimensionality. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape if dim_weights == 0: @@ -123,6 +153,9 @@ def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instanti @pytest.mark.parametrize("dim_intercepts", [0, 1, 2, 3]) def test_fit_intercepts_dimensionality(self, dim_intercepts, poissonGLM_model_instantiation): + """ + Test the `fit` method with intercepts of different dimensionalities. Check for correct dimensionality. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -145,6 +178,10 @@ def test_fit_intercepts_dimensionality(self, dim_intercepts, poissonGLM_model_in [jnp.zeros((1, 5)), ""], ["", jnp.zeros((1,))]]) def test_fit_init_params_type(self, init_params, poissonGLM_model_instantiation): + """ + Test the `fit` method with various types of initial parameters. Ensure that the provided initial parameters + are array-like. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # check if parameter can be converted try: @@ -162,6 +199,9 @@ def test_fit_init_params_type(self, init_params, poissonGLM_model_instantiation) @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_fit_n_neuron_match_weights(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `fit` method ensuring the number of neurons in the weights matches the expected number. + """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -183,6 +223,9 @@ def test_fit_n_neuron_match_weights(self, delta_n_neuron, poissonGLM_model_insta @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `fit` method ensuring the number of neurons in the baseline rate matches the expected number. + """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -198,6 +241,9 @@ def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, poissonGLM_model @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_fit_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `fit` method ensuring the number of neurons in X matches the number of neurons in the model. + """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -212,6 +258,9 @@ def test_fit_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiati @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_fit_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `fit` method ensuring the number of neurons in y matches the number of neurons in the model. + """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -226,6 +275,9 @@ def test_fit_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiati @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_fit_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + """ + Test the `fit` method with X input data of different dimensionalities. Ensure correct dimensionality for X. + """ raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -247,6 +299,9 @@ def test_fit_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_fit_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + """ + Test the `fit` method with y target data of different dimensionalities. Ensure correct dimensionality for y. + """ raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -268,6 +323,10 @@ def test_fit_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) def test_fit_n_feature_consistency_weights(self, delta_n_features, poissonGLM_model_instantiation): + """ + Test the `fit` method for inconsistencies between data features and initial weights provided. + Ensure the number of features align. + """ raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -283,6 +342,10 @@ def test_fit_n_feature_consistency_weights(self, delta_n_features, poissonGLM_mo @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) def test_fit_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + """ + Test the `fit` method for inconsistencies between data features and model's expectations. + Ensure the number of features in X aligns. + """ raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -304,6 +367,9 @@ def test_fit_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_in @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + """ + Test the `fit` method for inconsistencies in time-points in data X. Ensure the correct number of time-points. + """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -318,6 +384,9 @@ def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): + """ + Test the `fit` method for inconsistencies in time-points in spike_data. Ensure the correct number of time-points. + """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -335,6 +404,9 @@ def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): ####################### @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_score_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `score` method when the number of neurons in X differs. Ensure the correct number of neurons. + """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -350,6 +422,9 @@ def test_score_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantia @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_score_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `score` method when the number of neurons in spike_data differs. Ensure the correct number of neurons. + """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -365,6 +440,9 @@ def test_score_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantia @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_score_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + """ + Test the `score` method with X input data of different dimensionalities. Ensure correct dimensionality for X. + """ raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -387,6 +465,10 @@ def test_score_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation) @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + """ + Test the `score` method with spike_data of different dimensionalities. + Ensure correct dimensionality for spike_data. + """ raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, _ = X.shape @@ -409,6 +491,10 @@ def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation) @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + """ + Test the `score` method for inconsistencies in features of X. + Ensure the number of features in X aligns with the model params. + """ raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff @@ -429,6 +515,10 @@ def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_ @pytest.mark.parametrize("is_fit", [True, False]) def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): + """ + Test the `score` method on models based on their fit status. + Ensure scoring is only possible on fitted models. + """ raise_exception = not is_fit X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if is_fit: @@ -440,22 +530,13 @@ def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): else: model.score(X, y) - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): - raise_exception = delta_tp != 0 - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) - X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) - if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + """ + Test the `score` method for inconsistencies in time-points in X. + Ensure that the number of time-points in X and spike_data matches. + """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.basis_coeff_ = true_params[0] @@ -470,6 +551,10 @@ def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): + """ + Test the `score` method for inconsistencies in time-points in spike_data. + Ensure that the number of time-points in X and spike_data matches. + """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.basis_coeff_ = true_params[0] @@ -484,6 +569,10 @@ def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): @pytest.mark.parametrize("score_type", ["pseudo-r2", "log-likelihood", "not-implemented"]) def test_score_type_r2(self, score_type, poissonGLM_model_instantiation): + """ + Test the `score` method for unsupported scoring types. + Ensure only valid score types are used. + """ raise_exception = score_type not in ["pseudo-r2", "log-likelihood"] X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.basis_coeff_ = true_params[0] @@ -496,6 +585,10 @@ def test_score_type_r2(self, score_type, poissonGLM_model_instantiation): model.score(X, y, score_type=score_type) def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): + """ + Compare the model's log-likelihood computation against `jax.scipy`. + Ensure consistent and correct calculations. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff model.basis_coeff_ = true_params[0] @@ -515,6 +608,10 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) ####################### @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_predict_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + """ + Test the `predict` method when the number of neurons in X differs. + Ensure that the number of neurons in X, spike_data and params matches. + """ raise_exception = delta_n_neuron != 0 X, _, model, true_params, _ = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -530,6 +627,10 @@ def test_predict_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instant @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_predict_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + """ + Test the `predict` method with x input data of different dimensionalities. + Ensure correct dimensionality for x. + """ raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape @@ -552,6 +653,10 @@ def test_predict_x_dimensionality(self, delta_dim, poissonGLM_model_instantiatio @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) def test_predict_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + """ + Test the `predict` method ensuring the number of features in x input data + is consistent with the model's `model.`basis_coeff_`. + """ raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff @@ -572,6 +677,9 @@ def test_predict_n_feature_consistency_x(self, delta_n_features, poissonGLM_mode @pytest.mark.parametrize("is_fit", [True, False]) def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): + """ + Test if the model raises a ValueError when trying to score before it's fitted. + """ raise_exception = not is_fit X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if is_fit: @@ -589,6 +697,10 @@ def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_simulate_n_neuron_match_input(self, delta_n_neuron, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method to ensure that the number of neurons in the input + matches the model's parameters. + """ raise_exception = delta_n_neuron != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -616,6 +728,10 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_simulate_input_dimensionality(self, delta_dim, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method with input data of different dimensionalities. + Ensure correct dimensionality for input. + """ raise_exception = delta_dim != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -645,6 +761,10 @@ def test_simulate_input_dimensionality(self, delta_dim, @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_simulate_y_dimensionality(self, delta_dim, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method with init_spikes of different dimensionalities. + Ensure correct dimensionality for init_spikes. + """ raise_exception = delta_dim != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -675,6 +795,10 @@ def test_simulate_y_dimensionality(self, delta_dim, @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_simulate_n_neuron_match_y(self, delta_n_neuron, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method to ensure that the number of neurons in init_spikes + matches the model's parameters. + """ raise_exception = delta_n_neuron != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -700,6 +824,9 @@ def test_simulate_n_neuron_match_y(self, delta_n_neuron, @pytest.mark.parametrize("is_fit", [True, False]) def test_simulate_is_fit(self, is_fit, poissonGLM_coupled_model_config_simulate): + """ + Test if the model raises a ValueError when trying to simulate before it's fitted. + """ raise_exception = not is_fit model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -726,6 +853,10 @@ def test_simulate_is_fit(self, is_fit, @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_simulate_time_point_match_y(self, delta_tp, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method to ensure that the time points in init_spikes + are consistent with the coupling_basis window size (they must be equal). + """ raise_exception = delta_tp != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -752,6 +883,10 @@ def test_simulate_time_point_match_y(self, delta_tp, @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_simulate_time_point_match_coupling_basis(self, delta_tp, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method to ensure that the window size in coupling_basis + is consistent with the time-points in init_spikes (they must be equal). + """ raise_exception = delta_tp != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -778,6 +913,15 @@ def test_simulate_time_point_match_coupling_basis(self, delta_tp, @pytest.mark.parametrize("delta_features", [-1, 0, 1]) def test_simulate_feature_consistency_input(self, delta_features, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method ensuring the number of features in `feedforward_input` is + consistent with the model's expected number of features. + + Notes + ----- + The total feature number `model.basis_coeff_.shape[1]` must be equal to + `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` + """ raise_exception = delta_features != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -803,6 +947,15 @@ def test_simulate_feature_consistency_input(self, delta_features, @pytest.mark.parametrize("delta_features", [-1, 0, 1]) def test_simulate_feature_consistency_coupling_basis(self, delta_features, poissonGLM_coupled_model_config_simulate): + """ + Test the `simulate` method ensuring the number of features in `coupling_basis` is + consistent with the model's expected number of features. + + Notes + ----- + The total feature number `model.basis_coeff_.shape[1]` must be equal to + `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` + """ raise_exception = delta_features != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -827,6 +980,11 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_simulate_input_timepoints(self, delta_tp, poissonGLM_coupled_model_config_simulate): + """ + Test `simulate` with varying input timepoints. + Ensures that a mismatch between n_timesteps and the timepoints in + `feedforward_input` results in an exception. + """ raise_exception = delta_tp != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate @@ -853,7 +1011,11 @@ def test_simulate_input_timepoints(self, delta_tp, @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none"]) def test_simulate_device_tspec(self, device_spec, poissonGLM_coupled_model_config_simulate): - + """ + Test `simulate` across different device specifications. + Validates if unsupported or absent devices raise exception + or warning respectively. + """ raise_exception = not (device_spec in ["cpu", "tpu", "gpu"]) print(device_spec, raise_exception) raise_warning = all(device_spec != device.device_kind.lower() @@ -890,6 +1052,10 @@ def test_simulate_device_tspec(self, device_spec, # Compare with standard implementation ####################################### def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): + """ + Compare fitted parameters to statsmodels. + Assesses if the model estimates are close to statsmodels' results. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff model.basis_coeff_ = true_params[0] From 3f4cab2d1fa7c2615ed8cb5e6c17b5b41b57a706 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 24 Aug 2023 09:25:04 -0400 Subject: [PATCH 024/250] tested model_base --- src/neurostatslib/model_base.py | 18 +++++- tests/test_model_base.py | 104 ++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 tests/test_model_base.py diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/model_base.py index 9041e093..557f9b13 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/model_base.py @@ -17,6 +17,7 @@ class Model(abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps def __init__(self, **kwargs): + self._kwargs_keys = list(kwargs.keys()) for key in kwargs: setattr(self, key, kwargs[key]) @@ -38,6 +39,9 @@ def get_params(self, deep=True): deep_items = value.get_params().items() out.update((key + "__" + k, val) for k, val in deep_items) out[key] = value + # add kwargs + for key in self._kwargs_keys: + out[key] = getattr(self, key) return out def set_params(self, **params): @@ -62,7 +66,6 @@ def set_params(self, **params): # Simple optimization to gain speed (inspect is slow) return self valid_params = self.get_params(deep=True) - nested_params = defaultdict(dict) # grouped by prefix for key, value in params.items(): key, delim, sub_key = key.partition("__") @@ -133,8 +136,19 @@ def _get_param_names(cls): " %s with constructor %s doesn't " " follow this convention." % (cls, init_signature) ) + + # Consider the constructor parameters excluding 'self' + parameters = [ + p.name + for p in init_signature.parameters.values() + if p.name != "self" + ] + + # remove kwargs + if 'kwargs' in parameters: + parameters.remove('kwargs') # Extract and sort argument names excluding 'self' - return sorted([p.name for p in parameters]) + return sorted(parameters) @abc.abstractmethod def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): diff --git a/tests/test_model_base.py b/tests/test_model_base.py new file mode 100644 index 00000000..b6325560 --- /dev/null +++ b/tests/test_model_base.py @@ -0,0 +1,104 @@ +import pytest +from typing import Union, Tuple +import jax.numpy as jnp +from numpy.typing import NDArray +from neurostatslib.model_base import Model # Adjust this import to your module name + +# Sample subclass to test instantiation and methods +class MockModel(Model): + + def __init__(self, std_param: int = 0, **kwargs): + self.std_param = std_param + super().__init__(**kwargs) + + def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): + pass + + def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + pass + + def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + pass + + def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.ndarray: + pass + + def _score(self, X: jnp.ndarray, y: jnp.ndarray, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: + pass + +class MockModel_Invalid(Model): + """ + Mock model that doesn't implement `_score` abstract method + """ + + def __init__(self, std_param: int = 0, **kwargs): + self.std_param = std_param + super().__init__(**kwargs) + + def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): + pass + + def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + pass + + def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + pass + + def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.ndarray: + pass + + + +def test_init(): + model = MockModel(param1="test", param2=2) + assert model.param1 == "test" + assert model.param2 == 2 + assert model.std_param == 0 + +def test_get_params(): + model = MockModel(param1="test", param2=2) + params = model.get_params(deep=True) + assert params["param1"] == "test" + assert params["param2"] == 2 + assert params["std_param"] == 0 + +def set_params(): + model = MockModel(param1="init_param") + model.set_params(param1="changed") + model.set_params(std_param=1) + assert model.param1 == "changed" + assert model.std_param == 1 + +def test_invalid_set_params(): + model = MockModel() + with pytest.raises(ValueError): + model.set_params(invalid_param="invalid") + +def test_get_param_names(): + param_names = MockModel._get_param_names() + # As per your implementation, _get_param_names should capture the constructor arguments + assert "std_param" in param_names + +def test_convert_to_jnp_ndarray(): + data = [1, 2, 3] + jnp_data, = Model._convert_to_jnp_ndarray(data) + assert isinstance(jnp_data, jnp.ndarray) + assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) + +def test_has_invalid_entry(): + valid_data = jnp.array([1, 2, 3]) + invalid_data = jnp.array([1, 2, jnp.nan]) + assert not Model._has_invalid_entry(valid_data) + assert Model._has_invalid_entry(invalid_data) + +# To ensure abstract methods aren't callable +def test_abstract_class(): + with pytest.raises(TypeError, match="Can't instantiate abstract"): + Model() + +def test_invalid_concrete_class(): + with pytest.raises(TypeError, match="Can't instantiate abstract"): + model = MockModel_Invalid() + + + From f2ac53ef149dc0cb20d7052411a20060c91bcef1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 24 Aug 2023 21:14:31 -0400 Subject: [PATCH 025/250] started off the developers' notes? --- .gitignore | 3 ++ docs/developers_notes/glm.md | 61 ++++++++++++++++++++++++++++ docs/developers_notes/module_base.md | 41 +++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 docs/developers_notes/glm.md create mode 100644 docs/developers_notes/module_base.md diff --git a/.gitignore b/.gitignore index 5573d2b9..2579acdd 100644 --- a/.gitignore +++ b/.gitignore @@ -143,3 +143,6 @@ docs/generated/ # vscode .vscode/ + +# nwb cahce +nwb-cache/ diff --git a/docs/developers_notes/glm.md b/docs/developers_notes/glm.md new file mode 100644 index 00000000..c4f1dbc4 --- /dev/null +++ b/docs/developers_notes/glm.md @@ -0,0 +1,61 @@ +# The PoissonGLMBase Class + +## Introduction + +The `PoissonGLMBase` class serves as the foundation for implementing Poisson Generalized Linear Models (GLMs). These models are essential for analyzing neural data and other related time-series data. The class encapsulates various functionalities required for model definition, fitting, prediction, and scoring, all of which are crucial aspects of modeling neural activity. + +The core features of this class are centered around abstract methods that must be implemented by any concrete subclasses, ensuring a standardized interface for all types of Poisson GLM models. + +## The Class `PoissonGLMBase` + +### Initialization and Configuration + +The `PoissonGLMBase` class's constructor initializes various parameters and settings essential for the model's behavior. These include: + +- `solver_name`: The name of the optimization solver to be used. +- `solver_kwargs`: Additional keyword arguments for the chosen solver. +- `inverse_link_function`: The callable function for the inverse link transformation. +- `score_type`: The type of scoring method, either "log-likelihood" or "pseudo-r2". + +### Method `fit` + +The `fit` method is an abstract method that needs to be implemented by subclasses. It is used to train the Poisson GLM model using input data `X` and spike data. The method performs the model fitting process by optimizing the provided loss function. + +### Method `predict` + +The `predict` method takes input data `X` and predicts firing rates using the trained Poisson GLM model. It leverages the inverse link function to transform the model's internal parameters into meaningful predictions. + +### Method `score` + +The `score` method evaluates the performance of the Poisson GLM model. It computes a score based on the model's predictions and the true spike data. The score can be either the negative log-likelihood or a pseudo-R2 score, depending on the specified `score_type`. + +### Internal Methods + +The class defines several internal methods that aid in the implementation of its functionalities: + +- `_predict`: A specialized prediction method that calculates firing rates using model parameters and input data. +- `_score`: A specialized scoring method that computes a score based on predicted firing rates, true spike data, and model parameters. +- `_residual_deviance`: Calculates the residual deviance of the model's predictions. +- `_pseudo_r2`: Computes the pseudo-R2 score based on the model's predictions, true spike data, and model parameters. +- `_check_is_fit`: Ensures that the instance has been fitted before making predictions or scoring. +- `_check_and_convert_params`: Validates and converts initial parameters to the appropriate format. +- `_check_input_dimensionality`: Checks the dimensionality of input data and spike data to ensure consistency. +- `_check_input_and_params_consistency`: Validates the consistency between input data, spike data, and model parameters. +- `_check_input_n_timepoints`: Verifies that the number of time points in input data and spike data match. +- `_preprocess_fit`: Prepares input data, spike data, and initial parameters for the fitting process. + +### Method `simulate` + +The `simulate` method generates simulated spike data using the trained Poisson GLM model. It takes into account various parameters, including random keys, coupling basis matrix, and feedforward input. The simulated data can be generated for different devices, such as CPU, GPU, or TPU. + +## The Class `PoissonGLM` + +### Initialization + +The `PoissonGLM` class extends the `PoissonGLMBase` class and provides a concrete implementation. It inherits the constructor from its parent class and allows additional customization through the specified parameters. + +### Method `fit` + +The `fit` method is implemented in the `PoissonGLM` class to perform the model fitting process using the provided input data and spike data. It leverages optimization solvers and loss functions to update the model's internal parameters. + +This script defines a powerful framework for creating and training Poisson Generalized Linear Models, essential for analyzing and understanding neural activity patterns. diff --git a/docs/developers_notes/module_base.md b/docs/developers_notes/module_base.md new file mode 100644 index 00000000..68a24b39 --- /dev/null +++ b/docs/developers_notes/module_base.md @@ -0,0 +1,41 @@ +# The ModuleBase Module + +## Introduction + +The `module_base` module primarily defines the abstract class `Model`, laying the groundwork for model definitions that ensure compatibility with sci-kit learn pipelines. The functionalities include the retrieval and setting of parameters, prediction, fitting, and scoring, all essential to any modeling process. + + +The core functionality is centered around abstract methods that need to be implemented by any concrete subclasses, ensuring a consistent interface for all types of models. + +## The Class `module_base.Model` + +### The Public Method `get_params` + +`get_params` retrieves the parameters set during the initialization of the model instance. If deep inspection is chosen, the method also inspects nested objects' parameters. The return is a dictionary of parameters. + +### The Public Method `set_params` + +This method allows users to set or modify the parameters of an estimator. The method is designed to work with both simple estimators and nested objects, like pipelines. If an invalid parameter is passed, a `ValueError` will be raised. + +### Abstract Methods + +Any concrete subclass inheriting from `Model` must implement these abstract methods to be functional: + +1. `fit`: To fit the model using data `X` and labels `y`. +2. `predict`: Make predictions based on the model and data `X`. +3. `score`: Score the model's predictions on data `X` with true labels `y`. +4. `_predict`: A more specialized prediction method that takes model parameters and data `X`. +5. `_score`: A specialized scoring method that takes data `X`, true labels `y`, and model parameters. + +Additionally, the class has helper methods like `_get_param_names` to fetch parameter names and `_convert_to_jnp_ndarray` to convert data to JAX NumPy arrays. + +## Contributor Guidelines + +### Implementing Model Subclasses +When aiming to introduce a new model by inheriting the abstract `Model` class, ensure the following: + +- **Must** inherit the abstract superclass `Model`. +- **Must** define the abstract methods `fit`, `predict`, `score`, `_predict`, and `_score`. +- **Should not** overwrite the `get_params` and `set_params` methods inherited from `Model`. +- **May** utilize helper methods like `_get_param_names` for convenience. + From ff2a54957c7de07af8937aef5a2cdc19dbdd1d20 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 14:45:32 -0400 Subject: [PATCH 026/250] base_model restructured and modified note --- docs/developers_notes/base_class.md | 59 +++++++++++++++++++ docs/developers_notes/basis_module.md | 2 +- docs/developers_notes/glm.md | 15 ++++- docs/developers_notes/module_base.md | 41 ------------- .../{model_base.py => base_class.py} | 34 +++++------ src/neurostatslib/glm.py | 4 +- tests/test_model_base.py | 47 ++++++++------- 7 files changed, 115 insertions(+), 87 deletions(-) create mode 100644 docs/developers_notes/base_class.md delete mode 100644 docs/developers_notes/module_base.md rename src/neurostatslib/{model_base.py => base_class.py} (92%) diff --git a/docs/developers_notes/base_class.md b/docs/developers_notes/base_class.md new file mode 100644 index 00000000..ae17f006 --- /dev/null +++ b/docs/developers_notes/base_class.md @@ -0,0 +1,59 @@ +# The `base_class` Module + +## Introduction + +The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. Currently, the sole abstract class available is `BaseRegressor`. + +The `_Base` class is envisioned as the foundational component for any model type (e.g., regression, dimensionality reduction, clustering, etc.). In contrast, abstract classes derived from `_Base` define overarching model categories (e.g., `BaseRegressor` is building block for GLMs, GAMS, etc.). + +Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. + +!!! Example + The current package version includes a concrete class named `neurostatslib.glm.PoissonGLM`. This class inherits from `BaseRegressor`, since it falls under the "regression" category. As any `BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. + +## The Class `model_base._Base` + +The `_Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. + +For a detailed understanding, consult the [`scikit-learn` API Reference](https://scikit-learn.org/stable/modules/classes.html) and [`BaseEstimator`](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html). + +!!! Note + We've intentionally omitted the `get_metadata_routing` method. Given its current experimental status and its lack of relevance to the `GLM` class, this method was excluded. Should future needs arise around parameter routing, consider directly inheriting from `sklearn.BaseEstimator`. More information can be found [here](https://scikit-learn.org/stable/metadata_routing.html#metadata-routing). + +### The Public Method `get_params` + +The `get_params` method retrieves parameters set during model instance initialization. Opting for a deep inspection allows the method to assess nested object parameters, resulting in a comprehensive parameter dictionary. + +### The Public Method `set_params` + +The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. + +## The Abstract Class `model_base.BaseRegressor` + +`BaseRegressor` is an abstract class that inherits from `_Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. + +### Abstract Methods + +For subclasses derived from `BaseRegressor` to function correctly, they must implement the following: + +1. `fit`: Adapt the model using input data `X` and corresponding observations `y`. +2. `predict`: Provide predictions based on the trained model and input data `X`. +3. `score`: Gauge the accuracy of model predictions using input data `X` against the actual observations `y`. +4. `simulate`: Simulate data based on the trained regression model. + +Moreover, `BaseRegressor` incorporates auxiliary methods such as `_get_param_names`, `_convert_to_jnp_ndarray`, and `_has_invalid_entry`. + +!!! Tip + Deciding between concrete and abstract methods in a superclass can be nuanced. As a general guideline: any method that's expected in all subclasses and isn't subclass-specific should be concretely implemented in the superclass. Conversely, methods essential for a subclass's expected behavior, but vary based on the subclass, should be abstract in the superclass. For instance, compatibility with the `sklearn.cross_validation` module demands `score`, `fit`, `get_params`, and `set_params` methods. Given their specificity to individual models, `score` and `fit` are abstract in `BaseRegressor`. Conversely, as `get_params` and `set_params` are consistent across model classes, they're inherited from `_Base`. This approach typifies our general implementation strategy. However, it's important to note that while these are sound guidelines, exceptions exist based on various factors like future extensibility, clarity, and maintainability. + + +## Contributor Guidelines + +### Implementing Model Subclasses + +When devising a new model subclass based on the `BaseRegressor` abstract class, adhere to the subsequent guidelines: + +- **Must** inherit the `BaseRegressor` abstract superclass. +- **Must** realize the abstract methods: `fit`, `predict`, and `score`. +- **Should not** overwrite the `get_params` and `set_params` methods, inherited from `_Base`. +- **May** introduce auxiliary methods such as `_convert_to_jnp_ndarray` for added utility. diff --git a/docs/developers_notes/basis_module.md b/docs/developers_notes/basis_module.md index 7dc3dd6e..53ec2f55 100644 --- a/docs/developers_notes/basis_module.md +++ b/docs/developers_notes/basis_module.md @@ -1,4 +1,4 @@ -# The Basis Module +# The `basis` Module ## Introduction diff --git a/docs/developers_notes/glm.md b/docs/developers_notes/glm.md index c4f1dbc4..1ca1cecb 100644 --- a/docs/developers_notes/glm.md +++ b/docs/developers_notes/glm.md @@ -1,9 +1,20 @@ -# The PoissonGLMBase Class +# The `glm` Module ## Introduction -The `PoissonGLMBase` class serves as the foundation for implementing Poisson Generalized Linear Models (GLMs). These models are essential for analyzing neural data and other related time-series data. The class encapsulates various functionalities required for model definition, fitting, prediction, and scoring, all of which are crucial aspects of modeling neural activity. +The `neurostatslib.glm` basis module implements variations of Generalized Linear Models (GLMs) classes. +At stage, the module consists of two classes: + +1. The abstract class `PoissonGLMBase`. +2. The concrete class `PoissonGLM`. + +We followed the `scikit-learn` api, making the concrete GLM model classes compatible with the powerful `scikit-learn` pipeline and cross-validation modules. + +The `PoissonGLMBase` serves as the foundation for implementing Poisson Generalized Linear Models (GLMs). +It designed to follow the `scikit-learn` api in order to guarantee compatibility with `scikit-learn` pipelines. +It inherits `Model` (see the ["ModuleBase Module"](base_class.md)) and implements the public methods `predict`, `score` , `simulate`. +`predict` generates Poisson means based on the current parameter estimate, `score` evaluates the performance of the model, and `simulate` generates simulated spike trains taking into account recurrent connectivity and feedforward inputs. The core features of this class are centered around abstract methods that must be implemented by any concrete subclasses, ensuring a standardized interface for all types of Poisson GLM models. ## The Class `PoissonGLMBase` diff --git a/docs/developers_notes/module_base.md b/docs/developers_notes/module_base.md deleted file mode 100644 index 68a24b39..00000000 --- a/docs/developers_notes/module_base.md +++ /dev/null @@ -1,41 +0,0 @@ -# The ModuleBase Module - -## Introduction - -The `module_base` module primarily defines the abstract class `Model`, laying the groundwork for model definitions that ensure compatibility with sci-kit learn pipelines. The functionalities include the retrieval and setting of parameters, prediction, fitting, and scoring, all essential to any modeling process. - - -The core functionality is centered around abstract methods that need to be implemented by any concrete subclasses, ensuring a consistent interface for all types of models. - -## The Class `module_base.Model` - -### The Public Method `get_params` - -`get_params` retrieves the parameters set during the initialization of the model instance. If deep inspection is chosen, the method also inspects nested objects' parameters. The return is a dictionary of parameters. - -### The Public Method `set_params` - -This method allows users to set or modify the parameters of an estimator. The method is designed to work with both simple estimators and nested objects, like pipelines. If an invalid parameter is passed, a `ValueError` will be raised. - -### Abstract Methods - -Any concrete subclass inheriting from `Model` must implement these abstract methods to be functional: - -1. `fit`: To fit the model using data `X` and labels `y`. -2. `predict`: Make predictions based on the model and data `X`. -3. `score`: Score the model's predictions on data `X` with true labels `y`. -4. `_predict`: A more specialized prediction method that takes model parameters and data `X`. -5. `_score`: A specialized scoring method that takes data `X`, true labels `y`, and model parameters. - -Additionally, the class has helper methods like `_get_param_names` to fetch parameter names and `_convert_to_jnp_ndarray` to convert data to JAX NumPy arrays. - -## Contributor Guidelines - -### Implementing Model Subclasses -When aiming to introduce a new model by inheriting the abstract `Model` class, ensure the following: - -- **Must** inherit the abstract superclass `Model`. -- **Must** define the abstract methods `fit`, `predict`, `score`, `_predict`, and `_score`. -- **Should not** overwrite the `get_params` and `set_params` methods inherited from `Model`. -- **May** utilize helper methods like `_get_param_names` for convenience. - diff --git a/src/neurostatslib/model_base.py b/src/neurostatslib/base_class.py similarity index 92% rename from src/neurostatslib/model_base.py rename to src/neurostatslib/base_class.py index 557f9b13..4a14f86a 100644 --- a/src/neurostatslib/model_base.py +++ b/src/neurostatslib/base_class.py @@ -7,15 +7,13 @@ import inspect import warnings from collections import defaultdict -from typing import Tuple, Union +from typing import Tuple, Union, Optional, Literal +import jax import jax.numpy as jnp from numpy.typing import NDArray - -class Model(abc.ABC): - FLOAT_EPS = jnp.finfo(jnp.float32).eps - +class _Base(abc.ABC): def __init__(self, **kwargs): self._kwargs_keys = list(kwargs.keys()) for key in kwargs: @@ -150,6 +148,10 @@ def _get_param_names(cls): # Extract and sort argument names excluding 'self' return sorted(parameters) + +class BaseRegressor(_Base, abc.ABC): + FLOAT_EPS = jnp.finfo(jnp.float32).eps + @abc.abstractmethod def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): pass @@ -165,18 +167,15 @@ def score( pass @abc.abstractmethod - def _predict( - self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray - ) -> jnp.ndarray: - pass - - @abc.abstractmethod - def _score( - self, - X: jnp.ndarray, - y: jnp.ndarray, - params: Tuple[jnp.ndarray, jnp.ndarray], - ) -> jnp.ndarray: + def simulate( + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_spikes: Union[NDArray, jnp.ndarray], + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "cpu", + ): pass @staticmethod @@ -184,6 +183,7 @@ def _convert_to_jnp_ndarray( *args: Union[NDArray, jnp.ndarray], data_type: jnp.dtype = jnp.float32 ) -> Tuple[jnp.ndarray, ...]: return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) + @staticmethod def _has_invalid_entry(array: jnp.ndarray) -> bool: """Check if the array has nans or infs. diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 35b08e18..ffebbbdb 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -11,11 +11,11 @@ from numpy.typing import ArrayLike, NDArray from sklearn.exceptions import NotFittedError -from .model_base import Model +from .base_class import BaseRegressor from .utils import convolve_1d_trials, has_local_device -class PoissonGLMBase(Model, abc.ABC): +class PoissonGLMBase(BaseRegressor, abc.ABC): """Abstract base class for Poisson GLMs. Provides methods for score computation, simulation, and prediction. diff --git a/tests/test_model_base.py b/tests/test_model_base.py index b6325560..321a0668 100644 --- a/tests/test_model_base.py +++ b/tests/test_model_base.py @@ -2,10 +2,11 @@ from typing import Union, Tuple import jax.numpy as jnp from numpy.typing import NDArray -from neurostatslib.model_base import Model # Adjust this import to your module name +from neurostatslib.base_class import BaseRegressor # Adjust this import to your module name + # Sample subclass to test instantiation and methods -class MockModel(Model): +class MockBaseRegressor(BaseRegressor): def __init__(self, std_param: int = 0, **kwargs): self.std_param = std_param @@ -20,13 +21,18 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass - def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.ndarray: - pass - - def _score(self, X: jnp.ndarray, y: jnp.ndarray, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: + def simulate( + self, + random_key, + n_timesteps, + init_spikes, + coupling_basis_matrix, + feedforward_input=None, + device="cpu" + ) -> jnp.ndarray: pass -class MockModel_Invalid(Model): +class MockBaseRegressor_Invalid(BaseRegressor): """ Mock model that doesn't implement `_score` abstract method """ @@ -35,70 +41,63 @@ def __init__(self, std_param: int = 0, **kwargs): self.std_param = std_param super().__init__(**kwargs) - def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): - pass - def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass - def _predict(self, params: Tuple[jnp.ndarray, jnp.ndarray], X: NDArray) -> jnp.ndarray: - pass - - def test_init(): - model = MockModel(param1="test", param2=2) + model = MockBaseRegressor(param1="test", param2=2) assert model.param1 == "test" assert model.param2 == 2 assert model.std_param == 0 def test_get_params(): - model = MockModel(param1="test", param2=2) + model = MockBaseRegressor(param1="test", param2=2) params = model.get_params(deep=True) assert params["param1"] == "test" assert params["param2"] == 2 assert params["std_param"] == 0 def set_params(): - model = MockModel(param1="init_param") + model = MockBaseRegressor(param1="init_param") model.set_params(param1="changed") model.set_params(std_param=1) assert model.param1 == "changed" assert model.std_param == 1 def test_invalid_set_params(): - model = MockModel() + model = MockBaseRegressor() with pytest.raises(ValueError): model.set_params(invalid_param="invalid") def test_get_param_names(): - param_names = MockModel._get_param_names() + param_names = MockBaseRegressor._get_param_names() # As per your implementation, _get_param_names should capture the constructor arguments assert "std_param" in param_names def test_convert_to_jnp_ndarray(): data = [1, 2, 3] - jnp_data, = Model._convert_to_jnp_ndarray(data) + jnp_data, = BaseRegressor._convert_to_jnp_ndarray(data) assert isinstance(jnp_data, jnp.ndarray) assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) def test_has_invalid_entry(): valid_data = jnp.array([1, 2, 3]) invalid_data = jnp.array([1, 2, jnp.nan]) - assert not Model._has_invalid_entry(valid_data) - assert Model._has_invalid_entry(invalid_data) + assert not BaseRegressor._has_invalid_entry(valid_data) + assert BaseRegressor._has_invalid_entry(invalid_data) # To ensure abstract methods aren't callable def test_abstract_class(): with pytest.raises(TypeError, match="Can't instantiate abstract"): - Model() + BaseRegressor() def test_invalid_concrete_class(): with pytest.raises(TypeError, match="Can't instantiate abstract"): - model = MockModel_Invalid() + model = MockBaseRegressor_Invalid() From b9b5b85b81f8332278cc1878172fb94c80375984 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 14:49:48 -0400 Subject: [PATCH 027/250] added docstrings to model_base.py --- tests/test_model_base.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/test_model_base.py b/tests/test_model_base.py index 321a0668..ad5bbe1f 100644 --- a/tests/test_model_base.py +++ b/tests/test_model_base.py @@ -7,8 +7,12 @@ # Sample subclass to test instantiation and methods class MockBaseRegressor(BaseRegressor): - + """ + Mock implementation of the BaseRegressor abstract class for testing purposes. + Implements all required abstract methods as empty methods. + """ def __init__(self, std_param: int = 0, **kwargs): + """Initialize a MockBaseRegressor instance with optional standard parameters.""" self.std_param = std_param super().__init__(**kwargs) @@ -34,7 +38,8 @@ def simulate( class MockBaseRegressor_Invalid(BaseRegressor): """ - Mock model that doesn't implement `_score` abstract method + Mock model that intentionally doesn't implement all the required abstract methods. + Used for testing the instantiation of incomplete concrete classes. """ def __init__(self, std_param: int = 0, **kwargs): @@ -49,53 +54,69 @@ def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) def test_init(): + """Test the initialization of the MockBaseRegressor class.""" model = MockBaseRegressor(param1="test", param2=2) assert model.param1 == "test" assert model.param2 == 2 assert model.std_param == 0 + def test_get_params(): + """Test the get_params method.""" model = MockBaseRegressor(param1="test", param2=2) params = model.get_params(deep=True) assert params["param1"] == "test" assert params["param2"] == 2 assert params["std_param"] == 0 + def set_params(): + """Test the set_params method.""" model = MockBaseRegressor(param1="init_param") model.set_params(param1="changed") model.set_params(std_param=1) assert model.param1 == "changed" assert model.std_param == 1 + def test_invalid_set_params(): + """Test invalid parameter setting using the set_params method.""" model = MockBaseRegressor() with pytest.raises(ValueError): model.set_params(invalid_param="invalid") + def test_get_param_names(): + """Test retrieval of parameter names using the _get_param_names method.""" param_names = MockBaseRegressor._get_param_names() # As per your implementation, _get_param_names should capture the constructor arguments assert "std_param" in param_names + def test_convert_to_jnp_ndarray(): + """Test data conversion to JAX NumPy arrays.""" data = [1, 2, 3] jnp_data, = BaseRegressor._convert_to_jnp_ndarray(data) assert isinstance(jnp_data, jnp.ndarray) assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) + def test_has_invalid_entry(): + """Test validation of data arrays.""" valid_data = jnp.array([1, 2, 3]) invalid_data = jnp.array([1, 2, jnp.nan]) assert not BaseRegressor._has_invalid_entry(valid_data) assert BaseRegressor._has_invalid_entry(invalid_data) + # To ensure abstract methods aren't callable def test_abstract_class(): + """Ensure that abstract methods aren't callable.""" with pytest.raises(TypeError, match="Can't instantiate abstract"): BaseRegressor() def test_invalid_concrete_class(): + """Ensure that classes missing implementation of required abstract methods raise errors.""" with pytest.raises(TypeError, match="Can't instantiate abstract"): model = MockBaseRegressor_Invalid() From 096b3061eaf67b48b6b59e41d6015e17b755f8ed Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 18:30:08 -0400 Subject: [PATCH 028/250] massive refractoring, makes sure that score and simulate has a GLM-type specific docstrings --- pyproject.toml | 3 +- src/neurostatslib/__init__.py | 2 +- src/neurostatslib/base_class.py | 157 ++++- src/neurostatslib/exceptions.py | 20 + src/neurostatslib/glm.py | 643 +++++++++--------- ...{test_model_base.py => test_base_class.py} | 4 +- tests/test_glm.py | 90 +-- 7 files changed, 563 insertions(+), 356 deletions(-) create mode 100644 src/neurostatslib/exceptions.py rename tests/{test_model_base.py => test_base_class.py} (99%) diff --git a/pyproject.toml b/pyproject.toml index e91c7973..ed3bd0be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,8 @@ dev = [ "flake8", # Code linter "coverage", # Test coverage measurement "pytest-cov", # Test coverage plugin for pytest - "statsmodels" # Used to compare model pseudo-r2 in testing + "statsmodels", # Used to compare model pseudo-r2 in testing + "scikit-learn" # Testing compatibility with CV & pipelines ] docs = [ "mkdocs", # Documentation generator diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index 82a888b7..2779195c 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -from . import basis, glm, sample_points, utils +from . import basis, glm, sample_points, utils, exceptions diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 4a14f86a..feec6abb 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -7,11 +7,12 @@ import inspect import warnings from collections import defaultdict -from typing import Tuple, Union, Optional, Literal +from typing import Tuple, Union, Optional, Literal, Callable, Sequence import jax import jax.numpy as jnp -from numpy.typing import NDArray +from numpy.typing import NDArray, ArrayLike, DTypeLike + class _Base(abc.ABC): def __init__(self, **kwargs): @@ -174,7 +175,7 @@ def simulate( init_spikes: Union[NDArray, jnp.ndarray], coupling_basis_matrix: Union[NDArray, jnp.ndarray], feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu", + device: Literal["cpu", "gpu", "tpu"] = "cpu" ): pass @@ -199,3 +200,153 @@ def _has_invalid_entry(array: jnp.ndarray) -> bool: """ return (jnp.isinf(array) | jnp.isnan(array)).any() + + @staticmethod + def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, ...]: + """ + Validate the dimensions and consistency of parameters and data. + + This function checks the consistency of shapes and dimensions for model + parameters. + It ensures that the parameters and data are compatible for the model. + + """ + if not hasattr(params, "__getitem__"): + raise TypeError("Initial parameters must be array-like!") + try: + params = tuple(jnp.asarray(par, dtype=jnp.float32) for par in params) + except ValueError: + raise TypeError( + "Initial parameters must be array-like of array-like objects" + "with numeric data-type!" + ) + + if len(params) != 2: + raise ValueError("Params needs to be array-like of length two.") + + if params[0].ndim != 2: + raise ValueError( + "params[0] term must be of shape (n_neurons, n_features), but" + f"params[0] has {params[0].ndim} dimensions!" + ) + if params[1].ndim != 1: + raise ValueError( + "params[1] term must be of shape (n_neurons,) but " + f"params[1] has {params[1].ndim} dimensions!" + ) + return params + + @staticmethod + def _check_input_dimensionality( + X: Optional[jnp.ndarray] = None, y: Optional[jnp.ndarray] = None + ): + if not (y is None): + if y.ndim != 2: + raise ValueError( + "y must be two-dimensional, with shape (n_timebins, n_neurons)" + ) + if not (X is None): + if X.ndim != 3: + raise ValueError( + "X must be three-dimensional, with shape (n_timebins, n_neurons, n_features)" + ) + + @staticmethod + def _check_input_and_params_consistency( + params: Tuple[jnp.ndarray, jnp.ndarray], + X: Optional[jnp.ndarray] = None, + y: Optional[jnp.ndarray] = None, + ): + """ + Validate the number of neurons in model parameters and input arguments. + + Raises: + ------ + ValueError + - if the number of neurons is consistent across the model parameters (`params`) and + any additional inputs (`X` or `y` when provided). + - if the number of features is inconsistent between params[1] and X (when provided). + + """ + n_neurons = params[0].shape[0] + if n_neurons != params[1].shape[0]: + raise ValueError( + "Model parameters have inconsistent shapes. " + "Spike basis coefficients must be of shape (n_neurons, n_features), and " + "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both! " + f"Coefficients n_neurons: {params[0].shape[0]}, bias n_neurons: {params[1].shape[0]}" + ) + + if y is not None: + if y.shape[1] != n_neurons: + raise ValueError( + "The number of neuron in the model parameters and in the inputs" + "must match." + f"parameters has n_neurons: {n_neurons}, " + f"the input provided has n_neurons: {y.shape[1]}" + ) + + if X is not None: + if X.shape[1] != n_neurons: + raise ValueError( + "The number of neuron in the model parameters and in the inputs" + "must match." + f"parameters has n_neurons: {n_neurons}, " + f"the input provided has n_neurons: {X.shape[1]}" + ) + if params[0].shape[1] != X.shape[2]: + raise ValueError( + "Inconsistent number of features. " + f"spike basis coefficients has {params[0].shape[1]} features, " + f"X has {X.shape[2]} features instead!" + ) + + @staticmethod + def _check_input_n_timepoints(X: jnp.ndarray, y: jnp.ndarray): + if X.shape[0] != y.shape[0]: + raise ValueError( + "The number of time-points in X and y must agree. " + f"X has {X.shape[0]} time-points, " + f"y has {y.shape[0]} instead!" + ) + + def _preprocess_fit( + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None + ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: + + # check input dimensionality + self._check_input_dimensionality(X, y) + self._check_input_n_timepoints(X, y) + + # convert to jnp.ndarray of floats + X, y = self._convert_to_jnp_ndarray( + X, y, data_type=jnp.float32 + ) + + if self._has_invalid_entry(X): + raise ValueError("Input X contains a NaNs or Infs!") + elif self._has_invalid_entry(y): + raise ValueError("Input y contains a NaNs or Infs!") + + _, n_neurons = y.shape + n_features = X.shape[2] + + # Initialize parameters + if init_params is None: + # Ws, spike basis coeffs + init_params = ( + jnp.zeros((n_neurons, n_features)), + # bs, bias terms + jnp.log(jnp.mean(y, axis=0)), + ) + else: + # check parameter length, shape and dimensionality, convert to jnp.ndarray. + init_params = self._check_and_convert_params(init_params) + + # check that the inputs and the parameters has consistent sizes + self._check_input_and_params_consistency(init_params, X, y) + + return X, y, init_params diff --git a/src/neurostatslib/exceptions.py b/src/neurostatslib/exceptions.py new file mode 100644 index 00000000..30e14f82 --- /dev/null +++ b/src/neurostatslib/exceptions.py @@ -0,0 +1,20 @@ +class NotFittedError(ValueError, AttributeError): + """Exception class to raise if estimator is used before fitting. + + This class inherits from both ValueError and AttributeError to help with + exception handling and backward compatibility. + + Examples + -------- + >>> from sklearn.svm import LinearSVC + >>> from sklearn.exceptions import NotFittedError + >>> try: + ... PoissonGLM().predict([[1, 2], [2, 3], [3, 4]]) + ... except NotFittedError as e: + ... print(repr(e)) + NotFittedError("This LinearSVC instance is not fitted yet. Call 'fit' with + appropriate arguments before using this estimator."...) + + .. versionchanged:: 0.18 + Moved from sklearn.utils.validation. + """ diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index ffebbbdb..fbd76d06 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -9,13 +9,13 @@ import jax.numpy as jnp import jaxopt from numpy.typing import ArrayLike, NDArray -from sklearn.exceptions import NotFittedError from .base_class import BaseRegressor from .utils import convolve_1d_trials, has_local_device +from .exceptions import NotFittedError -class PoissonGLMBase(BaseRegressor, abc.ABC): +class GLMBase(BaseRegressor, abc.ABC): """Abstract base class for Poisson GLMs. Provides methods for score computation, simulation, and prediction. @@ -100,66 +100,16 @@ def _predict( Ws, bs = params return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) - def _score( - self, - X: jnp.ndarray, - target_spikes: jnp.ndarray, - params: Tuple[jnp.ndarray, jnp.ndarray], - ) -> jnp.ndarray: - """Score the predicted firing rates against target spike counts. - - This computes the Poisson negative log-likelihood up to a constant. - - Note that you can end up with infinities in here if there are zeros in - ``predicted_firing_rates``. We raise a warning in that case. - - Parameters - ---------- - X : - The exogenous variables. Shape (n_time_bins, n_neurons, n_features). - target_spikes : - The target spikes to compare against. Shape (n_time_bins, n_neurons). - params : - Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). - - Returns - ------- - jnp.ndarray - The Poisson negative log-likehood. Shape (1,). - - Notes - ----- - The Poisson probability mass function is: - - $$ - \frac{\lambda^k \exp(-\lambda)}{k!} - $$ - - But the $k!$ term is not a function of the parameters and can be disregarded - when computing the loss-function. Thus, the negative log of it is: - - $$ - -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] - &= -k\log(\lambda)-\lambda + \text{const} - $$ - - """ - # Avoid the edge-case of 0*log(0), much faster than - # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) - x = target_spikes * jnp.log(predicted_firing_rates) - # see above for derivation of this. - return jnp.mean(predicted_firing_rates - x) - + @abc.abstractmethod def _residual_deviance(self, predicted_rate, y): - r"""Compute the residual deviance for a Poisson model. + r"""Compute the residual deviance for a GLM model. Parameters ---------- predicted_rate: - The predicted firing rates. + The predicted rate of the GLM. y: - The spike counts. + The observations. Returns ------- @@ -172,21 +122,17 @@ def _residual_deviance(self, predicted_rate, y): $$ \begin{aligned} - D(y, \hat{y}) &= 2 \sum \left[ y \log\left(\frac{y}{\hat{y}}\right) - (y - \hat{y}) \right]\\\ - &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) + D(y, \hat{y}) &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) \end{aligned} $$ where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model log-likelihood. Lower values of deviance indicate a better fit. """ - # this takes care of 0s in the log - ratio = jnp.clip(y / predicted_rate, self.FLOAT_EPS, jnp.inf) - resid_dev = 2 * (y * jnp.log(ratio) - (y - predicted_rate)) - return resid_dev + pass def _pseudo_r2(self, params, X, y): - r"""Pseudo-R^2 calculation for a Poisson GLM. + r"""Pseudo-R^2 calculation for a GLM. The Pseudo-R^2 metric gives a sense of how well the model fits the data, relative to a null (or baseline) model. @@ -198,7 +144,7 @@ def _pseudo_r2(self, params, X, y): X: The predictors. y: - The spike counts. + The neural activity. Returns ------- @@ -224,116 +170,10 @@ def _check_is_fit(self): "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) - @staticmethod - def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, jnp.ndarray]: - """ - Validate the dimensions and consistency of parameters and data. - - This function checks the consistency of shapes and dimensions for model - parameters, input predictors (`X`), and spike counts (`spike_data`). - It ensures that the parameters and data are compatible for the model. - - """ - if not hasattr(params, "__getitem__"): - raise TypeError("Initial parameters must be array-like!") - try: - params = tuple(jnp.asarray(par, dtype=jnp.float32) for par in params) - except ValueError: - raise TypeError( - "Initial parameters must be array-like of array-like objects" - "with numeric data-type!" - ) - - if len(params) != 2: - raise ValueError("Params needs to be array-like of length two.") - - if params[0].ndim != 2: - raise ValueError( - "params[0] term must be of shape (n_neurons, n_features), but" - f"params[0] has {params[0].ndim} dimensions!" - ) - if params[1].ndim != 1: - raise ValueError( - "params[1] term must be of shape (n_neurons,) but " - f"params[1] has {params[1].ndim} dimensions!" - ) - return params - - @staticmethod - def _check_input_dimensionality( - X: Optional[jnp.ndarray] = None, spike_data: Optional[jnp.ndarray] = None - ): - if not (spike_data is None): - if spike_data.ndim != 2: - raise ValueError( - "spike_data must be two-dimensional, with shape (n_timebins, n_neurons)" - ) - if not (X is None): - if X.ndim != 3: - raise ValueError( - "X must be three-dimensional, with shape (n_timebins, n_neurons, n_features)" - ) - - @staticmethod - def _check_input_and_params_consistency( - params: Tuple[jnp.ndarray, jnp.ndarray], - X: Optional[jnp.ndarray] = None, - spike_data: Optional[jnp.ndarray] = None, - ): - """ - Validate the number of neurons in model parameters and input arguments. - - Raises: - ------ - ValueError - - if the number of neurons is consistent across the model parameters (`params`) and - any additional inputs (`X` or `spike_data` when provided). - - if the number of features is inconsistent between params[1] and X (when provided). - - """ - n_neurons = params[0].shape[0] - if n_neurons != params[1].shape[0]: - raise ValueError( - "Model parameters have inconsistent shapes. " - "Spike basis coefficients must be of shape (n_neurons, n_features), and " - "bias terms must be of shape (n_neurons,) but n_neurons doesn't look the same in both! " - f"Coefficients n_neurons: {params[0].shape[0]}, bias n_neurons: {params[1].shape[0]}" - ) - - if spike_data is not None: - if spike_data.shape[1] != n_neurons: - raise ValueError( - "The number of neuron in the model parameters and in the inputs" - "must match." - f"parameters has n_neurons: {n_neurons}, " - f"the input provided has n_neurons: {spike_data.shape[1]}" - ) - - if X is not None: - if X.shape[1] != n_neurons: - raise ValueError( - "The number of neuron in the model parameters and in the inputs" - "must match." - f"parameters has n_neurons: {n_neurons}, " - f"the input provided has n_neurons: {X.shape[1]}" - ) - if params[0].shape[1] != X.shape[2]: - raise ValueError( - "Inconsistent number of features. " - f"spike basis coefficients has {params[0].shape[1]} features, " - f"X has {X.shape[2]} features instead!" - ) - - @staticmethod - def _check_input_n_timepoints(X: jnp.ndarray, spike_data: jnp.ndarray): - if X.shape[0] != spike_data.shape[0]: - raise ValueError( - "The number of time-points in X and spike_data must agree. " - f"X has {X.shape[0]} time-points, " - f"spike_data has {spike_data.shape[0]} instead!" - ) - - def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + def predict( + self, + X: Union[NDArray, jnp.ndarray] + ) -> jnp.ndarray: """Predict firing rates based on fit parameters. Parameters @@ -361,8 +201,6 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: -------- score Score predicted firing rates against target spike counts. - simulate - Simulate spikes using GLM as a recurrent network, for extrapolating into the future. """ # check that the model is fitted self._check_is_fit() @@ -376,30 +214,18 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) return self._predict((Ws, bs), X) - def score( + def _safe_score( self, X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], - score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None, + y: Union[NDArray, jnp.ndarray], + score_func: Callable[[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]],jnp.ndarray], + score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None ) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. - This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the + This computes the GLM mean log-likelihood or the pseudo-$R^2$, thus the higher the number the better. - The formula for the mean log-likelihood is the following, - - $$ - \begin{aligned} - \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} - [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ - &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] - \end{aligned} - $$ - - Because $\Gamma(k+1)=k!$, see - https://en.wikipedia.org/wiki/Gamma_function. - The pseudo-$R^2$ can be computed as follows, $$ @@ -410,9 +236,9 @@ def score( \end{aligned} $$ - where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for - the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate - of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. See [1]. + where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is + the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model + predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate. See [1]. Parameters ---------- @@ -437,7 +263,7 @@ def score( If ``fit`` has not been called first with this instance. ValueError If attempting to simulate a different number of neurons than were - present during fitting (i.e., if ``init_spikes.shape[0] != + present during fitting (i.e., if ``init_y.shape[0] != self.baseline_link_fr_.shape[0]``). Notes @@ -450,6 +276,8 @@ def score( of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. + Refer to the concrete subclass docstrings `_score` for the specific likelihood equations. + References ---------- [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. @@ -462,12 +290,12 @@ def score( self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self._check_input_dimensionality(X, spike_data) - self._check_input_n_timepoints(X, spike_data) - self._check_input_and_params_consistency((Ws, bs), X=X, spike_data=spike_data) + self._check_input_dimensionality(X, y) + self._check_input_n_timepoints(X, y) + self._check_input_and_params_consistency((Ws, bs), X=X, y=y) - X, spike_data = self._convert_to_jnp_ndarray( - X, spike_data, data_type=jnp.float32 + X, y = self._convert_to_jnp_ndarray( + X, y, data_type=jnp.float32 ) if score_type is None: @@ -475,11 +303,10 @@ def score( if score_type == "log-likelihood": score = -( - self._score(X, spike_data, (Ws, bs)) - + jax.scipy.special.gammaln(spike_data + 1).mean() + score_func(X, y, (Ws, bs)) ) elif score_type == "pseudo-r2": - score = self._pseudo_r2((Ws, bs), X, spike_data) + score = self._pseudo_r2((Ws, bs), X, y) else: # this should happen only if one manually set score_type raise NotImplementedError( @@ -488,113 +315,40 @@ def score( ) return score - @abc.abstractmethod - def fit( - self, - X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None - ): - """Fit GLM to spiking data. - - Following scikit-learn API, the solutions are stored as attributes - ``basis_coeff_`` and ``baseline_link_fr``. - - Parameters - ---------- - X : - Predictors, shape (n_time_bins, n_neurons, n_features) - spike_data : - Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). - init_params : - Initial values for the spike basis coefficients and bias terms. If - None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) - - Raises - ------ - ValueError - If spike_data is not two-dimensional. - ValueError - If shapes of init_params are not correct. - ValueError - If solver returns at least one NaN parameter, which means it found - an invalid solution. Try tuning optimization hyperparameters. - - """ - pass - - def _preprocess_fit( - self, - X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None - ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: - - # check input dimensionality - self._check_input_dimensionality(X, spike_data) - self._check_input_n_timepoints(X, spike_data) - - # convert to jnp.ndarray of floats - X, spike_data = self._convert_to_jnp_ndarray( - X, spike_data, data_type=jnp.float32 - ) - - if self._has_invalid_entry(X): - raise ValueError("Input X contains a NaNs or Infs!") - elif self._has_invalid_entry(spike_data): - raise ValueError("Input spike_data contains a NaNs or Infs!") - - _, n_neurons = spike_data.shape - n_features = X.shape[2] - - # Initialize parameters - if init_params is None: - # Ws, spike basis coeffs - init_params = ( - jnp.zeros((n_neurons, n_features)), - # bs, bias terms - jnp.log(jnp.mean(spike_data, axis=0)), - ) - else: - # check parameter length, shape and dimensionality, convert to jnp.ndarray. - init_params = self._check_and_convert_params(init_params) - - # check that the inputs and the parameters has consistent sizes - self._check_input_and_params_consistency(init_params, X, spike_data) - - return X, spike_data, init_params - - - def simulate( + def _safe_simulate( self, random_key: jax.random.PRNGKeyArray, n_timesteps: int, - init_spikes: Union[NDArray, jnp.ndarray], + init_y: Union[NDArray, jnp.ndarray], coupling_basis_matrix: Union[NDArray, jnp.ndarray], + random_function: Callable[ + [jax.random.PRNGKeyArray, ArrayLike], jnp.ndarray], feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu", + device: Literal["cpu", "gpu", "tpu"] = "cpu" ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using the GLM as a recurrent network. - This function projects spike trains into the future, employing the fitted - parameters of the GLM. It is capable of simulating spike trains based on a combination + This function projects neural activity into the future, employing the fitted + parameters of the GLM. It is capable of simulating activity based on a combination of historical spike activity and external feedforward inputs like convolved currents, light intensities, etc. - Parameters ---------- random_key : PRNGKey for seeding the simulation. n_timesteps : Duration of the simulation in terms of time steps. - init_spikes : - Initial spike counts matrix that kickstarts the simulation. + init_y : + Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). coupling_basis_matrix : Basis matrix for coupling, representing inter-neuron effects and auto-correlations. Expected shape: (window_size, n_basis_coupling). + random_function : + A random function, like jax.random.poisson, which takes as input a random.PRNGKeyArray + and the mean rate, and samples observations, (spike counts for a poisson).. feedforward_input : External input matrix to the model, representing factors like convolved currents, light intensities, etc. When not provided, the simulation is done with coupling-only. @@ -604,8 +358,8 @@ def simulate( Returns ------- - simulated_spikes : - Simulated spike counts for each neuron over time. + simulated_obs : + Simulated observations (spike counts for PoissonGLMs) for each neuron over time. Shape: (n_neurons, n_timesteps). firing_rates : Simulated firing rates for each neuron over time. @@ -653,7 +407,7 @@ def simulate( self._check_is_fit() # Transfer data to the target device - init_spikes = jax.device_put(init_spikes, target_device) + init_y = jax.device_put(init_y, target_device) coupling_basis_matrix = jax.device_put(coupling_basis_matrix, target_device) feedforward_input = jax.device_put(feedforward_input, target_device) @@ -669,7 +423,7 @@ def simulate( Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - self._check_input_dimensionality(feedforward_input, init_spikes) + self._check_input_dimensionality(feedforward_input, init_y) if ( feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] @@ -687,14 +441,14 @@ def simulate( self._check_input_and_params_consistency( (Ws[:, n_basis_coupling * n_neurons :], bs), X=feedforward_input, - spike_data=init_spikes, + y=init_y, ) - if init_spikes.shape[0] != coupling_basis_matrix.shape[0]: + if init_y.shape[0] != coupling_basis_matrix.shape[0]: raise ValueError( - "`init_spikes` and `coupling_basis_matrix`" + "`init_y` and `coupling_basis_matrix`" " should have the same window size! " - f"`init_spikes` window size: {init_spikes.shape[1]}, " + f"`init_y` window size: {init_y.shape[1]}, " f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" ) @@ -707,8 +461,9 @@ def simulate( subkeys = jax.random.split(random_key, num=n_timesteps) def scan_fn( - data: Tuple[NDArray, int], key: jax.random.PRNGKeyArray - ) -> Tuple[Tuple[NDArray, int], NDArray]: + data: Tuple[jnp.ndarray, int], + key: jax.random.PRNGKeyArray + ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: """Function to scan over time steps and simulate spikes and firing rates. This function simulates the spikes and firing rates for each time step @@ -736,18 +491,18 @@ def scan_fn( firing_rate = self._predict((Ws, bs), X) # Simulate spikes based on the predicted firing rate - new_spikes = jax.random.poisson(key, firing_rate) + new_spikes = random_function(key, firing_rate) # Prepare the spikes for the next iteration (keeping the most recent spikes) concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 return concat_spikes, (new_spikes, firing_rate) - _, outputs = jax.lax.scan(scan_fn, (init_spikes, 0), subkeys) + _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) simulated_spikes, firing_rates = outputs return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) -class PoissonGLM(PoissonGLMBase): +class PoissonGLM(GLMBase): """Un-regularized Poisson-GLM. The class fits the un-penalized maximum likelihood Poisson GLM parameter estimate. @@ -790,10 +545,199 @@ def __init__( score_type=score_type, ) + def _score( + self, + X: jnp.ndarray, + target_spikes: jnp.ndarray, + params: Tuple[jnp.ndarray, jnp.ndarray] + ) -> jnp.ndarray: + """Score the predicted firing rates against target spike counts. + + This computes the Poisson negative log-likelihood up to a constant. + + Note that you can end up with infinities in here if there are zeros in + ``predicted_firing_rates``. We raise a warning in that case. + + The formula for the Poisson mean log-likelihood is the following, + + $$ + \begin{aligned} + \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} + [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ + &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] + \end{aligned} + $$ + + Because $\Gamma(k+1)=k!$, see + https://en.wikipedia.org/wiki/Gamma_function. + + Parameters + ---------- + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features). + target_spikes : + The target spikes to compare against. Shape (n_time_bins, n_neurons). + params : + Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). + + Returns + ------- + jnp.ndarray + The Poisson negative log-likehood. Shape (1,). + + Notes + ----- + The Poisson probability mass function is: + + $$ + \frac{\lambda^k \exp(-\lambda)}{k!} + $$ + + But the $k!$ term is not a function of the parameters and can be disregarded + when computing the loss-function. Thus, the negative log of it is: + + $$ + -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] + &= -k\log(\lambda)-\lambda + \text{const} + $$ + + """ + # Avoid the edge-case of 0*log(0), much faster than + # where on large arrays. + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) + x = target_spikes * jnp.log(predicted_firing_rates) + # see above for derivation of this. + return jnp.mean(predicted_firing_rates - x) + + def _residual_deviance( + self, + predicted_rate: jnp.ndarray, + spike_counts: jnp.ndarray + ) -> jnp.ndarray: + r"""Compute the residual deviance for a Poisson model. + + Parameters + ---------- + predicted_rate: + The predicted firing rates. + spike_counts: + The spike counts. + + Returns + ------- + The residual deviance of the model. + + Notes + ----- + Deviance is a measure of the goodness of fit of a statistical model. + For a Poisson model, the residual deviance is computed as: + + $$ + \begin{aligned} + D(y, \hat{y}) &= 2 \sum \left[ y \log\left(\frac{y}{\hat{y}}\right) - (y - \hat{y}) \right]\\\ + &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) + \end{aligned} + $$ + where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model + log-likelihood. Lower values of deviance indicate a better fit. + + """ + # this takes care of 0s in the log + ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) + resid_dev = 2 * (spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate)) + return resid_dev + + def score( + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood" + ) -> jnp.ndarray: + r"""Score the predicted firing rates (based on fit) to the target spike counts. + + This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the + number the better. + + The formula for the mean log-likelihood is the following, + + $$ + \begin{aligned} + \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} + [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ + &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] + \end{aligned} + $$ + + Because $\Gamma(k+1)=k!$, see + https://en.wikipedia.org/wiki/Gamma_function. + + The pseudo-$R^2$ can be computed as follows, + + $$ + \begin{aligned} + R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ + &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) + - \log \text{LL}(\bar{\lambda}| y)}, + \end{aligned} + $$ + + where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for + the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate + of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. See [1]. + + Parameters + ---------- + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features) + y : + Spike counts arranged in a matrix. n_neurons must be the same as + during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). + score_type: + String indicating the type of scoring to return. Options are: + - `log-likelihood` for the model log-likelihood. + - `pseudo-r2` for the model pseudo-$R^2$. + Default is defined at class initialization. + Returns + ------- + score : (1,) + The Poisson log-likelihood or the pseudo-$R^2$ of the current model. + + Raises + ------ + NotFittedError + If ``fit`` has not been called first with this instance. + ValueError + If attempting to simulate a different number of neurons than were + present during fitting (i.e., if ``init_y.shape[0] != + self.baseline_link_fr_.shape[0]``). + + Notes + ----- + The log-likelihood is not on a standard scale, its value is influenced by many factors, + among which the number of model parameters. The log-likelihood can assume both positive + and negative values. + + The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure + of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. + The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. + + References + ---------- + [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. + Routledge, 2013. + + """ + norm_constant = jax.scipy.special.gammaln(y + 1).mean() + return super()._safe_score(X=X, + y=y, + score_type=score_type, + score_func=self._score + ) - norm_constant + def fit( self, X: Union[NDArray, jnp.ndarray], - spike_data: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): """Fit GLM to spiking data. @@ -805,7 +749,7 @@ def fit( ---------- X : Predictors, shape (n_time_bins, n_neurons, n_features) - spike_data : + y : Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). init_params : Initial values for the spike basis coefficients and bias terms. If @@ -827,14 +771,14 @@ def fit( """ - X, spike_data, init_params = self._preprocess_fit(X, spike_data, init_params) + X, y, init_params = self._preprocess_fit(X, y, init_params) def loss(params, X, y): return self._score(X, y, params) # Run optimization solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) - params, state = solver.run(init_params, X=X, y=spike_data) + params, state = solver.run(init_params, X=X, y=y) if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): raise ValueError( @@ -849,3 +793,88 @@ def loss(params, X, y): # solver.l2_optimality_error self.solver_state = state self.solver = solver + + def simulate( + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_y: Union[NDArray, jnp.ndarray], + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "cpu" + ) -> Tuple[jnp.ndarray, jnp.ndarray]: + """ + Simulate spike trains using the Poisson-GLM as a recurrent network. + + This function projects spike trains into the future, employing the fitted + parameters of the GLM. It is capable of simulating spike trains based on a combination + of historical spike activity and external feedforward inputs like convolved currents, light + intensities, etc. + + + Parameters + ---------- + random_key : + PRNGKey for seeding the simulation. + n_timesteps : + Duration of the simulation in terms of time steps. + init_y : + Initial spike counts matrix that kickstarts the simulation. + Expected shape: (window_size, n_neurons). + coupling_basis_matrix : + Basis matrix for coupling, representing inter-neuron effects + and auto-correlations. Expected shape: (window_size, n_basis_coupling). + feedforward_input : + External input matrix to the model, representing factors like convolved currents, + light intensities, etc. When not provided, the simulation is done with coupling-only. + Expected shape: (n_timesteps, n_neurons, n_basis_input). + device : + Computation device to use ('cpu' or 'gpu'). Default is 'cpu'. + + Returns + ------- + simulated_spikes : + Simulated spike counts for each neuron over time. + Shape: (n_neurons, n_timesteps). + firing_rates : + Simulated firing rates for each neuron over time. + Shape: (n_neurons, n_timesteps). + + Raises + ------ + NotFittedError + If the model hasn't been fitted prior to calling this method. + Raises + ------ + ValueError + - If the instance has not been previously fitted. + - If there's an inconsistency between the number of neurons in model parameters. + - If the number of neurons in input arguments doesn't match with model parameters. + - For an invalid computational device selection. + + + See Also + -------- + predict : Method to predict firing rates based on the model's parameters. + + Notes + ----- + The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients + (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. + The remaining coefficients correspond to the weights for the feed-forward input. + + + The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` + to ensure consistency in the model's input feature dimensionality. + """ + simulated_spikes, firing_rates = super()._safe_simulate(random_key=random_key, + n_timesteps=n_timesteps, + init_y=init_y, + coupling_basis_matrix=coupling_basis_matrix, + random_function=jax.random.poisson, + feedforward_input=feedforward_input, + device=device + ) + return simulated_spikes, firing_rates + + diff --git a/tests/test_model_base.py b/tests/test_base_class.py similarity index 99% rename from tests/test_model_base.py rename to tests/test_base_class.py index ad5bbe1f..41f57986 100644 --- a/tests/test_model_base.py +++ b/tests/test_base_class.py @@ -1,5 +1,5 @@ import pytest -from typing import Union, Tuple +from typing import Union import jax.numpy as jnp from numpy.typing import NDArray from neurostatslib.base_class import BaseRegressor # Adjust this import to your module name @@ -36,6 +36,7 @@ def simulate( ) -> jnp.ndarray: pass + class MockBaseRegressor_Invalid(BaseRegressor): """ Mock model that intentionally doesn't implement all the required abstract methods. @@ -78,7 +79,6 @@ def set_params(): assert model.param1 == "changed" assert model.std_param == 1 - def test_invalid_set_params(): """Test invalid parameter setting using the set_params method.""" model = MockBaseRegressor() diff --git a/tests/test_glm.py b/tests/test_glm.py index d0d47a18..915ac89d 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -6,6 +6,7 @@ import numpy as np import pytest import statsmodels.api as sm +from sklearn.model_selection import GridSearchCV import neurostatslib as nsl @@ -122,7 +123,7 @@ def test_fit_param_values(self, add_entry, add_to, poissonGLM_model_instantiatio raise_exception = jnp.isnan(add_entry) or jnp.isinf(add_entry) if raise_exception: - with pytest.raises(ValueError, match="Input (X|spike_data) contains a NaNs or Infs"): + with pytest.raises(ValueError, match="Input (X|y) contains a NaNs or Infs"): model.fit(X, y, init_params=true_params) else: model.fit(X, y, init_params=true_params) @@ -316,7 +317,7 @@ def test_fit_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): y = np.zeros((n_samples, n_neurons, 1)) if raise_exception: - with pytest.raises(ValueError, match="spike_data must be two-dimensional"): + with pytest.raises(ValueError, match="y must be two-dimensional"): model.fit(X, y, init_params=(init_w, init_b)) else: model.fit(X, y, init_params=(init_w, init_b)) @@ -377,7 +378,7 @@ def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): init_b = jnp.zeros((n_neurons,)) X = jnp.zeros((X.shape[0] + delta_tp, ) + X.shape[1:]) if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + with pytest.raises(ValueError, match="The number of time-points in X and y"): model.fit(X, y, init_params=(init_w, init_b)) else: model.fit(X, y, init_params=(init_w, init_b)) @@ -385,7 +386,7 @@ def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): """ - Test the `fit` method for inconsistencies in time-points in spike_data. Ensure the correct number of time-points. + Test the `fit` method for inconsistencies in time-points in y. Ensure the correct number of time-points. """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -394,7 +395,7 @@ def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): init_b = jnp.zeros((n_neurons,)) y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + with pytest.raises(ValueError, match="The number of time-points in X and y"): model.fit(X, y, init_params=(init_w, init_b)) else: model.fit(X, y, init_params=(init_w, init_b)) @@ -423,7 +424,7 @@ def test_score_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantia @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_score_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): """ - Test the `score` method when the number of neurons in spike_data differs. Ensure the correct number of neurons. + Test the `score` method when the number of neurons in y differs. Ensure the correct number of neurons. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -466,8 +467,8 @@ def test_score_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation) @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): """ - Test the `score` method with spike_data of different dimensionalities. - Ensure correct dimensionality for spike_data. + Test the `score` method with y of different dimensionalities. + Ensure correct dimensionality for y. """ raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -484,7 +485,7 @@ def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation) y = np.zeros((n_samples, n_neurons, 1)) if raise_exception: - with pytest.raises(ValueError, match="spike_data must be two-dimensional"): + with pytest.raises(ValueError, match="y must be two-dimensional"): model.score(X, y) else: model.score(X, y) @@ -535,7 +536,7 @@ def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): """ Test the `score` method for inconsistencies in time-points in X. - Ensure that the number of time-points in X and spike_data matches. + Ensure that the number of time-points in X and y matches. """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -544,7 +545,7 @@ def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + with pytest.raises(ValueError, match="The number of time-points in X and y"): model.score(X, y) else: model.score(X, y) @@ -552,8 +553,8 @@ def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): """ - Test the `score` method for inconsistencies in time-points in spike_data. - Ensure that the number of time-points in X and spike_data matches. + Test the `score` method for inconsistencies in time-points in y. + Ensure that the number of time-points in X and y matches. """ raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -562,7 +563,7 @@ def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and spike_data"): + with pytest.raises(ValueError, match="The number of time-points in X and y"): model.score(X, y) else: model.score(X, y) @@ -610,7 +611,7 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) def test_predict_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): """ Test the `predict` method when the number of neurons in X differs. - Ensure that the number of neurons in X, spike_data and params matches. + Ensure that the number of neurons in X, y and params matches. """ raise_exception = delta_n_neuron != 0 X, _, model, true_params, _ = poissonGLM_model_instantiation @@ -712,14 +713,14 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, with pytest.raises(ValueError, match="The number of neuron in the model parameters"): model.simulate(random_key=random_key, n_timesteps=n_time_points, - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=n_time_points, - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -746,14 +747,14 @@ def test_simulate_input_dimensionality(self, delta_dim, with pytest.raises(ValueError, match="X must be three-dimensional"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -777,17 +778,17 @@ def test_simulate_y_dimensionality(self, delta_dim, init_spikes = np.zeros((n_samples, n_neurons, 1)) if raise_exception: - with pytest.raises(ValueError, match="spike_data must be two-dimensional"): + with pytest.raises(ValueError, match="y must be two-dimensional"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -809,14 +810,14 @@ def test_simulate_n_neuron_match_y(self, delta_n_neuron, with pytest.raises(ValueError, match="The number of neuron in the model parameters"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -838,14 +839,14 @@ def test_simulate_is_fit(self, is_fit, with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -854,7 +855,7 @@ def test_simulate_is_fit(self, is_fit, def test_simulate_time_point_match_y(self, delta_tp, poissonGLM_coupled_model_config_simulate): """ - Test the `simulate` method to ensure that the time points in init_spikes + Test the `simulate` method to ensure that the time points in init_y are consistent with the coupling_basis window size (they must be equal). """ raise_exception = delta_tp != 0 @@ -865,17 +866,17 @@ def test_simulate_time_point_match_y(self, delta_tp, init_spikes.shape[1])) if raise_exception: - with pytest.raises(ValueError, match="`init_spikes` and `coupling_basis_matrix`"): + with pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -895,17 +896,17 @@ def test_simulate_time_point_match_coupling_basis(self, delta_tp, coupling_basis.shape[1:]) if raise_exception: - with pytest.raises(ValueError, match="`init_spikes` and `coupling_basis_matrix`"): + with pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -932,14 +933,14 @@ def test_simulate_feature_consistency_input(self, delta_features, with pytest.raises(ValueError, match="The number of feed forward input features"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -965,14 +966,14 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, with pytest.raises(ValueError, match="The number of feed forward input features"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -996,14 +997,14 @@ def test_simulate_input_timepoints(self, delta_tp, with pytest.raises(ValueError, match="`feedforward_input` must be of length"): model.simulate(random_key=random_key, n_timesteps=n_timesteps, - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, n_timesteps=n_timesteps, - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") @@ -1029,7 +1030,7 @@ def test_simulate_device_tspec(self, device_spec, with pytest.raises(ValueError, match=f"Invalid device specification: {device_spec}"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device=device_spec) @@ -1037,14 +1038,14 @@ def test_simulate_device_tspec(self, device_spec, with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device=device_spec) else: model.simulate(random_key=random_key, n_timesteps=feedforward_input.shape[0], - init_spikes=init_spikes, + init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device=device_spec) @@ -1083,3 +1084,8 @@ def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiatio model.basis_coeff_.flatten())) if not np.allclose(fit_params_sm, fit_params_model): raise ValueError("Fitted parameters do not match that of statsmodels!") + + def test_compatibility_with_sklearn_cv(self, poissonGLM_model_instantiation): + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + param_grid = {"solver_name": ["BFGS", "GradientDescent"]} + GridSearchCV(model, param_grid).fit(X, y) From 8d2622ceacdf50c38f73337a17719203690d3804 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 18:38:23 -0400 Subject: [PATCH 029/250] fixed example in Error --- src/neurostatslib/exceptions.py | 10 +++++----- src/neurostatslib/glm.py | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/exceptions.py b/src/neurostatslib/exceptions.py index 30e14f82..9b925b90 100644 --- a/src/neurostatslib/exceptions.py +++ b/src/neurostatslib/exceptions.py @@ -6,14 +6,14 @@ class NotFittedError(ValueError, AttributeError): Examples -------- - >>> from sklearn.svm import LinearSVC - >>> from sklearn.exceptions import NotFittedError + >>> from neurostatslib.glm import PoissonGLM + >>> from neurostatslib.exceptions import NotFittedError >>> try: - ... PoissonGLM().predict([[1, 2], [2, 3], [3, 4]]) + ... PoissonGLM().predict([[[1, 2], [2, 3], [3, 4]]]) ... except NotFittedError as e: ... print(repr(e)) - NotFittedError("This LinearSVC instance is not fitted yet. Call 'fit' with - appropriate arguments before using this estimator."...) + NotFittedError("This GLM instance is not fitted yet. Call 'fit' with + appropriate arguments.") .. versionchanged:: 0.18 Moved from sklearn.utils.validation. diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index fbd76d06..b9bb7439 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -844,8 +844,6 @@ def simulate( ------ NotFittedError If the model hasn't been fitted prior to calling this method. - Raises - ------ ValueError - If the instance has not been previously fitted. - If there's an inconsistency between the number of neurons in model parameters. From 5d80e9dfcb831bcfc60a9a7724c7679242f59cd0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 18:40:09 -0400 Subject: [PATCH 030/250] linted all --- src/neurostatslib/__init__.py | 2 +- src/neurostatslib/base_class.py | 47 +++++++++----------- src/neurostatslib/glm.py | 79 +++++++++++++++------------------ src/neurostatslib/utils.py | 5 ++- 4 files changed, 62 insertions(+), 71 deletions(-) diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index 2779195c..8a8c660d 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -from . import basis, glm, sample_points, utils, exceptions +from . import basis, exceptions, glm, sample_points, utils diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index feec6abb..0cf6dbe2 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -7,11 +7,11 @@ import inspect import warnings from collections import defaultdict -from typing import Tuple, Union, Optional, Literal, Callable, Sequence +from typing import Callable, Literal, Optional, Sequence, Tuple, Union import jax import jax.numpy as jnp -from numpy.typing import NDArray, ArrayLike, DTypeLike +from numpy.typing import ArrayLike, DTypeLike, NDArray class _Base(abc.ABC): @@ -138,14 +138,12 @@ def _get_param_names(cls): # Consider the constructor parameters excluding 'self' parameters = [ - p.name - for p in init_signature.parameters.values() - if p.name != "self" + p.name for p in init_signature.parameters.values() if p.name != "self" ] # remove kwargs - if 'kwargs' in parameters: - parameters.remove('kwargs') + if "kwargs" in parameters: + parameters.remove("kwargs") # Extract and sort argument names excluding 'self' return sorted(parameters) @@ -169,13 +167,13 @@ def score( @abc.abstractmethod def simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_spikes: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray], - feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu" + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_spikes: Union[NDArray, jnp.ndarray], + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "cpu", ): pass @@ -238,7 +236,7 @@ def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, ...]: @staticmethod def _check_input_dimensionality( - X: Optional[jnp.ndarray] = None, y: Optional[jnp.ndarray] = None + X: Optional[jnp.ndarray] = None, y: Optional[jnp.ndarray] = None ): if not (y is None): if y.ndim != 2: @@ -253,9 +251,9 @@ def _check_input_dimensionality( @staticmethod def _check_input_and_params_consistency( - params: Tuple[jnp.ndarray, jnp.ndarray], - X: Optional[jnp.ndarray] = None, - y: Optional[jnp.ndarray] = None, + params: Tuple[jnp.ndarray, jnp.ndarray], + X: Optional[jnp.ndarray] = None, + y: Optional[jnp.ndarray] = None, ): """ Validate the number of neurons in model parameters and input arguments. @@ -311,20 +309,17 @@ def _check_input_n_timepoints(X: jnp.ndarray, y: jnp.ndarray): ) def _preprocess_fit( - self, - X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: - # check input dimensionality self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) # convert to jnp.ndarray of floats - X, y = self._convert_to_jnp_ndarray( - X, y, data_type=jnp.float32 - ) + X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) if self._has_invalid_entry(X): raise ValueError("Input X contains a NaNs or Infs!") diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index b9bb7439..367fe1ce 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -11,8 +11,8 @@ from numpy.typing import ArrayLike, NDArray from .base_class import BaseRegressor -from .utils import convolve_1d_trials, has_local_device from .exceptions import NotFittedError +from .utils import convolve_1d_trials, has_local_device class GLMBase(BaseRegressor, abc.ABC): @@ -170,10 +170,7 @@ def _check_is_fit(self): "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) - def predict( - self, - X: Union[NDArray, jnp.ndarray] - ) -> jnp.ndarray: + def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: """Predict firing rates based on fit parameters. Parameters @@ -218,8 +215,10 @@ def _safe_score( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], - score_func: Callable[[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]],jnp.ndarray], - score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None + score_func: Callable[ + [jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]], jnp.ndarray + ], + score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None, ) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. @@ -294,17 +293,13 @@ def _safe_score( self._check_input_n_timepoints(X, y) self._check_input_and_params_consistency((Ws, bs), X=X, y=y) - X, y = self._convert_to_jnp_ndarray( - X, y, data_type=jnp.float32 - ) + X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) if score_type is None: score_type = self.score_type if score_type == "log-likelihood": - score = -( - score_func(X, y, (Ws, bs)) - ) + score = -(score_func(X, y, (Ws, bs))) elif score_type == "pseudo-r2": score = self._pseudo_r2((Ws, bs), X, y) else: @@ -321,10 +316,9 @@ def _safe_simulate( n_timesteps: int, init_y: Union[NDArray, jnp.ndarray], coupling_basis_matrix: Union[NDArray, jnp.ndarray], - random_function: Callable[ - [jax.random.PRNGKeyArray, ArrayLike], jnp.ndarray], + random_function: Callable[[jax.random.PRNGKeyArray, ArrayLike], jnp.ndarray], feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu" + device: Literal["cpu", "gpu", "tpu"] = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using the GLM as a recurrent network. @@ -402,7 +396,9 @@ def _safe_simulate( warnings.warn(f"No {device.upper()} found! Falling back to CPU") target_device = jax.devices("cpu")[0] else: - raise ValueError(f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`.") + raise ValueError( + f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`." + ) # check if the model is fit self._check_is_fit() @@ -461,8 +457,7 @@ def _safe_simulate( subkeys = jax.random.split(random_key, num=n_timesteps) def scan_fn( - data: Tuple[jnp.ndarray, int], - key: jax.random.PRNGKeyArray + data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: """Function to scan over time steps and simulate spikes and firing rates. @@ -549,8 +544,8 @@ def _score( self, X: jnp.ndarray, target_spikes: jnp.ndarray, - params: Tuple[jnp.ndarray, jnp.ndarray] - ) -> jnp.ndarray: + params: Tuple[jnp.ndarray, jnp.ndarray], + ) -> jnp.ndarray: """Score the predicted firing rates against target spike counts. This computes the Poisson negative log-likelihood up to a constant. @@ -604,15 +599,13 @@ def _score( """ # Avoid the edge-case of 0*log(0), much faster than # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10 ** -10) + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) x = target_spikes * jnp.log(predicted_firing_rates) # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) def _residual_deviance( - self, - predicted_rate: jnp.ndarray, - spike_counts: jnp.ndarray + self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray ) -> jnp.ndarray: r"""Compute the residual deviance for a Poisson model. @@ -644,14 +637,16 @@ def _residual_deviance( """ # this takes care of 0s in the log ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) - resid_dev = 2 * (spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate)) + resid_dev = 2 * ( + spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) + ) return resid_dev def score( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood" + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", ) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. @@ -728,11 +723,10 @@ def score( """ norm_constant = jax.scipy.special.gammaln(y + 1).mean() - return super()._safe_score(X=X, - y=y, - score_type=score_type, - score_func=self._score - ) - norm_constant + return ( + super()._safe_score(X=X, y=y, score_type=score_type, score_func=self._score) + - norm_constant + ) def fit( self, @@ -801,7 +795,7 @@ def simulate( init_y: Union[NDArray, jnp.ndarray], coupling_basis_matrix: Union[NDArray, jnp.ndarray], feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu" + device: Literal["cpu", "gpu", "tpu"] = "cpu", ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Simulate spike trains using the Poisson-GLM as a recurrent network. @@ -865,14 +859,13 @@ def simulate( The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure consistency in the model's input feature dimensionality. """ - simulated_spikes, firing_rates = super()._safe_simulate(random_key=random_key, - n_timesteps=n_timesteps, - init_y=init_y, - coupling_basis_matrix=coupling_basis_matrix, - random_function=jax.random.poisson, - feedforward_input=feedforward_input, - device=device - ) + simulated_spikes, firing_rates = super()._safe_simulate( + random_key=random_key, + n_timesteps=n_timesteps, + init_y=init_y, + coupling_basis_matrix=coupling_basis_matrix, + random_function=jax.random.poisson, + feedforward_input=feedforward_input, + device=device, + ) return simulated_spikes, firing_rates - - diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index 46c7c43a..8af3335f 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -384,6 +384,7 @@ def row_wise_kron(a, c): return K + def has_local_device(device_type: str) -> bool: """ Scan for local device availability. @@ -397,4 +398,6 @@ def has_local_device(device_type: str) -> bool: True if the jax finds the device, False otherwise. """ - return any(device_type in device.device_kind.lower() for device in jax.local_devices()) \ No newline at end of file + return any( + device_type in device.device_kind.lower() for device in jax.local_devices() + ) From 6c3da95346d19e5a6c6783af3ae2c80fa9bdbabd Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 18:42:51 -0400 Subject: [PATCH 031/250] switched residual deviance to public --- src/neurostatslib/glm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 367fe1ce..410158f0 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -101,7 +101,7 @@ def _predict( return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) @abc.abstractmethod - def _residual_deviance(self, predicted_rate, y): + def residual_deviance(self, predicted_rate, y): r"""Compute the residual deviance for a GLM model. Parameters @@ -154,11 +154,11 @@ def _pseudo_r2(self, params, X, y): """ mu = self._predict(params, X) - res_dev_t = self._residual_deviance(mu, y) + res_dev_t = self.residual_deviance(mu, y) resid_deviance = jnp.sum(res_dev_t**2) null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() - null_dev_t = self._residual_deviance(null_mu, y) + null_dev_t = self.residual_deviance(null_mu, y) null_deviance = jnp.sum(null_dev_t**2) return (null_deviance - resid_deviance) / null_deviance @@ -604,7 +604,7 @@ def _score( # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) - def _residual_deviance( + def residual_deviance( self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray ) -> jnp.ndarray: r"""Compute the residual deviance for a Poisson model. From a342808d496f5745c033e87e2540b02ed7507ca6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 20:20:52 -0400 Subject: [PATCH 032/250] bugfixed tests --- mkdocs.yml | 2 ++ src/neurostatslib/base_class.py | 2 +- src/neurostatslib/glm.py | 15 ++++++++------- tests/test_glm.py | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 40c413a9..196dc4bd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,8 @@ plugins: docstring_style: numpy show_source: true members_order: source + filters: + - "!neurostatslib.glm.GLMBase.residual_deviance" extra_javascript: - javascripts/katex.js diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 0cf6dbe2..d28016d8 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -148,7 +148,7 @@ def _get_param_names(cls): return sorted(parameters) -class BaseRegressor(_Base, abc.ABC): +class _BaseRegressor(_Base, abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps @abc.abstractmethod diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 410158f0..128fa28d 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -10,12 +10,12 @@ import jaxopt from numpy.typing import ArrayLike, NDArray -from .base_class import BaseRegressor +from .base_class import _BaseRegressor from .exceptions import NotFittedError from .utils import convolve_1d_trials, has_local_device -class GLMBase(BaseRegressor, abc.ABC): +class _BaseGLM(_BaseRegressor, abc.ABC): """Abstract base class for Poisson GLMs. Provides methods for score computation, simulation, and prediction. @@ -497,7 +497,7 @@ def scan_fn( return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) -class PoissonGLM(GLMBase): +class PoissonGLM(_BaseGLM): """Un-regularized Poisson-GLM. The class fits the un-penalized maximum likelihood Poisson GLM parameter estimate. @@ -577,7 +577,7 @@ def _score( Returns ------- - jnp.ndarray + : The Poisson negative log-likehood. Shape (1,). Notes @@ -618,6 +618,7 @@ def residual_deviance( Returns ------- + : The residual deviance of the model. Notes @@ -627,8 +628,8 @@ def residual_deviance( $$ \begin{aligned} - D(y, \hat{y}) &= 2 \sum \left[ y \log\left(\frac{y}{\hat{y}}\right) - (y - \hat{y}) \right]\\\ - &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) + D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ + &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) \end{aligned} $$ where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model @@ -694,7 +695,7 @@ def score( Default is defined at class initialization. Returns ------- - score : (1,) + score : The Poisson log-likelihood or the pseudo-$R^2$ of the current model. Raises diff --git a/tests/test_glm.py b/tests/test_glm.py index 915ac89d..ad58fff9 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1064,7 +1064,7 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): model.set_params(inverse_link_function=jnp.exp) # get the rate dev = sm.families.Poisson().deviance(y, firing_rate) - dev_model = model._residual_deviance(firing_rate, y).sum() + dev_model = model.residual_deviance(firing_rate, y).sum() if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") From 91680e1f9b8207ad22e9f939c0c13846e7b71690 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 20:26:46 -0400 Subject: [PATCH 033/250] refractor classes names --- src/neurostatslib/base_class.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index d28016d8..617070bd 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -1,20 +1,18 @@ """Abstract class for models. - -Inheriting this class will result in compatibility with sci-kit learn pipelines. """ import abc import inspect import warnings from collections import defaultdict -from typing import Callable, Literal, Optional, Sequence, Tuple, Union +from typing import Literal, Optional, Tuple, Union import jax import jax.numpy as jnp -from numpy.typing import ArrayLike, DTypeLike, NDArray +from numpy.typing import ArrayLike, NDArray -class _Base(abc.ABC): +class Base(abc.ABC): def __init__(self, **kwargs): self._kwargs_keys = list(kwargs.keys()) for key in kwargs: @@ -148,7 +146,7 @@ def _get_param_names(cls): return sorted(parameters) -class _BaseRegressor(_Base, abc.ABC): +class _BaseRegressor(Base, abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps @abc.abstractmethod From c8dd03138baf518fd49dc442ef4befbf4dd582b3 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 25 Aug 2023 20:37:12 -0400 Subject: [PATCH 034/250] bugfixed test_base --- pyproject.toml | 3 ++- src/neurostatslib/__init__.py | 2 +- src/neurostatslib/glm.py | 3 ++- tests/test_base_class.py | 14 +++++++------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ed3bd0be..20245ec3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,7 +55,8 @@ dev = [ "coverage", # Test coverage measurement "pytest-cov", # Test coverage plugin for pytest "statsmodels", # Used to compare model pseudo-r2 in testing - "scikit-learn" # Testing compatibility with CV & pipelines + "scikit-learn", # Testing compatibility with CV & pipelines + "PyYAML" # Load GLM params for testing ] docs = [ "mkdocs", # Documentation generator diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index 8a8c660d..0e5a2718 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -from . import basis, exceptions, glm, sample_points, utils +from . import base_class, basis, exceptions, glm, sample_points, utils diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 128fa28d..efb2bf45 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -628,7 +628,8 @@ def residual_deviance( $$ \begin{aligned} - D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ + D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) + - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) \end{aligned} $$ diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 41f57986..a900a7e8 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -2,11 +2,11 @@ from typing import Union import jax.numpy as jnp from numpy.typing import NDArray -from neurostatslib.base_class import BaseRegressor # Adjust this import to your module name +from neurostatslib.base_class import _BaseRegressor # Sample subclass to test instantiation and methods -class MockBaseRegressor(BaseRegressor): +class MockBaseRegressor(_BaseRegressor): """ Mock implementation of the BaseRegressor abstract class for testing purposes. Implements all required abstract methods as empty methods. @@ -37,7 +37,7 @@ def simulate( pass -class MockBaseRegressor_Invalid(BaseRegressor): +class MockBaseRegressor_Invalid(_BaseRegressor): """ Mock model that intentionally doesn't implement all the required abstract methods. Used for testing the instantiation of incomplete concrete classes. @@ -96,7 +96,7 @@ def test_get_param_names(): def test_convert_to_jnp_ndarray(): """Test data conversion to JAX NumPy arrays.""" data = [1, 2, 3] - jnp_data, = BaseRegressor._convert_to_jnp_ndarray(data) + jnp_data, = _BaseRegressor._convert_to_jnp_ndarray(data) assert isinstance(jnp_data, jnp.ndarray) assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) @@ -105,15 +105,15 @@ def test_has_invalid_entry(): """Test validation of data arrays.""" valid_data = jnp.array([1, 2, 3]) invalid_data = jnp.array([1, 2, jnp.nan]) - assert not BaseRegressor._has_invalid_entry(valid_data) - assert BaseRegressor._has_invalid_entry(invalid_data) + assert not _BaseRegressor._has_invalid_entry(valid_data) + assert _BaseRegressor._has_invalid_entry(invalid_data) # To ensure abstract methods aren't callable def test_abstract_class(): """Ensure that abstract methods aren't callable.""" with pytest.raises(TypeError, match="Can't instantiate abstract"): - BaseRegressor() + _BaseRegressor() def test_invalid_concrete_class(): """Ensure that classes missing implementation of required abstract methods raise errors.""" From c56fd7e393dae10fb3dda52b2d9c825f0ece1336 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Aug 2023 15:27:56 -0600 Subject: [PATCH 035/250] added an end-to-end fit --- src/neurostatslib/glm.py | 4 +- tests/test_glm.py | 54 +++++++++++ tests/test_glm_runs.py | 63 ------------- tests/test_glm_synthetic.py | 108 ---------------------- tests/test_glm_synthetic_single_neuron.py | 63 ------------- 5 files changed, 56 insertions(+), 236 deletions(-) delete mode 100644 tests/test_glm_runs.py delete mode 100644 tests/test_glm_synthetic.py delete mode 100644 tests/test_glm_synthetic_single_neuron.py diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index efb2bf45..f6257193 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -426,8 +426,8 @@ def _safe_simulate( != Ws.shape[1] ): raise ValueError( - "The number of feed forward input features" - "and the number of recurrent features must add up to" + "The number of feed forward input features " + "and the number of recurrent features must add up to " "the overall model features." f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " diff --git a/tests/test_glm.py b/tests/test_glm.py index ad58fff9..a5d63c7e 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1089,3 +1089,57 @@ def test_compatibility_with_sklearn_cv(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation param_grid = {"solver_name": ["BFGS", "GradientDescent"]} GridSearchCV(model, param_grid).fit(X, y) + + def test_end_to_end_fit_and_simulate(self, + poissonGLM_coupled_model_config_simulate): + model, coupling_basis, feedforward_input, init_spikes, random_key = \ + poissonGLM_coupled_model_config_simulate + window_size = coupling_basis.shape[0] + n_neurons = init_spikes.shape[1] + n_trials = 1 + n_timepoints = feedforward_input.shape[0] + + # generate spike trains + spikes, _ = model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + # convolve basis and spikes + # (n_trials, n_timepoints - ws + 1, n_neurons, n_coupling_basis) + conv_spikes = jnp.asarray( + nsl.utils.convolve_1d_trials(coupling_basis, [spikes]), + dtype=jnp.float32 + ) + + # create an individual neuron predictor by stacking the + # two convolved spike trains in a single feature vector + # and concatenate the trials. + conv_spikes = conv_spikes.reshape(n_trials * (n_timepoints - window_size + 1), -1) + + # replicate for each neuron, + # (n_trials * (n_timepoints - ws + 1), n_neurons, n_neurons * n_coupling_basis) + conv_spikes = jnp.tile(conv_spikes, n_neurons).reshape(conv_spikes.shape[0], + n_neurons, + conv_spikes.shape[1]) + + # add the feed-forward input to the predictors + X = jnp.concatenate((conv_spikes[1:], + feedforward_input[:-window_size]), + axis=2) + + # fit the model + model.fit(X, spikes[:-window_size]) + + # simulate + model.simulate(random_key=random_key, + n_timesteps=feedforward_input.shape[0], + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + device="cpu") + + + diff --git a/tests/test_glm_runs.py b/tests/test_glm_runs.py deleted file mode 100644 index c5073387..00000000 --- a/tests/test_glm_runs.py +++ /dev/null @@ -1,63 +0,0 @@ -import jax -import numpy as np - -from neurostatslib.basis import MSplineBasis -from neurostatslib.glm import GLM - - -class DimensionMismatchError(Exception): - """Exception raised for dimension mismatch errors.""" - - def __init__(self, message): - self.message = message - super().__init__(self.message) - -def test_setup_msplinebasis(): - """ - Minimal test for MSplineBasis definition. - - Returns - ------- - None - - Raises - ------ - DimensionMismatchError - If the output basis matrix has mismatched dimensions with the specified basis functions or window size. - - Notes - ----- - This function performs a minimal test for defining the MBasis by generating basis functions using different orders. - It checks if the output basis matrix has dimensions that match the specified number of basis functions and window size. - """ - n_basis = 6 - window = 100 - for order in range(1,6): - spike_basis = MSplineBasis(n_basis_funcs=n_basis, order=order) - spike_basis_matrix = spike_basis.evaluate(np.arange(window)).T - if spike_basis_matrix.shape[0] != n_basis: - raise DimensionMismatchError(f"The output basis matrix has {spike_basis_matrix.shape[1]} time points, while the number of basis specified is {n_basis}. They must agree.") - - - if spike_basis_matrix.shape[1] != window: - raise DimensionMismatchError(f"The output basis basis matrix has {spike_basis_matrix.shape[1]} window size, while the window size specified is {window}. They must agree.") - - - -def test_run_end_to_end_glm(): - nn, nt = 10, 1000 - key = jax.random.PRNGKey(123) - key, subkey = jax.random.split(key) - spike_data = jax.random.bernoulli( - subkey, jax.numpy.ones((nn, nt))*.5 - ).astype("int64") - - spike_basis = MSplineBasis(n_basis_funcs=6, order=3) - spike_basis_matrix = spike_basis.evaluate(np.arange(100)).T - model = GLM(spike_basis_matrix) - - model.fit(spike_data) - model.predict(spike_data) - key, subkey = jax.random.split(key) - X = model.simulate(subkey, 20, spike_data[:, :100]) - \ No newline at end of file diff --git a/tests/test_glm_synthetic.py b/tests/test_glm_synthetic.py deleted file mode 100644 index df2c67e3..00000000 --- a/tests/test_glm_synthetic.py +++ /dev/null @@ -1,108 +0,0 @@ -import matplotlib - -matplotlib.use('agg') - -import itertools - -import jax -import jax.numpy as jnp -import matplotlib.pyplot as plt -import numpy as onp - -import neurostatslib as nsl -from neurostatslib.basis import RaisedCosineBasisLinear -from neurostatslib.glm import GLM - - -def test_set_up_glm(): - """Test the setup of the Generalized Linear Model (GLM). - - Returns - ------- - GLM - The simulated model of the Generalized Linear Model. - - Notes - ----- - This function performs the setup for the Generalized Linear Model (GLM) by creating the necessary objects and variables. - It generates a raised cosine basis, defines the simulated model using the basis functions, and returns the GLM object. - """ - nn, nt, ws = 2, 1000, 100 - simulation_key = jax.random.PRNGKey(123) - - spike_basis = RaisedCosineBasisLinear( - n_basis_funcs=5 - ) - - B = spike_basis.evaluate(onp.linspace(0, 1, ws)).T - - w0 = onp.array([-.1, -.1, -.2, -.2, -1]) - w1 = onp.array([0, .1, .5, .1, 0]) - - W = onp.empty((2, 5, 2)) - for i, j in itertools.product(range(nn), range(nn)): - W[i, :, j] = w0 if (i == j) else w1 - - simulated_model = GLM(B) - - - -def test_fit_glm2(): - jax.config.update("jax_platform_name", "cpu") - jax.config.update("jax_enable_x64", True) - - nn, nt, ws = 2, 1000, 100 - simulation_key = jax.random.PRNGKey(123) - - spike_basis = RaisedCosineBasisLinear( - n_basis_funcs=5 - ) - - B = spike_basis.evaluate(onp.linspace(0, 1, ws)).T - - w0 = onp.array([-.1, -.1, -.2, -.2, -1]) - w1 = onp.array([0, .1, .5, .1, 0]) - - W = onp.empty((2, 5, 2)) - for i, j in itertools.product(range(nn), range(nn)): - W[i, :, j] = w0 if (i == j) else w1 - - simulated_model = GLM(B) - simulated_model.basis_coeff_ = jnp.array(W) - simulated_model.baseline_link_fr_ = jnp.ones(nn) * .1 - - init_spikes = jnp.zeros((2, ws)) - spike_data = simulated_model.simulate(simulation_key, nt, init_spikes) - sim_pred = simulated_model.predict(spike_data) - - fitted_model = GLM( - B, - solver_name="GradientDescent", - solver_kwargs=dict(maxiter=1000, acceleration=False, verbose=True, stepsize=-1) - ) - - fitted_model.fit(spike_data) - fit_pred = fitted_model.predict(spike_data) - - fig, axes = plt.subplots(2, 1) - axes[0].plot(onp.arange(nt), spike_data[0]) - axes[0].plot(onp.arange(ws, nt + 1), sim_pred[0]) - axes[0].plot(onp.arange(ws, nt + 1), fit_pred[0]) - axes[1].plot(onp.arange(nt), spike_data[1]) - axes[1].plot(onp.arange(ws, nt + 1), sim_pred[1]) - axes[1].plot(onp.arange(ws, nt + 1), fit_pred[1]) - - - fig, axes = plt.subplots(nn, nn, sharey=True) - for i, j in itertools.product(range(nn), range(nn)): - axes[i, j].plot( - B.T @ simulated_model.basis_coeff_[i, :, j], - label="true" - ) - axes[i, j].plot( - B.T @ fitted_model.basis_coeff_[i, :, j], - label="est" - ) - axes[i, j].axhline(0, dashes=[2, 2], color='k') - axes[-1, -1].legend() - plt.close('all') \ No newline at end of file diff --git a/tests/test_glm_synthetic_single_neuron.py b/tests/test_glm_synthetic_single_neuron.py deleted file mode 100644 index 29651b2a..00000000 --- a/tests/test_glm_synthetic_single_neuron.py +++ /dev/null @@ -1,63 +0,0 @@ -import matplotlib - -matplotlib.use('agg') - -import jax -import jax.numpy as jnp -import matplotlib.pyplot as plt -import numpy as onp - -import neurostatslib as nsl -from neurostatslib.basis import RaisedCosineBasisLog -from neurostatslib.glm import GLM - - -def test_glm_fit(): - jax.config.update("jax_platform_name", "cpu") - jax.config.update("jax_enable_x64", True) - - nn, nt, ws = 1, 1000, 100 - simulation_key = jax.random.PRNGKey(123) - - spike_basis = RaisedCosineBasisLog( - n_basis_funcs=5 - ) - B = spike_basis.evaluate(onp.linspace(0, 1, ws)).T - - simulated_model = GLM(B) - simulated_model.basis_coeff_ = jnp.array([0, 0, -1, -1, -1])[None, :, None] - simulated_model.baseline_link_fr_ = jnp.ones(nn) * .1 - - init_spikes = jnp.zeros((nn, ws)) - spike_data = simulated_model.simulate(simulation_key, nt, init_spikes) - sim_pred = simulated_model.predict(spike_data) - - fitted_model = GLM( - B, - solver_name="GradientDescent", - solver_kwargs=dict(maxiter=1000, acceleration=False, verbose=True, stepsize=0.0) - - ) - - fitted_model.fit(spike_data) - fit_pred = fitted_model.predict(spike_data) - - fig, ax = plt.subplots(1, 1) - ax.plot(onp.arange(nt), spike_data[0]) - ax.plot(onp.arange(ws, nt + 1), sim_pred[0]) - ax.plot(onp.arange(ws, nt + 1), fit_pred[0]) - - - fig, ax = plt.subplots(1, 1, sharey=True) - ax.plot( - B.T @ simulated_model.basis_coeff_[0, :, 0], - label="true" - ) - ax.plot( - B.T @ fitted_model.basis_coeff_[0, :, 0], - label="est" - ) - ax.axhline(0, dashes=[2, 2], color='k') - ax.legend() - - plt.close('all') \ No newline at end of file From 7e21a3659c3c3dcc7236b9eb893921bf88bccaf1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Aug 2023 16:08:23 -0600 Subject: [PATCH 036/250] improved docstrings of base_class --- src/neurostatslib/base_class.py | 53 +++++++++++++++++++++++++++++---- src/neurostatslib/glm.py | 7 +++-- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 617070bd..3e4b6487 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -12,7 +12,7 @@ from numpy.typing import ArrayLike, NDArray -class Base(abc.ABC): +class _Base: def __init__(self, **kwargs): self._kwargs_keys = list(kwargs.keys()) for key in kwargs: @@ -146,7 +146,7 @@ def _get_param_names(cls): return sorted(parameters) -class _BaseRegressor(Base, abc.ABC): +class _BaseRegressor(_Base, abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps @abc.abstractmethod @@ -179,6 +179,20 @@ def simulate( def _convert_to_jnp_ndarray( *args: Union[NDArray, jnp.ndarray], data_type: jnp.dtype = jnp.float32 ) -> Tuple[jnp.ndarray, ...]: + """Convert provided arrays to jnp.ndarray of specified type. + + Parameters + ---------- + *args : + Input arrays to convert. + data_type : + Data type to convert to. Default is jnp.float32. + + Returns + ------- + : + Converted arrays. + """ return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) @staticmethod @@ -312,13 +326,42 @@ def _preprocess_fit( y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: + """Preprocess input data and initial parameters for the fit method. + + This method carries out the following preprocessing steps: + - Convert input data `X` and `y` to `jnp.ndarray` of type float32. + - Check the dimensionality of the inputs. + - Check for any NaNs or Infs in the inputs. + - If `init_params` is not provided, initialize it with default values. + - Validate the consistency of input dimensions with the initial parameters. + + Parameters + ---------- + X : + Input data, expected to be of shape (n_timebins, n_neurons, n_features). + y : + Target values, expected to be of shape (n_timebins, n_neurons). + init_params : + Initial parameters for the model. If None, they are initialized with default values. + + Returns + ------- + : + Preprocessed input data `X`, target values `y`, and initialized parameters. + + Raises + ------ + ValueError + If there are inconsistencies in the input shapes or if NaNs or Infs are detected. + """ + + # convert to jnp.ndarray of float32 + X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) + # check input dimensionality self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) - # convert to jnp.ndarray of floats - X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) - if self._has_invalid_entry(X): raise ValueError("Input X contains a NaNs or Infs!") elif self._has_invalid_entry(y): diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index f6257193..ba2ca8a0 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -208,7 +208,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: self._check_input_dimensionality(X=X) # check consistency between X and params self._check_input_and_params_consistency((Ws, bs), X=X) - (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) + (X, ) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) return self._predict((Ws, bs), X) def _safe_score( @@ -289,12 +289,13 @@ def _safe_score( self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ + + X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) + self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) self._check_input_and_params_consistency((Ws, bs), X=X, y=y) - X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) - if score_type is None: score_type = self.score_type From 02be1dabb3aa8c4c929ba3620accff7b6fde7a4c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 28 Aug 2023 17:47:46 -0600 Subject: [PATCH 037/250] improved notes --- docs/developers_notes/base_class.md | 15 +++--- docs/developers_notes/glm.md | 73 +++++++++++++---------------- src/neurostatslib/glm.py | 5 +- 3 files changed, 44 insertions(+), 49 deletions(-) diff --git a/docs/developers_notes/base_class.md b/docs/developers_notes/base_class.md index ae17f006..7c919168 100644 --- a/docs/developers_notes/base_class.md +++ b/docs/developers_notes/base_class.md @@ -2,14 +2,14 @@ ## Introduction -The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. Currently, the sole abstract class available is `BaseRegressor`. +The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. Currently, the sole abstract class available is `_BaseRegressor`. -The `_Base` class is envisioned as the foundational component for any model type (e.g., regression, dimensionality reduction, clustering, etc.). In contrast, abstract classes derived from `_Base` define overarching model categories (e.g., `BaseRegressor` is building block for GLMs, GAMS, etc.). +The `_Base` class is envisioned as the foundational component for any model type (e.g., regression, dimensionality reduction, clustering, etc.). In contrast, abstract classes derived from `_Base` define overarching model categories (e.g., `_BaseRegressor` is building block for GLMs, GAMS, etc.). Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. !!! Example - The current package version includes a concrete class named `neurostatslib.glm.PoissonGLM`. This class inherits from `BaseRegressor`, since it falls under the "regression" category. As any `BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. + The current package version includes a concrete class named `neurostatslib.glm.PoissonGLM`. This class inherits from `_BaseRegressor`, since it falls under the "regression" category. As any `_BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. ## The Class `model_base._Base` @@ -28,20 +28,21 @@ The `get_params` method retrieves parameters set during model instance initializ The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. -## The Abstract Class `model_base.BaseRegressor` +## The Abstract Class `model_base._BaseRegressor` -`BaseRegressor` is an abstract class that inherits from `_Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. +`_BaseRegressor` is an abstract class that inherits from `_Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. ### Abstract Methods -For subclasses derived from `BaseRegressor` to function correctly, they must implement the following: +For subclasses derived from `_BaseRegressor` to function correctly, they must implement the following: 1. `fit`: Adapt the model using input data `X` and corresponding observations `y`. 2. `predict`: Provide predictions based on the trained model and input data `X`. 3. `score`: Gauge the accuracy of model predictions using input data `X` against the actual observations `y`. 4. `simulate`: Simulate data based on the trained regression model. -Moreover, `BaseRegressor` incorporates auxiliary methods such as `_get_param_names`, `_convert_to_jnp_ndarray`, and `_has_invalid_entry`. +Moreover, `_BaseRegressor` incorporates auxiliary methods such as `_convert_to_jnp_ndarray`, `_has_invalid_entry` +and a number of other methods for checking input consistency. !!! Tip Deciding between concrete and abstract methods in a superclass can be nuanced. As a general guideline: any method that's expected in all subclasses and isn't subclass-specific should be concretely implemented in the superclass. Conversely, methods essential for a subclass's expected behavior, but vary based on the subclass, should be abstract in the superclass. For instance, compatibility with the `sklearn.cross_validation` module demands `score`, `fit`, `get_params`, and `set_params` methods. Given their specificity to individual models, `score` and `fit` are abstract in `BaseRegressor`. Conversely, as `get_params` and `set_params` are consistent across model classes, they're inherited from `_Base`. This approach typifies our general implementation strategy. However, it's important to note that while these are sound guidelines, exceptions exist based on various factors like future extensibility, clarity, and maintainability. diff --git a/docs/developers_notes/glm.md b/docs/developers_notes/glm.md index 1ca1cecb..bbcfee4f 100644 --- a/docs/developers_notes/glm.md +++ b/docs/developers_notes/glm.md @@ -6,67 +6,58 @@ The `neurostatslib.glm` basis module implements variations of Generalized Linear At stage, the module consists of two classes: -1. The abstract class `PoissonGLMBase`. -2. The concrete class `PoissonGLM`. +1. **`_BaseGLM`:** An abstract class serving as the backbone for building GLM variations. +2. **`PoissonGLM`:** A concrete implementation of the GLM for Poisson-distributed data. We followed the `scikit-learn` api, making the concrete GLM model classes compatible with the powerful `scikit-learn` pipeline and cross-validation modules. -The `PoissonGLMBase` serves as the foundation for implementing Poisson Generalized Linear Models (GLMs). -It designed to follow the `scikit-learn` api in order to guarantee compatibility with `scikit-learn` pipelines. -It inherits `Model` (see the ["ModuleBase Module"](base_class.md)) and implements the public methods `predict`, `score` , `simulate`. -`predict` generates Poisson means based on the current parameter estimate, `score` evaluates the performance of the model, and `simulate` generates simulated spike trains taking into account recurrent connectivity and feedforward inputs. -The core features of this class are centered around abstract methods that must be implemented by any concrete subclasses, ensuring a standardized interface for all types of Poisson GLM models. +## The class `_BaseGLM` -## The Class `PoissonGLMBase` +The class `_BaseGLM` is designed to follow the `scikit-learn` api in order to guarantee compatibility with the `scikit-learn` pipelines, as well as to implement all the computation that is shared by the different `GLM` subclasses. -### Initialization and Configuration +### Inheritance -The `PoissonGLMBase` class's constructor initializes various parameters and settings essential for the model's behavior. These include: +`_BaseGLM` inherits from the `_BaseRegressor` (detailed in the [`base_class` module](base_class.md)). This inheritance provides `_BaseGLM` with a suite of auxiliary methods for handling and validating model inputs. Through abstraction mechanism inherited from `_BaseRegressor`, any GLM subclass is compelled to reimplement the `fit`, `predict`, `score`, and `simulate` methods facilitating compatibility with `scikit-learn`. -- `solver_name`: The name of the optimization solver to be used. -- `solver_kwargs`: Additional keyword arguments for the chosen solver. -- `inverse_link_function`: The callable function for the inverse link transformation. -- `score_type`: The type of scoring method, either "log-likelihood" or "pseudo-r2". +### Attributes -### Method `fit` +- **solver_name**: Name of the solver to use when fitting the GLM. It should be an attribute of `jaxopt`. +- **solver_kwargs**: Dictionary containing keyword arguments for initializing the solver. +- **inverse_link_function**: The link function of the GLM. It must be callable and return non-negative values. +- **kwargs**: Other keyword arguments, like regularization hyperparameters. -The `fit` method is an abstract method that needs to be implemented by subclasses. It is used to train the Poisson GLM model using input data `X` and spike data. The method performs the model fitting process by optimizing the provided loss function. -### Method `predict` +### Public Methods -The `predict` method takes input data `X` and predicts firing rates using the trained Poisson GLM model. It leverages the inverse link function to transform the model's internal parameters into meaningful predictions. +1. **`predict`**: This method checks that the model is fit and validates input consistency and dimensions, and computes mean rates based on the current parameter estimates through the `_predict` method. -### Method `score` +!!! note + `_BaseGLM` lacks concrete implementations for methods like `score`, `fit`, and `simulate`. This is because the specifics of these methods depend on the chosen emission probability. For instance, the scoring method for a Poisson GLM will differ from a Gamma GLM, given their distinct likelihood functions. -The `score` method evaluates the performance of the Poisson GLM model. It computes a score based on the model's predictions and the true spike data. The score can be either the negative log-likelihood or a pseudo-R2 score, depending on the specified `score_type`. +### Private Methods -### Internal Methods +1. **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameter names are likely to be GLM specific. +2. **`_predict`**: Predicts firing rates given predictors and parameters. +3. **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. +4. **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. +5. **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. -The class defines several internal methods that aid in the implementation of its functionalities: -- `_predict`: A specialized prediction method that calculates firing rates using model parameters and input data. -- `_score`: A specialized scoring method that computes a score based on predicted firing rates, true spike data, and model parameters. -- `_residual_deviance`: Calculates the residual deviance of the model's predictions. -- `_pseudo_r2`: Computes the pseudo-R2 score based on the model's predictions, true spike data, and model parameters. -- `_check_is_fit`: Ensures that the instance has been fitted before making predictions or scoring. -- `_check_and_convert_params`: Validates and converts initial parameters to the appropriate format. -- `_check_input_dimensionality`: Checks the dimensionality of input data and spike data to ensure consistency. -- `_check_input_and_params_consistency`: Validates the consistency between input data, spike data, and model parameters. -- `_check_input_n_timepoints`: Verifies that the number of time points in input data and spike data match. -- `_preprocess_fit`: Prepares input data, spike data, and initial parameters for the fitting process. +!!! note + The introduction of `_safe_score` and `_safe_simulate` offers notable benefits: -### Method `simulate` + 1. It eliminates the need for subclasses to redo checks in their `score` and `simulate` methods, leading to concise code. + 2. The methods `score` and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This mandates alignment with the `scikit-learn` API and ensures subclass-specific docstrings. -The `simulate` method generates simulated spike data using the trained Poisson GLM model. It takes into account various parameters, including random keys, coupling basis matrix, and feedforward input. The simulated data can be generated for different devices, such as CPU, GPU, or TPU. +### Abstract Methods +On top of the abstract methods inherited from `_BaseRegressor`, `_BaseGLM` implements, -## The Class `PoissonGLM` +1. **`residual_deviance`**: Computes the residual deviance for a GLM model. The deviance, on par with the likelihood, is model specific. -### Initialization +!!! note + The residual deviance can be written as a function of the log-likelihood. This allows for a concrete implementation of it in the `_BaseGLM`, however the subclass specific implementation can be more robust and/or efficient. -The `PoissonGLM` class extends the `PoissonGLMBase` class and provides a concrete implementation. It inherits the constructor from its parent class and allows additional customization through the specified parameters. +## The Concrete Class `PoissonGLM` -### Method `fit` +The class `PoissonGLM` implements an un-regularized Poisson GLM model. -The `fit` method is implemented in the `PoissonGLM` class to perform the model fitting process using the provided input data and spike data. It leverages optimization solvers and loss functions to update the model's internal parameters. - -This script defines a powerful framework for creating and training Poisson Generalized Linear Models, essential for analyzing and understanding neural activity patterns. diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index ba2ca8a0..2b842343 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -204,11 +204,14 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: # extract model params Ws = self.basis_coeff_ bs = self.baseline_link_fr_ + + (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) + # check input dimensionality self._check_input_dimensionality(X=X) # check consistency between X and params self._check_input_and_params_consistency((Ws, bs), X=X) - (X, ) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) + return self._predict((Ws, bs), X) def _safe_score( From ffd640b1ebb1dba9d390385f74bdbf956ba44761 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 09:24:31 -0600 Subject: [PATCH 038/250] added scheme of model classes --- .../{basis_module.md => 01-basis_module.md} | 0 .../{base_class.md => 02-base_class.md} | 46 +++++++-- docs/developers_notes/03-glm.md | 99 +++++++++++++++++++ docs/developers_notes/README.md | 6 ++ docs/developers_notes/glm.md | 63 ------------ src/neurostatslib/glm.py | 56 ++++++----- 6 files changed, 175 insertions(+), 95 deletions(-) rename docs/developers_notes/{basis_module.md => 01-basis_module.md} (100%) rename docs/developers_notes/{base_class.md => 02-base_class.md} (69%) create mode 100644 docs/developers_notes/03-glm.md delete mode 100644 docs/developers_notes/glm.md diff --git a/docs/developers_notes/basis_module.md b/docs/developers_notes/01-basis_module.md similarity index 100% rename from docs/developers_notes/basis_module.md rename to docs/developers_notes/01-basis_module.md diff --git a/docs/developers_notes/base_class.md b/docs/developers_notes/02-base_class.md similarity index 69% rename from docs/developers_notes/base_class.md rename to docs/developers_notes/02-base_class.md index 7c919168..b9275410 100644 --- a/docs/developers_notes/base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -8,8 +8,37 @@ The `_Base` class is envisioned as the foundational component for any model type Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. +Below a scheme of how we envision the architecture of the `neurostatslib` models. + +``` +Class _Base +| +└─ Abstract Subclass _BaseRegressor +│ │ +│ └─ Abstract Subclass _BaseGLM +│ │ +│ ├─ Concrete Subclass PoissonGLM +│ │ │ +│ │ └─ Concrete Subclass RidgePoissonGLM *(not implemented yet) +│ │ │ +│ │ └─ Concrete Subclass LassoPoissonGLM *(not implemented yet) +│ │ │ +│ │ ... +│ │ +│ ├─ Concrete Subclass GammaGLM *(not implemented yet) +│ │ │ +│ │ ... +│ │ +│ ... +│ +├─ Abstract Subclass _BaseManifold *(not implemented yet) +... +``` + !!! Example - The current package version includes a concrete class named `neurostatslib.glm.PoissonGLM`. This class inherits from `_BaseRegressor`, since it falls under the "regression" category. As any `_BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. + The current package version includes a concrete class named `neurostatslib.glm.PoissonGLM`. This class inherits from `_BaseGLM` <- `_BaseRegressor` <- `_Base`, since it falls under the " GLM regression" category. + As any `_BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. + ## The Class `model_base._Base` @@ -20,13 +49,10 @@ For a detailed understanding, consult the [`scikit-learn` API Reference](https:/ !!! Note We've intentionally omitted the `get_metadata_routing` method. Given its current experimental status and its lack of relevance to the `GLM` class, this method was excluded. Should future needs arise around parameter routing, consider directly inheriting from `sklearn.BaseEstimator`. More information can be found [here](https://scikit-learn.org/stable/metadata_routing.html#metadata-routing). -### The Public Method `get_params` - -The `get_params` method retrieves parameters set during model instance initialization. Opting for a deep inspection allows the method to assess nested object parameters, resulting in a comprehensive parameter dictionary. - -### The Public Method `set_params` +### Public methods -The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. +- **`get_params`**: The `get_params` method retrieves parameters set during model instance initialization. Opting for a deep inspection allows the method to assess nested object parameters, resulting in a comprehensive parameter dictionary. +- **`set_params`**: The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. ## The Abstract Class `model_base._BaseRegressor` @@ -52,9 +78,9 @@ and a number of other methods for checking input consistency. ### Implementing Model Subclasses -When devising a new model subclass based on the `BaseRegressor` abstract class, adhere to the subsequent guidelines: +When devising a new model subclass based on the `_BaseRegressor` abstract class, adhere to the subsequent guidelines: -- **Must** inherit the `BaseRegressor` abstract superclass. -- **Must** realize the abstract methods: `fit`, `predict`, and `score`. +- **Must** inherit the `_BaseRegressor` abstract superclass. +- **Must** realize the abstract methods: `fit`, `predict`, `score`, and `simulate`. - **Should not** overwrite the `get_params` and `set_params` methods, inherited from `_Base`. - **May** introduce auxiliary methods such as `_convert_to_jnp_ndarray` for added utility. diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md new file mode 100644 index 00000000..a401e0a0 --- /dev/null +++ b/docs/developers_notes/03-glm.md @@ -0,0 +1,99 @@ +# The `glm` Module + +## Introduction + +The `neurostatslib.glm` basis module implements variations of Generalized Linear Models (GLMs) classes. + +At stage, the module consists of two classes: + +1. **`_BaseGLM`:** An abstract class serving as the backbone for building GLM variations. +2. **`PoissonGLM`:** A concrete implementation of the GLM for Poisson-distributed data. + +We followed the `scikit-learn` api, making the concrete GLM model classes compatible with the powerful `scikit-learn` pipeline and cross-validation modules. + +## The class `_BaseGLM` + +The class `_BaseGLM` is designed to follow the `scikit-learn` api in order to guarantee compatibility with the `scikit-learn` pipelines, as well as to implement all the computation that is shared by the different `GLM` subclasses. + +### Inheritance + +`_BaseGLM` inherits from the `_BaseRegressor` (detailed in the [`base_class` module](02-base_class.md)). This inheritance provides `_BaseGLM` with a suite of auxiliary methods for handling and validating model inputs. Through abstraction mechanism inherited from `_BaseRegressor`, any GLM subclass is compelled to reimplement the `fit`, `predict`, `score`, and `simulate` methods facilitating compatibility with `scikit-learn`. + +### Attributes + +- **`solver`**: The optimization solver from jaxopt. +- **`solver_state`**: Represents the current state of the solver. +- **`basis_coeff_`**: Holds the solution for spike basis coefficients after the model has been fitted. Initialized to `None` at class instantiation. +- **`baseline_link_fr`**: Contains the bias terms' solutions after fitting. Initialized to `None` at class instantiation. +- **`kwargs`**: Other keyword arguments, like regularization hyperparameters. + + +### Public Methods + +1. **`predict`**: This method checks that the model is fit and validates input consistency and dimensions, and computes mean rates based on the current parameter estimates through the `_predict` method. + +!!! note + `_BaseGLM` lacks concrete implementations for methods like `score`, `fit`, and `simulate`. This is because the specifics of these methods depend on the chosen emission probability. For instance, the scoring method for a Poisson GLM will differ from a Gamma GLM, given their distinct likelihood functions. + +### Private Methods + +1. **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameter names are likely to be GLM specific. +2. **`_predict`**: Predicts firing rates given predictors and parameters. +3. **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. +4. **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. +5. **`_safe_fit`**: Fit the GLM to the neural activity. Checks that the input dimensions and types matches expected one, runs the `jaxopt` optimizer on the loss function provided by a concrete GLM subclass. +6. **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. + + +!!! note + The introduction of `_safe_score` and `_safe_simulate` offers notable benefits: + + 1. It eliminates the need for subclasses to redo checks in their `score` and `simulate` methods, leading to concise code. + 2. The methods `score` and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This mandates alignment with the `scikit-learn` API and ensures subclass-specific docstrings. + +### Abstract Methods +On top of the abstract methods inherited from `_BaseRegressor`, `_BaseGLM` implements, + +1. **`residual_deviance`**: Computes the residual deviance for a GLM model. The deviance, on par with the likelihood, is model specific. + +!!! note + The residual deviance can be written as a function of the log-likelihood. This allows for a concrete implementation of it in the `_BaseGLM`, however the subclass specific implementation can be more robust and/or efficient. + +## The Concrete Class `PoissonGLM` + +The class `PoissonGLM` is a concrete implementation of an un-regularized Poisson GLM model. + +### Inheritance + +`PoissonGLM` inherits from `_BaseGLM`, which provides methods for predicting firing rates and "safe" methods to score and simulate spike trains. Inheritance enforce the concrete implementation of `fit`, `score`, `simulate`, and `residual_deviance`. + +### Attributes + +- **`solver`**: The optimization solver from jaxopt. +- **`solver_state`**: Represents the current state of the solver. +- **`basis_coeff_`**: Holds the solution for spike basis coefficients after the model has been fitted. Initialized to `None` at class instantiation. +- **`baseline_link_fr`**: Contains the bias terms' solutions after fitting. Initialized to `None` at class instantiation. + + +### Public Methods + +- **`score`**: Score the Poisson GLM by either computing the log-likelihood or the pseudo-$R^2$. It calls the superclass `_safe_score` method that implements checks on the provided inputs and parameters. +- **`fit`**: Fit the Poisson GLM to some spike trains by calling the superclass `_safe_fit` method passing the Poisson negative log-likelihood as a loss function. +- **`residual_deviance`**: Compute the residual deviance of each observation for a Poisson model given predicted rates and spike counts. +- **`simulate`**: Simulates spike trains using the GLM as a recurrent network by calling the superclass `_safe_simulate` method, passing `jax.random.poisson` as emission probability function. + +### Private Methods + +- **`_score`**: Computes the Poisson negative log-likelihood up to a normalization constant. This method is used to define the optimization loss function for the model. + +## Contributor Guidelines + +### Implementing Model Subclasses + +To write a usable (i.e. concrete) GLM class you + +- **Must** inherit `_BaseGLM` or any of its subclasses. +- **Must** realize the methods `fit`, `score`, `simulate`, and `residual_deviance`. This means either implementing it directly, or inheriting it from a `_BaseGLM` subclass. +- **Should** call `_safe_fit`, `_safe_score`, `_safe_simulate` in your realization of the `fit`, `score` and `simulate` methods. +- **Should not** overwrite `_safe_fit`, `_safe_score`, `_safe_simulate`. +- **May** implement additional checks on the parameters and input if required by the GLM subclass. \ No newline at end of file diff --git a/docs/developers_notes/README.md b/docs/developers_notes/README.md index c58e25bb..4576ea60 100644 --- a/docs/developers_notes/README.md +++ b/docs/developers_notes/README.md @@ -2,6 +2,12 @@ Welcome to the Developer Notes of the `neurostatslib` project. These notes aim to provide detailed technical information about the various modules, classes, and functions that make up this library, as well as guidelines on how to write code that integrates nicely with our package. They are intended to help current and future developers understand the design decisions, structure, and functioning of the library, and to provide guidance on how to modify, extend, and maintain the codebase. +## Index + +#### - [The `basis` Module](01-basis_module.md) +#### - [The `base_class` Module](02-base_class.md) +#### - [The `glm` Module](03-glm.md) + ## Intended Audience These notes are primarily intended for the following groups: diff --git a/docs/developers_notes/glm.md b/docs/developers_notes/glm.md deleted file mode 100644 index bbcfee4f..00000000 --- a/docs/developers_notes/glm.md +++ /dev/null @@ -1,63 +0,0 @@ -# The `glm` Module - -## Introduction - -The `neurostatslib.glm` basis module implements variations of Generalized Linear Models (GLMs) classes. - -At stage, the module consists of two classes: - -1. **`_BaseGLM`:** An abstract class serving as the backbone for building GLM variations. -2. **`PoissonGLM`:** A concrete implementation of the GLM for Poisson-distributed data. - -We followed the `scikit-learn` api, making the concrete GLM model classes compatible with the powerful `scikit-learn` pipeline and cross-validation modules. - -## The class `_BaseGLM` - -The class `_BaseGLM` is designed to follow the `scikit-learn` api in order to guarantee compatibility with the `scikit-learn` pipelines, as well as to implement all the computation that is shared by the different `GLM` subclasses. - -### Inheritance - -`_BaseGLM` inherits from the `_BaseRegressor` (detailed in the [`base_class` module](base_class.md)). This inheritance provides `_BaseGLM` with a suite of auxiliary methods for handling and validating model inputs. Through abstraction mechanism inherited from `_BaseRegressor`, any GLM subclass is compelled to reimplement the `fit`, `predict`, `score`, and `simulate` methods facilitating compatibility with `scikit-learn`. - -### Attributes - -- **solver_name**: Name of the solver to use when fitting the GLM. It should be an attribute of `jaxopt`. -- **solver_kwargs**: Dictionary containing keyword arguments for initializing the solver. -- **inverse_link_function**: The link function of the GLM. It must be callable and return non-negative values. -- **kwargs**: Other keyword arguments, like regularization hyperparameters. - - -### Public Methods - -1. **`predict`**: This method checks that the model is fit and validates input consistency and dimensions, and computes mean rates based on the current parameter estimates through the `_predict` method. - -!!! note - `_BaseGLM` lacks concrete implementations for methods like `score`, `fit`, and `simulate`. This is because the specifics of these methods depend on the chosen emission probability. For instance, the scoring method for a Poisson GLM will differ from a Gamma GLM, given their distinct likelihood functions. - -### Private Methods - -1. **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameter names are likely to be GLM specific. -2. **`_predict`**: Predicts firing rates given predictors and parameters. -3. **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. -4. **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. -5. **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. - - -!!! note - The introduction of `_safe_score` and `_safe_simulate` offers notable benefits: - - 1. It eliminates the need for subclasses to redo checks in their `score` and `simulate` methods, leading to concise code. - 2. The methods `score` and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This mandates alignment with the `scikit-learn` API and ensures subclass-specific docstrings. - -### Abstract Methods -On top of the abstract methods inherited from `_BaseRegressor`, `_BaseGLM` implements, - -1. **`residual_deviance`**: Computes the residual deviance for a GLM model. The deviance, on par with the likelihood, is model specific. - -!!! note - The residual deviance can be written as a function of the log-likelihood. This allows for a concrete implementation of it in the `_BaseGLM`, however the subclass specific implementation can be more robust and/or efficient. - -## The Concrete Class `PoissonGLM` - -The class `PoissonGLM` implements an un-regularized Poisson GLM model. - diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 2b842343..a7a2cd3a 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -3,7 +3,7 @@ import abc import inspect import warnings -from typing import Callable, Literal, Optional, Tuple, Union +from typing import Callable, Literal, Optional, Tuple, Union, Any import jax import jax.numpy as jnp @@ -314,6 +314,36 @@ def _safe_score( ) return score + def _safe_fit( + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + loss: Callable[[Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32], + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + ): + + # convert to jnp.ndarray & perform checks + X, y, init_params = self._preprocess_fit(X, y, init_params) + + # Run optimization + solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) + params, state = solver.run(init_params, X=X, y=y) + + if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): + raise ValueError( + "Solver returned at least one NaN parameter, so solution is invalid!" + " Try tuning optimization hyperparameters." + ) + + # Store parameters + self.basis_coeff_ = params[0] + self.baseline_link_fr_ = params[1] + # note that this will include an error value, which is not the same as + # the output of loss. I believe it's the output of + # solver.l2_optimality_error + self.solver_state = state + self.solver = solver + def _safe_simulate( self, random_key: jax.random.PRNGKeyArray, @@ -526,7 +556,7 @@ class PoissonGLM(_BaseGLM): state of the solver, set during ``fit()`` basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) Solutions for the spike basis coefficients, set during ``fit()`` - baseline_log_fr : jnp.ndarray, (n_neurons,) + baseline_link_fr : jnp.ndarray, (n_neurons,) Solutions for bias terms, set during ``fit()`` """ @@ -743,7 +773,7 @@ def fit( """Fit GLM to spiking data. Following scikit-learn API, the solutions are stored as attributes - ``basis_coeff_`` and ``baseline_log_fr``. + ``basis_coeff_`` and ``baseline_link_fr``. Parameters ---------- @@ -771,28 +801,10 @@ def fit( """ - X, y, init_params = self._preprocess_fit(X, y, init_params) - def loss(params, X, y): return self._score(X, y, params) - # Run optimization - solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) - params, state = solver.run(init_params, X=X, y=y) - - if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): - raise ValueError( - "Solver returned at least one NaN parameter, so solution is invalid!" - " Try tuning optimization hyperparameters." - ) - # Store parameters - self.basis_coeff_ = params[0] - self.baseline_link_fr_ = params[1] - # note that this will include an error value, which is not the same as - # the output of loss. I believe it's the output of - # solver.l2_optimality_error - self.solver_state = state - self.solver = solver + self._safe_fit(X=X, y=y, loss=loss, init_params=init_params) def simulate( self, From 9c6e1357edb2f7de9c535b0a235dfc4e010cc3b5 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 09:38:24 -0600 Subject: [PATCH 039/250] linted glm.py, removed basic_test.py --- src/neurostatslib/glm.py | 15 +++++++------- tests/basic_test.py | 43 ---------------------------------------- 2 files changed, 8 insertions(+), 50 deletions(-) delete mode 100644 tests/basic_test.py diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index a7a2cd3a..0c067ad9 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -3,7 +3,7 @@ import abc import inspect import warnings -from typing import Callable, Literal, Optional, Tuple, Union, Any +from typing import Callable, Literal, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -315,13 +315,14 @@ def _safe_score( return score def _safe_fit( - self, - X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray], - loss: Callable[[Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32], - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + loss: Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32 + ], + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ): - # convert to jnp.ndarray & perform checks X, y, init_params = self._preprocess_fit(X, y, init_params) diff --git a/tests/basic_test.py b/tests/basic_test.py deleted file mode 100644 index a503f0ae..00000000 --- a/tests/basic_test.py +++ /dev/null @@ -1,43 +0,0 @@ -import jax.numpy as jnp -import matplotlib.pylab as plt -import numpy as np -import scipy.stats as sts -from jax import grad -from scipy.optimize import minimize -from sklearn.linear_model import PoissonRegressor - -from neurostatslib.glm import PoissonGLM - -np.random.seed(100) - -nn, nt, ws, nb,nbi = 2, 15000, 30, 5, 0 -X = np.random.normal(size=(nt, nn, nb*nn+nbi)) -W_true = np.random.normal(size=(nn, nb*nn+nbi)) * 0.8 -b_true = -3*np.ones(nn) -firing_rate = np.exp(np.einsum("ik,tik->ti", W_true, X) + b_true[None, :]) -spikes = np.random.poisson(firing_rate) - -# check likelihood -poiss_rand = sts.poisson(firing_rate) -mean_ll = poiss_rand.logpmf(spikes).mean() - -# SKL FIT -weights_skl = np.zeros((nn, nb*nn+nbi)) -b_skl = np.zeros(nn) -pred_skl = np.zeros((nt,nn)) -for k in range(nn): - model_skl = PoissonRegressor(alpha=0.,tol=10**-8,solver="lbfgs",max_iter=1000,fit_intercept=True) - model_skl.fit(X[:,k,:], spikes[:, k]) - weights_skl[k] = model_skl.coef_ - b_skl[k] = model_skl.intercept_ - pred_skl[:, k] = model_skl.predict(X[:, k,:]) - - -model_jax = PoissonGLM(score_type="pseudo-r2",solver_name="BFGS", - solver_kwargs={'jit':True, 'tol': 10**-8, 'maxiter':1000}, - inverse_link_function=jnp.exp) -model_jax.fit(X, spikes) -mean_ll_jax = model_jax._score(X, spikes, (W_true, b_true)) -firing_rate_jax = model_jax._predict((W_true, b_true),X) - -print('jax pars - skl pars:', np.max(np.abs(model_jax.basis_coeff_ - weights_skl))) \ No newline at end of file From 80b067658fdc26d383564278f5eb393ac035a3d9 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 10:32:28 -0600 Subject: [PATCH 040/250] reviewed glm note --- docs/developers_notes/03-glm.md | 51 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index a401e0a0..b37cf83d 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -2,22 +2,21 @@ ## Introduction -The `neurostatslib.glm` basis module implements variations of Generalized Linear Models (GLMs) classes. +The `neurostatslib.glm` module implements variations of Generalized Linear Models (GLMs) classes. -At stage, the module consists of two classes: +At this stage, the module consists of two primary classes: -1. **`_BaseGLM`:** An abstract class serving as the backbone for building GLM variations. +1. **`_BaseGLM`:** An abstract class serving as the backbone for building GLMs. 2. **`PoissonGLM`:** A concrete implementation of the GLM for Poisson-distributed data. -We followed the `scikit-learn` api, making the concrete GLM model classes compatible with the powerful `scikit-learn` pipeline and cross-validation modules. +Our design aligns with the `scikit-learn` API. This ensures that our GLM classes integrate seamlessly with the robust `scikit-learn` pipeline and its cross-validation capabilities. ## The class `_BaseGLM` -The class `_BaseGLM` is designed to follow the `scikit-learn` api in order to guarantee compatibility with the `scikit-learn` pipelines, as well as to implement all the computation that is shared by the different `GLM` subclasses. - +Designed with `scikit-learn` compatibility in mind, `_BaseGLM` provides the common computations and functionalities needed by the diverse `GLM` subclasses. ### Inheritance -`_BaseGLM` inherits from the `_BaseRegressor` (detailed in the [`base_class` module](02-base_class.md)). This inheritance provides `_BaseGLM` with a suite of auxiliary methods for handling and validating model inputs. Through abstraction mechanism inherited from `_BaseRegressor`, any GLM subclass is compelled to reimplement the `fit`, `predict`, `score`, and `simulate` methods facilitating compatibility with `scikit-learn`. +The `_BaseGLM` inherits attributes and methods from the `_BaseRegressor`, as detailed in the [`base_class` module](02-base_class.md). This grants `_BaseGLM` a toolkit for managing and verifying model inputs. Leveraging the inherited abstraction, all GLM subclasses must explicitly define the `fit`, `predict`, `score`, and `simulate` methods, ensuring alignment with the `scikit-learn` framework. ### Attributes @@ -30,18 +29,18 @@ The class `_BaseGLM` is designed to follow the `scikit-learn` api in order to gu ### Public Methods -1. **`predict`**: This method checks that the model is fit and validates input consistency and dimensions, and computes mean rates based on the current parameter estimates through the `_predict` method. +1. **`predict`**: Validates the model's fit status and input consistency before calculating mean rates using the `_predict` method. !!! note - `_BaseGLM` lacks concrete implementations for methods like `score`, `fit`, and `simulate`. This is because the specifics of these methods depend on the chosen emission probability. For instance, the scoring method for a Poisson GLM will differ from a Gamma GLM, given their distinct likelihood functions. + `_BaseGLM` lacks concrete implementations for methods like `score`, `fit`, and `simulate` since the specific behaviors of these methods are contingent upon their emission probability. For instance, the scoring method for a Poisson GLM will differ from a Gamma GLM, given their distinct likelihood functions. ### Private Methods -1. **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameter names are likely to be GLM specific. -2. **`_predict`**: Predicts firing rates given predictors and parameters. +1. **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameters are likely to be GLM specific. +2. **`_predict`**: Forecasts firing rates based on predictors and parameters. 3. **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. 4. **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. -5. **`_safe_fit`**: Fit the GLM to the neural activity. Checks that the input dimensions and types matches expected one, runs the `jaxopt` optimizer on the loss function provided by a concrete GLM subclass. +5. **`_safe_fit`**: Fit the GLM to the neural activity. Verifies input conformity, then leverages the `jaxopt` optimizer on the designated loss function (provided by the concrete GLM subclass). 6. **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. @@ -49,23 +48,23 @@ The class `_BaseGLM` is designed to follow the `scikit-learn` api in order to gu The introduction of `_safe_score` and `_safe_simulate` offers notable benefits: 1. It eliminates the need for subclasses to redo checks in their `score` and `simulate` methods, leading to concise code. - 2. The methods `score` and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This mandates alignment with the `scikit-learn` API and ensures subclass-specific docstrings. + 2. The methods `score` and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This ensures subclass-specific docstrings for public methods. ### Abstract Methods -On top of the abstract methods inherited from `_BaseRegressor`, `_BaseGLM` implements, +Besides the methods acquired from `_BaseRegressor`, `_BaseGLM` introduces: -1. **`residual_deviance`**: Computes the residual deviance for a GLM model. The deviance, on par with the likelihood, is model specific. +1. **`residual_deviance`**: Computes a GLM's residual deviance. The deviance, on par with the likelihood, is model specific. !!! note - The residual deviance can be written as a function of the log-likelihood. This allows for a concrete implementation of it in the `_BaseGLM`, however the subclass specific implementation can be more robust and/or efficient. + The residual deviance can be formulated as a function of log-likelihood. Although a concrete `_BaseGLM` implementation is feasible, subclass-specific implementations might offer increased robustness or efficiency. ## The Concrete Class `PoissonGLM` -The class `PoissonGLM` is a concrete implementation of an un-regularized Poisson GLM model. +The class `PoissonGLM` is a concrete implementation of the un-regularized Poisson GLM model. ### Inheritance -`PoissonGLM` inherits from `_BaseGLM`, which provides methods for predicting firing rates and "safe" methods to score and simulate spike trains. Inheritance enforce the concrete implementation of `fit`, `score`, `simulate`, and `residual_deviance`. +`PoissonGLM` inherits from `_BaseGLM`, which provides methods for predicting firing rates and "safe" methods to score and simulate spike trains. Inheritance enforces the concrete implementation of `fit`, `score`, `simulate`, and `residual_deviance`. ### Attributes @@ -77,10 +76,10 @@ The class `PoissonGLM` is a concrete implementation of an un-regularized Poisson ### Public Methods -- **`score`**: Score the Poisson GLM by either computing the log-likelihood or the pseudo-$R^2$. It calls the superclass `_safe_score` method that implements checks on the provided inputs and parameters. -- **`fit`**: Fit the Poisson GLM to some spike trains by calling the superclass `_safe_fit` method passing the Poisson negative log-likelihood as a loss function. -- **`residual_deviance`**: Compute the residual deviance of each observation for a Poisson model given predicted rates and spike counts. -- **`simulate`**: Simulates spike trains using the GLM as a recurrent network by calling the superclass `_safe_simulate` method, passing `jax.random.poisson` as emission probability function. +- **`score`**: Scores the Poisson GLM using either log-likelihood or pseudo-$R^2$. It invokes the parent `_safe_score` method to validate input and parameters. +- **`fit`**: Fits the Poisson GLM to align with spike train data by invoking `_safe_fit` and setting Poisson negative log-likelihood as the loss function. +- **`residual_deviance`**: Computes the residual deviance for each Poisson model observation, given predicted rates and spike counts. +- **`simulate`**: Simulates spike trains using the GLM as a recurrent network, invoking `_safe_simulate` and setting `jax.random.poisson` as the emission probability mechanism. ### Private Methods @@ -93,7 +92,7 @@ The class `PoissonGLM` is a concrete implementation of an un-regularized Poisson To write a usable (i.e. concrete) GLM class you - **Must** inherit `_BaseGLM` or any of its subclasses. -- **Must** realize the methods `fit`, `score`, `simulate`, and `residual_deviance`. This means either implementing it directly, or inheriting it from a `_BaseGLM` subclass. -- **Should** call `_safe_fit`, `_safe_score`, `_safe_simulate` in your realization of the `fit`, `score` and `simulate` methods. -- **Should not** overwrite `_safe_fit`, `_safe_score`, `_safe_simulate`. -- **May** implement additional checks on the parameters and input if required by the GLM subclass. \ No newline at end of file +- **Must** implement the `fit`, `score`, `simulate`, and `residual_deviance` methods, either directly or through inheritance. +- **Should** invoke `_safe_fit`, `_safe_score`, and `_safe_simulate` within the `fit`, `score`, and `simulate` methods, respectively. +- **Should not** override `_safe_fit`, `_safe_score`, or `_safe_simulate`. +- **May** integrate supplementary parameter and input checks if mandated by the GLM subclass. \ No newline at end of file From a0bafdc0289d902088dc956e4f820f474806264d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 10:47:40 -0600 Subject: [PATCH 041/250] update docstring for simulate --- src/neurostatslib/glm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 0c067ad9..19643475 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -835,14 +835,14 @@ def simulate( Initial spike counts matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). coupling_basis_matrix : - Basis matrix for coupling, representing inter-neuron effects + Basis matrix for coupling, representing between-neuron couplings and auto-correlations. Expected shape: (window_size, n_basis_coupling). feedforward_input : External input matrix to the model, representing factors like convolved currents, light intensities, etc. When not provided, the simulation is done with coupling-only. Expected shape: (n_timesteps, n_neurons, n_basis_input). device : - Computation device to use ('cpu' or 'gpu'). Default is 'cpu'. + Computation device to use ('cpu', 'gpu' or 'tpu'). Default is 'cpu'. Returns ------- From d349b0d6ba8790014613c86d8101a18d82318c42 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 10:48:48 -0600 Subject: [PATCH 042/250] update docstring for simulate --- src/neurostatslib/glm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 19643475..86868319 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -373,17 +373,17 @@ def _safe_simulate( Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). coupling_basis_matrix : - Basis matrix for coupling, representing inter-neuron effects + Basis matrix for coupling, representing between-neuron couplings and auto-correlations. Expected shape: (window_size, n_basis_coupling). random_function : - A random function, like jax.random.poisson, which takes as input a random.PRNGKeyArray + A probability emission function, like jax.random.poisson, which takes as input a random.PRNGKeyArray and the mean rate, and samples observations, (spike counts for a poisson).. feedforward_input : External input matrix to the model, representing factors like convolved currents, light intensities, etc. When not provided, the simulation is done with coupling-only. Expected shape: (n_timesteps, n_neurons, n_basis_input). device : - Computation device to use ('cpu' or 'gpu'). Default is 'cpu'. + Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. Returns ------- From 5ff37e1560486a64f9678c918f18fa3fe5cbbf33 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 11:02:10 -0600 Subject: [PATCH 043/250] improved structure --- docs/developers_notes/03-glm.md | 31 ++++++++++++++----------------- src/neurostatslib/glm.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index b37cf83d..ede6701e 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -27,33 +27,29 @@ The `_BaseGLM` inherits attributes and methods from the `_BaseRegressor`, as det - **`kwargs`**: Other keyword arguments, like regularization hyperparameters. -### Public Methods - -1. **`predict`**: Validates the model's fit status and input consistency before calculating mean rates using the `_predict` method. - -!!! note - `_BaseGLM` lacks concrete implementations for methods like `score`, `fit`, and `simulate` since the specific behaviors of these methods are contingent upon their emission probability. For instance, the scoring method for a Poisson GLM will differ from a Gamma GLM, given their distinct likelihood functions. - ### Private Methods -1. **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameters are likely to be GLM specific. -2. **`_predict`**: Forecasts firing rates based on predictors and parameters. -3. **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. -4. **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. -5. **`_safe_fit`**: Fit the GLM to the neural activity. Verifies input conformity, then leverages the `jaxopt` optimizer on the designated loss function (provided by the concrete GLM subclass). -6. **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. +- **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameters are likely to be GLM specific. +- **`_predict`**: Forecasts firing rates based on predictors and parameters. +- **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. +- **`_safe_predict`**: Validates the model's fit status and input consistency before calculating mean rates using the `_predict` method. +- **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. +- **`_safe_fit`**: Fit the GLM to the neural activity. Verifies input conformity, then leverages the `jaxopt` optimizer on the designated loss function (provided by the concrete GLM subclass). +- **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. !!! note - The introduction of `_safe_score` and `_safe_simulate` offers notable benefits: + The introduction of `_safe_predict`, `_safe_fit`, `_safe_score` and `_safe_simulate` offers the following benefits: + + 1. It eliminates the need for subclasses to redo checks in their `fit`, `score` and `simulate` methods, leading to concise code. + 2. The methods `predict`, `score`, `fit`, and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This ensures subclass-specific docstrings for public methods. - 1. It eliminates the need for subclasses to redo checks in their `score` and `simulate` methods, leading to concise code. - 2. The methods `score` and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This ensures subclass-specific docstrings for public methods. + While `predict` is common to any GLM, we explicitly omit its implementation in `_BaseGLM` so that the method will be documented in the `Code References` under each concrete class. ### Abstract Methods Besides the methods acquired from `_BaseRegressor`, `_BaseGLM` introduces: -1. **`residual_deviance`**: Computes a GLM's residual deviance. The deviance, on par with the likelihood, is model specific. +- **`residual_deviance`**: Computes a GLM's residual deviance. The deviance, on par with the likelihood, is model specific. !!! note The residual deviance can be formulated as a function of log-likelihood. Although a concrete `_BaseGLM` implementation is feasible, subclass-specific implementations might offer increased robustness or efficiency. @@ -76,6 +72,7 @@ The class `PoissonGLM` is a concrete implementation of the un-regularized Poisso ### Public Methods +- **`predict`**: Calculates mean rates by invoking the `_safe_predict` method of `_BaseGLM`. - **`score`**: Scores the Poisson GLM using either log-likelihood or pseudo-$R^2$. It invokes the parent `_safe_score` method to validate input and parameters. - **`fit`**: Fits the Poisson GLM to align with spike train data by invoking `_safe_fit` and setting Poisson negative log-likelihood as the loss function. - **`residual_deviance`**: Computes the residual deviance for each Poisson model observation, given predicted rates and spike counts. diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 86868319..cddff868 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -170,7 +170,7 @@ def _check_is_fit(self): "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) - def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + def _safe_predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: """Predict firing rates based on fit parameters. Parameters @@ -679,6 +679,37 @@ def residual_deviance( ) return resid_dev + def predict(self, X: Union[NDArray, jnp.ndarray]): + """Predict firing rates based on fit parameters. + + Parameters + ---------- + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features). + + Returns + ------- + predicted_firing_rates : jnp.ndarray + The predicted firing rates with shape (n_neurons, n_time_bins). + + Raises + ------ + NotFittedError + If ``fit`` has not been called first with this instance. + ValueError + - If `params` is not a JAX pytree of size two. + - If weights and bias terms in `params` don't have the expected dimensions. + - If the number of neurons in the model parameters and in the inputs do not match. + - If `X` is not three-dimensional. + - If there's an inconsistent number of features between spike basis coefficients and `X`. + + See Also + -------- + score + Score predicted firing rates against target spike counts. + """ + return self._safe_predict(X) + def score( self, X: Union[NDArray, jnp.ndarray], From 6a5840e847f548a50b17707bcd86749c381bf293 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 11:08:54 -0600 Subject: [PATCH 044/250] imrpoved docstrings --- docs/developers_notes/03-glm.md | 1 + src/neurostatslib/glm.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index ede6701e..3d325276 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -14,6 +14,7 @@ Our design aligns with the `scikit-learn` API. This ensures that our GLM classes ## The class `_BaseGLM` Designed with `scikit-learn` compatibility in mind, `_BaseGLM` provides the common computations and functionalities needed by the diverse `GLM` subclasses. + ### Inheritance The `_BaseGLM` inherits attributes and methods from the `_BaseRegressor`, as detailed in the [`base_class` module](02-base_class.md). This grants `_BaseGLM` a toolkit for managing and verifying model inputs. Leveraging the inherited abstraction, all GLM subclasses must explicitly define the `fit`, `predict`, `score`, and `simulate` methods, ensuring alignment with the `scikit-learn` framework. diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index cddff868..d5c33c0d 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -705,7 +705,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]): See Also -------- - score + [score](../glm/#neurostatslib.glm.PoissonGLM.score) Score predicted firing rates against target spike counts. """ return self._safe_predict(X) @@ -897,7 +897,7 @@ def simulate( See Also -------- - predict : Method to predict firing rates based on the model's parameters. + [predict](../glm/#neurostatslib.glm.PoissonGLM.predict) : Method to predict firing rates based on the model's parameters. Notes ----- From 29a8dfdf16ec398210175bdd5bd6c26bb767f9c1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 29 Aug 2023 11:12:24 -0600 Subject: [PATCH 045/250] linted --- src/neurostatslib/glm.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index d5c33c0d..a1e3e2f5 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -897,7 +897,8 @@ def simulate( See Also -------- - [predict](../glm/#neurostatslib.glm.PoissonGLM.predict) : Method to predict firing rates based on the model's parameters. + [predict](../glm/#neurostatslib.glm.PoissonGLM.predict) : Method to predict firing rates based on + the model's parameters. Notes ----- From 32277b53238b7e64093245c485209d441dcaf4b8 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 31 Aug 2023 16:11:25 -0600 Subject: [PATCH 046/250] added device check on fit --- docs/developers_notes/02-base_class.md | 2 + src/neurostatslib/base_class.py | 58 ++++++++++++++++++++- src/neurostatslib/glm.py | 72 +++++++++++++++++++------- tests/test_glm.py | 30 ++++++++++- 4 files changed, 139 insertions(+), 23 deletions(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index b9275410..5ba00548 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -53,6 +53,8 @@ For a detailed understanding, consult the [`scikit-learn` API Reference](https:/ - **`get_params`**: The `get_params` method retrieves parameters set during model instance initialization. Opting for a deep inspection allows the method to assess nested object parameters, resulting in a comprehensive parameter dictionary. - **`set_params`**: The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. +- **`select_target_device`**: Selects either "cpu", "gpu" or "tpu" as the device. If not found, rolls back to "cpu". +- **`device_put`**: Sends arrays to device, if not on device already. ## The Abstract Class `model_base._BaseRegressor` diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 3e4b6487..715a97d2 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -5,12 +5,15 @@ import inspect import warnings from collections import defaultdict -from typing import Literal, Optional, Tuple, Union +from typing import Any, Literal, Optional, Tuple, Union import jax import jax.numpy as jnp +from jax._src.lib import xla_client from numpy.typing import ArrayLike, NDArray +from .utils import has_local_device + class _Base: def __init__(self, **kwargs): @@ -105,6 +108,59 @@ def set_params(self, **params): return self + @staticmethod + def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Device: + """Select a device + + Parameters + ---------- + device + A device between "cpu", "gpu" or "tpu". Rolls back to "cpu" if device is not found. + + Returns + ------- + The selected device. + """ + if device == "cpu": + target_device = jax.devices(device)[0] + elif (device == "gpu") or (device == "tpu"): + if has_local_device(device): + # assume for now 1 gpu/tpu (no further parallelization) + target_device = jax.devices(device)[0] + else: + warnings.warn(f"No {device.upper()} found! Falling back to CPU") + target_device = jax.devices("cpu")[0] + else: + raise ValueError( + f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`." + ) + return target_device + + @staticmethod + def device_put( + *args: jnp.ndarray, device: xla_client.Device + ) -> Union[Any, jnp.ndarray]: + """Send arrays to device. + + This function sends the arrays to the target devices, if the arrays are + not already there. + + Parameters + ---------- + *args: + NDArray + device: + A target device, such as that returned by `select_target_device`. + Returns + ------- + : + The arrays on the desired device. + """ + return tuple( + jax.device_put(arg, device) if arg.device_buffer.device() != device else arg + for arg in args + ) + @classmethod def _get_param_names(cls): """Get parameter names for the estimator""" diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index a1e3e2f5..f57598e5 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -2,7 +2,6 @@ """ import abc import inspect -import warnings from typing import Callable, Literal, Optional, Tuple, Union import jax @@ -12,7 +11,7 @@ from .base_class import _BaseRegressor from .exceptions import NotFittedError -from .utils import convolve_1d_trials, has_local_device +from .utils import convolve_1d_trials class _BaseGLM(_BaseRegressor, abc.ABC): @@ -322,10 +321,48 @@ def _safe_fit( [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32 ], init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "gpu", ): + """Fit GLM to neuroal activity. + + Following scikit-learn API, the solutions are stored as attributes + ``basis_coeff_`` and ``baseline_link_fr``. + + Parameters + ---------- + X : + Predictors, shape (n_time_bins, n_neurons, n_features) + y : + Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). + loss: + The loss function to be minimized. + init_params : + Initial values for the spike basis coefficients and bias terms. If + None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) + device: + Device used for optimizing model parameters. + Raises + ------ + ValueError + - If `init_params` is not of length two. + - If dimensionality of `init_params` are not correct. + - If the number of neurons in the model parameters and in the inputs do not match. + - If `X` is not three-dimensional. + - If spike_data is not two-dimensional. + - If solver returns at least one NaN parameter, which means it found + an invalid solution. Try tuning optimization hyperparameters. + TypeError + - If `init_params` are not array-like + - If `init_params[i]` cannot be converted to jnp.ndarray for all i + """ # convert to jnp.ndarray & perform checks X, y, init_params = self._preprocess_fit(X, y, init_params) + # send to device + target_device = self.select_target_device(device) + X, y = self.device_put(X, y, device=target_device) + init_params = self.device_put(*init_params, device=target_device) + # Run optimization solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) params, state = solver.run(init_params, X=X, y=y) @@ -421,26 +458,19 @@ def _safe_simulate( The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure consistency in the model's input feature dimensionality. """ - if device == "cpu": - target_device = jax.devices(device)[0] - elif (device == "gpu") or (device == "tpu"): - if has_local_device(device): - # assume for now 1 gpu/tpu (no further parallelization) - target_device = jax.devices(device)[0] - else: - warnings.warn(f"No {device.upper()} found! Falling back to CPU") - target_device = jax.devices("cpu")[0] - else: - raise ValueError( - f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`." - ) + target_device = self.select_target_device(device) # check if the model is fit self._check_is_fit() + # convert to jnp.ndarray + init_y, coupling_basis_matrix, feedforward_input = self._convert_to_jnp_ndarray( + init_y, coupling_basis_matrix, feedforward_input, data_type=jnp.float32 + ) + # Transfer data to the target device - init_y = jax.device_put(init_y, target_device) - coupling_basis_matrix = jax.device_put(coupling_basis_matrix, target_device) - feedforward_input = jax.device_put(feedforward_input, target_device) + init_y, coupling_basis_matrix, feedforward_input = self.device_put( + init_y, coupling_basis_matrix, feedforward_input, device=target_device + ) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] @@ -801,6 +831,7 @@ def fit( X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "gpu", ): """Fit GLM to spiking data. @@ -816,7 +847,8 @@ def fit( init_params : Initial values for the spike basis coefficients and bias terms. If None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) - + device: + Device used for optimizing model parameters. Raises ------ ValueError @@ -836,7 +868,7 @@ def fit( def loss(params, X, y): return self._score(X, y, params) - self._safe_fit(X=X, y=y, loss=loss, init_params=init_params) + self._safe_fit(X=X, y=y, loss=loss, init_params=init_params, device=device) def simulate( self, diff --git a/tests/test_glm.py b/tests/test_glm.py index a5d63c7e..c04134a7 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -400,6 +400,32 @@ def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): else: model.fit(X, y, init_params=(init_w, init_b)) + @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none", 1]) + def test_fit_device_spec(self, device_spec, + poissonGLM_model_instantiation): + """ + Test `simulate` across different device specifications. + Validates if unsupported or absent devices raise exception + or warning respectively. + """ + raise_exception = not (device_spec in ["cpu", "tpu", "gpu"]) + raise_warning = all(device_spec != device.device_kind.lower() + for device in jax.local_devices()) + raise_warning = raise_warning and (not raise_exception) + + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + n_samples, n_neurons, n_features = X.shape + init_w = jnp.zeros((n_neurons, n_features)) + init_b = jnp.zeros((n_neurons,)) + if raise_exception: + with pytest.raises(ValueError, match=f"Invalid device specification: {device_spec}"): + model.fit(X, y, init_params=(init_w, init_b), device=device_spec) + elif raise_warning: + with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): + model.fit(X, y, init_params=(init_w, init_b), device=device_spec) + else: + model.fit(X, y, init_params=(init_w, init_b), device=device_spec) + ####################### # Test model.score ####################### @@ -1009,8 +1035,8 @@ def test_simulate_input_timepoints(self, delta_tp, feedforward_input=feedforward_input, device="cpu") - @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none"]) - def test_simulate_device_tspec(self, device_spec, + @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none", 1]) + def test_simulate_device_spec(self, device_spec, poissonGLM_coupled_model_config_simulate): """ Test `simulate` across different device specifications. From 95675468ffff3e0af9720f80435fcce7cdc9127e Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 6 Sep 2023 12:53:08 -0400 Subject: [PATCH 047/250] Update 02-base_class.md improved clarity --- docs/developers_notes/02-base_class.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 5ba00548..d5d85934 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -66,7 +66,7 @@ For subclasses derived from `_BaseRegressor` to function correctly, they must im 1. `fit`: Adapt the model using input data `X` and corresponding observations `y`. 2. `predict`: Provide predictions based on the trained model and input data `X`. -3. `score`: Gauge the accuracy of model predictions using input data `X` against the actual observations `y`. +3. `score`: Score the accuracy of model predictions using input data `X` against the actual observations `y`. 4. `simulate`: Simulate data based on the trained regression model. Moreover, `_BaseRegressor` incorporates auxiliary methods such as `_convert_to_jnp_ndarray`, `_has_invalid_entry` From d6458b2c116e859e58916ef3737dce3201c85eab Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 13:01:44 -0400 Subject: [PATCH 048/250] improved exception messages --- src/neurostatslib/base_class.py | 4 ++-- tests/test_glm.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 715a97d2..cff3f2a3 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -292,12 +292,12 @@ def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, ...]: if params[0].ndim != 2: raise ValueError( - "params[0] term must be of shape (n_neurons, n_features), but" + "params[0] must be of shape (n_neurons, n_features), but" f"params[0] has {params[0].ndim} dimensions!" ) if params[1].ndim != 1: raise ValueError( - "params[1] term must be of shape (n_neurons,) but " + "params[1] must be of shape (n_neurons,) but " f"params[1] has {params[1].ndim} dimensions!" ) return params diff --git a/tests/test_glm.py b/tests/test_glm.py index c04134a7..fa99643d 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -147,7 +147,7 @@ def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instanti init_b = jnp.log(y.mean(axis=0)) raise_exception = dim_weights != 2 if raise_exception: - with pytest.raises(ValueError, match="params\[0\] term must be of shape \(n_neurons, n_features\)"): + with pytest.raises(ValueError, match="params\[0\] must be of shape \(n_neurons, n_features\)"): model.fit(X, y, init_params=(init_w, init_b)) else: model.fit(X, y, init_params=(init_w, init_b)) @@ -164,7 +164,7 @@ def test_fit_intercepts_dimensionality(self, dim_intercepts, poissonGLM_model_in init_w = jnp.zeros((n_neurons, n_features)) raise_exception = dim_intercepts != 1 if raise_exception: - with pytest.raises(ValueError, match="params\[1\] term must be of shape"): + with pytest.raises(ValueError, match="params\[1\] must be of shape"): model.fit(X, y, init_params=(init_w, init_b)) else: model.fit(X, y, init_params=(init_w, init_b)) From 31d1b855c4d26f73e3fdba6d674eded2cc717780 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 13:06:55 -0400 Subject: [PATCH 049/250] flexible data_type --- src/neurostatslib/base_class.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index cff3f2a3..0c2c08de 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -268,7 +268,9 @@ def _has_invalid_entry(array: jnp.ndarray) -> bool: return (jnp.isinf(array) | jnp.isnan(array)).any() @staticmethod - def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, ...]: + def _check_and_convert_params(params: ArrayLike, + data_type: jnp.dtype = jnp.float32 + ) -> Tuple[jnp.ndarray, ...]: """ Validate the dimensions and consistency of parameters and data. @@ -280,7 +282,7 @@ def _check_and_convert_params(params: ArrayLike) -> Tuple[jnp.ndarray, ...]: if not hasattr(params, "__getitem__"): raise TypeError("Initial parameters must be array-like!") try: - params = tuple(jnp.asarray(par, dtype=jnp.float32) for par in params) + params = tuple(jnp.asarray(par, dtype=data_type) for par in params) except ValueError: raise TypeError( "Initial parameters must be array-like of array-like objects" From a9c2a9ece5d3fa9d9e3cbac3d16f9b211afe8e6e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 13:15:16 -0400 Subject: [PATCH 050/250] linted --- src/neurostatslib/base_class.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 0c2c08de..9f51b8d5 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -268,9 +268,9 @@ def _has_invalid_entry(array: jnp.ndarray) -> bool: return (jnp.isinf(array) | jnp.isnan(array)).any() @staticmethod - def _check_and_convert_params(params: ArrayLike, - data_type: jnp.dtype = jnp.float32 - ) -> Tuple[jnp.ndarray, ...]: + def _check_and_convert_params( + params: ArrayLike, data_type: jnp.dtype = jnp.float32 + ) -> Tuple[jnp.ndarray, ...]: """ Validate the dimensions and consistency of parameters and data. From f61b1f7cfbec2dca79b2c55343df32fe9dcdbc9e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 13:33:19 -0400 Subject: [PATCH 051/250] data_type as parameter --- src/neurostatslib/base_class.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 9f51b8d5..7f7d6c5a 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -383,6 +383,7 @@ def _preprocess_fit( X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, + data_type: jnp.dtype = jnp.float32 ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: """Preprocess input data and initial parameters for the fit method. @@ -401,7 +402,8 @@ def _preprocess_fit( Target values, expected to be of shape (n_timebins, n_neurons). init_params : Initial parameters for the model. If None, they are initialized with default values. - + data_type : + Data type to convert to. Default is jnp.float32. Returns ------- : @@ -414,7 +416,7 @@ def _preprocess_fit( """ # convert to jnp.ndarray of float32 - X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) + X, y = self._convert_to_jnp_ndarray(X, y, data_type=data_type) # check input dimensionality self._check_input_dimensionality(X, y) From 3034a9a0e77a4385b14fb85c3ee13f45462be1af Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 13:34:11 -0400 Subject: [PATCH 052/250] linted --- src/neurostatslib/base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 7f7d6c5a..18ad6748 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -383,7 +383,7 @@ def _preprocess_fit( X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, - data_type: jnp.dtype = jnp.float32 + data_type: jnp.dtype = jnp.float32, ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: """Preprocess input data and initial parameters for the fit method. From aec419e09b4039cd226b405f7f89840ffcfd5a3c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 13:36:57 -0400 Subject: [PATCH 053/250] improved docstrings --- src/neurostatslib/base_class.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 18ad6748..ce3a3f1f 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -406,8 +406,12 @@ def _preprocess_fit( Data type to convert to. Default is jnp.float32. Returns ------- - : - Preprocessed input data `X`, target values `y`, and initialized parameters. + X : + Preprocessed input data `X` converted to jnp.ndarray with the desired floating point precision. + y : + Target values `y` converted to jnp.ndarray with the desired floating point precision + init_params : + Initialized parameters converted to jnp.ndarray with the desired floating point precision. Raises ------ From 0d5e65a18af24acc064ac06c67c0f9a0927763b8 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 14:06:31 -0400 Subject: [PATCH 054/250] pydoctest linting --- src/neurostatslib/base_class.py | 19 +++++++++++-------- src/neurostatslib/exceptions.py | 4 ++++ src/neurostatslib/glm.py | 14 +++++++------- src/neurostatslib/sample_points.py | 5 ++--- src/neurostatslib/utils.py | 26 +++++++++++++++----------- 5 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index ce3a3f1f..7cde502f 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -1,5 +1,4 @@ -"""Abstract class for models. -""" +"""Abstract class for models.""" import abc import inspect @@ -23,14 +22,17 @@ def __init__(self, **kwargs): def get_params(self, deep=True): """ - from scikit-learn, get parameters by inspecting init + From scikit-learn, get parameters by inspecting init. + Parameters ---------- deep Returns ------- - + out: + A dictionary containing the parameters. Key is the parameter + name, value is the parameter value. """ out = dict() for key in self._get_param_names(): @@ -110,7 +112,7 @@ def set_params(self, **params): @staticmethod def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Device: - """Select a device + """Select a device. Parameters ---------- @@ -151,6 +153,7 @@ def device_put( NDArray device: A target device, such as that returned by `select_target_device`. + Returns ------- : @@ -163,7 +166,7 @@ def device_put( @classmethod def _get_param_names(cls): - """Get parameter names for the estimator""" + """Get parameter names for the estimator.""" # fetch the constructor or the original constructor before # deprecation wrapping if any init = getattr(cls.__init__, "deprecated_original", cls.__init__) @@ -328,7 +331,7 @@ def _check_input_and_params_consistency( """ Validate the number of neurons in model parameters and input arguments. - Raises: + Raises ------ ValueError - if the number of neurons is consistent across the model parameters (`params`) and @@ -404,6 +407,7 @@ def _preprocess_fit( Initial parameters for the model. If None, they are initialized with default values. data_type : Data type to convert to. Default is jnp.float32. + Returns ------- X : @@ -418,7 +422,6 @@ def _preprocess_fit( ValueError If there are inconsistencies in the input shapes or if NaNs or Infs are detected. """ - # convert to jnp.ndarray of float32 X, y = self._convert_to_jnp_ndarray(X, y, data_type=data_type) diff --git a/src/neurostatslib/exceptions.py b/src/neurostatslib/exceptions.py index 9b925b90..82265fbf 100644 --- a/src/neurostatslib/exceptions.py +++ b/src/neurostatslib/exceptions.py @@ -1,3 +1,6 @@ +"""Model specific exceptions.""" + + class NotFittedError(ValueError, AttributeError): """Exception class to raise if estimator is used before fitting. @@ -12,6 +15,7 @@ class NotFittedError(ValueError, AttributeError): ... PoissonGLM().predict([[[1, 2], [2, 3], [3, 4]]]) ... except NotFittedError as e: ... print(repr(e)) + NotFittedError("This GLM instance is not fitted yet. Call 'fit' with appropriate arguments.") diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index f57598e5..d7ef39c0 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,5 +1,4 @@ -"""GLM core module -""" +"""GLM core module.""" import abc import inspect from typing import Callable, Literal, Optional, Tuple, Union @@ -253,6 +252,7 @@ def _safe_score( - `log-likelihood` for the model log-likelihood. - `pseudo-r2` for the model pseudo-$R^2$. Default is defined at class initialization. + Returns ------- score : (1,) @@ -285,7 +285,6 @@ def _safe_score( Routledge, 2013. """ - # ignore the last time point from predict, because that corresponds to # the next time step, which we have no observed data for self._check_is_fit() @@ -341,6 +340,7 @@ def _safe_fit( None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) device: Device used for optimizing model parameters. + Raises ------ ValueError @@ -435,8 +435,6 @@ def _safe_simulate( ------ NotFittedError If the model hasn't been fitted prior to calling this method. - Raises - ------ ValueError - If the instance has not been previously fitted. - If there's an inconsistency between the number of neurons in model parameters. @@ -524,7 +522,7 @@ def _safe_simulate( def scan_fn( data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: - """Function to scan over time steps and simulate spikes and firing rates. + """Scan over time steps and simulate spikes and firing rates. This function simulates the spikes and firing rates for each time step based on the previous spike data, feedforward input, and model coefficients. @@ -611,7 +609,7 @@ def _score( target_spikes: jnp.ndarray, params: Tuple[jnp.ndarray, jnp.ndarray], ) -> jnp.ndarray: - """Score the predicted firing rates against target spike counts. + r"""Score the predicted firing rates against target spike counts. This computes the Poisson negative log-likelihood up to a constant. @@ -790,6 +788,7 @@ def score( - `log-likelihood` for the model log-likelihood. - `pseudo-r2` for the model pseudo-$R^2$. Default is defined at class initialization. + Returns ------- score : @@ -849,6 +848,7 @@ def fit( None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) device: Device used for optimizing model parameters. + Raises ------ ValueError diff --git a/src/neurostatslib/sample_points.py b/src/neurostatslib/sample_points.py index a0c8a496..fccf5bec 100644 --- a/src/neurostatslib/sample_points.py +++ b/src/neurostatslib/sample_points.py @@ -1,5 +1,4 @@ -"""Helper functions for generating arrays of sample points, for basis functions. -""" +"""Helper functions for generating arrays of sample points, for basis functions.""" import numpy as np from numpy.typing import NDArray @@ -39,7 +38,7 @@ def raised_cosine_log(n_basis_funcs: int, window_size: int) -> NDArray: def raised_cosine_linear(n_basis_funcs: int, window_size: int) -> NDArray: - """Generate linear-spaced sample points for RaisedCosineBasis + """Generate linear-spaced sample points for RaisedCosineBasis. When used with the RaisedCosineBasis, results in evenly (linear) spaced cosine bumps. diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index 8af3335f..bee9a07c 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -1,5 +1,4 @@ -"""Utility functions for data pre-processing -""" +"""Utility functions for data pre-processing.""" # required to get ArrayLike to render correctly, unnecessary as of python 3.10 from __future__ import annotations @@ -52,8 +51,7 @@ def convolve_1d_trials( basis_matrix: ArrayLike, time_series: Union[Iterable[NDArray], NDArray, Iterable[jnp.ndarray], jnp.ndarray], ) -> List[jnp.ndarray]: - """ - Convolve trial time series with a basis matrix. + """Convolve trial time series with a basis matrix. This function checks if all trials have the same duration. If they do, it uses a fast method to convolve all trials with the basis matrix at once. If they do not, it falls back to convolving @@ -82,7 +80,6 @@ def convolve_1d_trials( - If trials_time_series contains empty trials. - If the number of time points in each trial is less than the window size. """ - basis_matrix = jnp.asarray(basis_matrix) # check input size if basis_matrix.ndim != 2: @@ -143,6 +140,7 @@ def _pad_dimension( Add padding to the last dimension of an array based on the convolution type. This is a helper function used by `nan_pad_conv`, which is the function we expect the user will interact with. + Parameters ---------- array: @@ -218,6 +216,7 @@ def nan_pad_conv( ValueError If the window_size is not a positive integer, or if the filter_type is not one of 'causal', 'acausal', or 'anti-causal'. Also raises ValueError if the dimensionality of conv_trials is not as expected. + """ if not isinstance(window_size, int) or window_size <= 0: raise ValueError( @@ -340,9 +339,10 @@ def plot_spike_raster( def row_wise_kron(A: jnp.array, C: jnp.array, jit=False, transpose=True) -> jnp.array: - """ - Compute the row-wise Kronecker product between two matrices using JAX. See [1] - for more details on the Kronecker product. + """Compute the row-wise Kronecker product. + + Compute the row-wise Kronecker product between two matrices using JAX. + See [1] for more details on the Kronecker product. Parameters ---------- @@ -367,8 +367,8 @@ def row_wise_kron(A: jnp.array, C: jnp.array, jit=False, transpose=True) -> jnp. .. [1] Petersen, Kaare Brandt, and Michael Syskind Pedersen. "The matrix cookbook." Technical University of Denmark 7.15 (2008): 510. - """ + """ if transpose: A = A.T C = C.T @@ -386,15 +386,19 @@ def row_wise_kron(a, c): def has_local_device(device_type: str) -> bool: - """ - Scan for local device availability. + """Scan for local device availability. + + Looks for local device availability and returns True if the specified + type of device (e.g., GPU, TPU) is available. Parameters ---------- device_type: The the device type in lower-case, e.g. `gpu`, `tpu`... + Returns ------- + : True if the jax finds the device, False otherwise. """ From 1c543395a0d7d724e074a13b284b76d040aa7dae Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 6 Sep 2023 14:20:06 -0400 Subject: [PATCH 055/250] pydoctest linting --- src/neurostatslib/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index d7ef39c0..7a1aa804 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -414,7 +414,7 @@ def _safe_simulate( and auto-correlations. Expected shape: (window_size, n_basis_coupling). random_function : A probability emission function, like jax.random.poisson, which takes as input a random.PRNGKeyArray - and the mean rate, and samples observations, (spike counts for a poisson).. + and the mean rate, and samples observations, (spike counts for a poisson). feedforward_input : External input matrix to the model, representing factors like convolved currents, light intensities, etc. When not provided, the simulation is done with coupling-only. From c415c68136c1f281584dd7c7ce2f625b05c5f127 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 7 Sep 2023 19:55:01 -0400 Subject: [PATCH 056/250] started loss and obs noise --- src/neurostatslib/loss.py | 206 +++++++++++++++++++++++++ src/neurostatslib/observation_noise.py | 48 ++++++ src/neurostatslib/proximal_operator.py | 66 ++++++++ 3 files changed, 320 insertions(+) create mode 100644 src/neurostatslib/loss.py create mode 100644 src/neurostatslib/observation_noise.py create mode 100644 src/neurostatslib/proximal_operator.py diff --git a/src/neurostatslib/loss.py b/src/neurostatslib/loss.py new file mode 100644 index 00000000..84a76601 --- /dev/null +++ b/src/neurostatslib/loss.py @@ -0,0 +1,206 @@ +import abc +from typing import Callable, Optional, Tuple + +import jax.numpy as jnp +import jaxopt + +from .base_class import _Base +from .proximal_operator import prox_group_lasso + + +class Solver( + jaxopt.GradientDescent, + jaxopt.BFGS, + jaxopt.LBFGS, + jaxopt.ScipyMinimize, + jaxopt.NonlinearCG, + jaxopt.ScipyBoundedMinimize, + jaxopt.LBFGSB, + jaxopt.ProximalGradient, +): + """Class grouping any solver we allow. + + We allow the following solvers: + - Unconstrained: GradientDescent, BFGS, LBFGS, ScipyMinimize, NonlinearCG + - Box-Bounded: ScipyBoundedMinimize, LBFGSB + - Non-Smooth: ProximalGradient + + """ + + +class Regularizer(_Base, abc.ABC): + allowed_solvers = [] + + def __init__(self, alpha: float = 0.0, **kwargs): + super().__init__(**kwargs) + self.alpha = alpha + + def _check_solver(self, solver_name: str): + if solver_name not in self.allowed_solvers: + raise ValueError( + f"Solver `{solver_name}` not allowed for " + f"{self.__class__} regularization. " + f"Allowed solvers are {self.allowed_solvers}." + ) + + @abc.abstractmethod + def instantiate_solver( + self, + solver_name: str, + loss: Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray + ], + solver_kwargs: Optional[dict] = None, + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep + ]: + pass + + +class UnRegularized(Regularizer): + allowed_solvers = [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG" "ScipyBoundedMinimize", + "LBFGSB", + ] + + def __init__(self): + super().__init__() + + def instantiate_solver( + self, + solver_name: str, + loss: Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray + ], + solver_kwargs: Optional[dict] = None, + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep + ]: + self._check_solver(solver_name) + solver = getattr(jaxopt, solver_name)(fun=loss, **solver_kwargs) + + def solver_run( + init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray + ) -> jaxopt.OptStep: + return solver.run(init_params, X=X, y=y) + + return solver_run + + +class Ridge(Regularizer): + allowed_solvers = [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG" "ScipyBoundedMinimize", + "LBFGSB", + ] + + def __init__(self, alpha: float): + super().__init__() + + def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]): + return 0.5 * self.alpha * jnp.sum(jnp.power(params[0], 2)) / params[1].shape[0] + + def instantiate_solver( + self, + solver_name: str, + loss: Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray + ], + solver_kwargs: Optional[dict] = None, + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep + ]: + self._check_solver(solver_name) + + def penalized_loss(params, X, y): + return loss(params, X, y) + self.penalization(params) + + solver = getattr(jaxopt, solver_name)(fun=penalized_loss, **solver_kwargs) + + def solver_run( + init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray + ) -> jaxopt.OptStep: + return solver.run(init_params, X=X, y=y) + + return solver_run + + +class ProxGradientRegularizer(Regularizer, abc.ABC): + allowed_solvers = ["ProximalGradient"] + + def __init__(self, alpha, mask: jnp.ndarray): + super().__init__(alpha) + if not jnp.all((mask == 1) | (mask == 0)): + raise ValueError("mask must be an jnp.ndarray of 0s and 1s!") + if jnp.any(jnp.sum(mask, axis=0) != 1): + raise ValueError("Each feature must be assigned to a group!") + self.mask = mask + + @abc.abstractmethod + def prox_operator( + self, + params: Tuple[jnp.ndarray, jnp.ndarray], + l2reg: float, + scaling: float = 1.0, + ) -> Tuple[jnp.ndarray, jnp.ndarray]: + pass + + def instantiate_solver( + self, + solver_name: str, + loss: Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray + ], + solver_kwargs: Optional[dict] = None, + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep + ]: + self._check_solver(solver_name) + + if solver_kwargs is None: + solver_kwargs = dict() + + solver = getattr(jaxopt, solver_name)( + fun=loss, prox=self.prox_operator, **solver_kwargs + ) + + def solver_run( + init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray + ) -> jaxopt.OptStep: + return solver.run(init_params, X=X, y=y, hyperparams_prox=self.alpha) + + return solver_run + + +class LassoRegularizer(ProxGradientRegularizer): + def __init__(self, alpha, mask: jnp.ndarray): + super().__init__(alpha=alpha, mask=mask) + + def prox_operator( + self, + params: Tuple[jnp.ndarray, jnp.ndarray], + alpha: float, + scaling: float = 1.0, + ) -> Tuple[jnp.ndarray, jnp.ndarray]: + Ws, bs = params + return jaxopt.prox.prox_lasso(Ws, l1reg=alpha, scaling=scaling), bs + + +class GroupLassoRegularizer(ProxGradientRegularizer): + def __init__(self, alpha, mask: jnp.ndarray): + super().__init__(alpha=alpha, mask=mask) + + def prox_operator( + self, + params: Tuple[jnp.ndarray, jnp.ndarray], + alpha: float, + scaling: float = 1.0, + ) -> Tuple[jnp.ndarray, jnp.ndarray]: + return prox_group_lasso(params, alpha=alpha, mask=self.mask, scaling=scaling) diff --git a/src/neurostatslib/observation_noise.py b/src/neurostatslib/observation_noise.py new file mode 100644 index 00000000..c1ca611a --- /dev/null +++ b/src/neurostatslib/observation_noise.py @@ -0,0 +1,48 @@ +import abc +from typing import Tuple + +import jax +import jax.numpy as jnp +import jaxopt + +from .base_class import _Base + + +class NoiseModel(_Base, abc.ABC): + def __init__(self, inverse_link_function, **kwargs): + super().__init__(**kwargs) + self.inverse_link_function = inverse_link_function + + @abc.abstractmethod + def log_likelihood(self, params, X, y): + pass + + @abc.abstractmethod + def emission_probability(self, rate, **kwargs): + pass + + def _predict(self, params, X): + Ws, bs = params + return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) + + +class PoissonNoiseModel(_Base, NoiseModel): + def __init__(self, inverse_link_function, **kwargs): + super().__init__(inverse_link_function=inverse_link_function, **kwargs) + + def log_likelihood( + self, + params: Tuple[jnp.ndarray, jnp.ndarray], + X: jnp.ndarray, + y: jnp.ndarray, + ): + predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) + x = y * jnp.log(predicted_firing_rates) + # see above for derivation of this. + return jnp.mean(predicted_firing_rates - x) + + @staticmethod + def emission_probability( + key: jax.random.PRNGKey, firing_rate: jnp.ndarray + ) -> jnp.ndarray: + return jax.random.poisson(key, firing_rate) diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py new file mode 100644 index 00000000..e8680a52 --- /dev/null +++ b/src/neurostatslib/proximal_operator.py @@ -0,0 +1,66 @@ +from typing import Tuple + +import jax +import jax.numpy as jnp + + +def _norm2_masked(weight_neuron: jnp.ndarray, mask: jnp.ndarray) -> jnp.ndarray: + """Group-by-group norm 2. + + Parameters + ---------- + weight_neuron: + The feature vector for a neuron. Shape (n_features, ). + mask: + The mask vector for group. mask[i] = 1, if the i-th element of weight_neuron + belongs to the group, 0 otherwise. + + Returns + ------- + : + The norm of the weight vector corresponding to the feature in mask. + Notes + ----- + The proximal gradient operator is described in article [1], Proposition 1. + + .. [1] Yuan, Ming, and Yi Lin. "Model selection and estimation in regression with grouped variables." + Journal of the Royal Statistical Society Series B: Statistical Methodology 68.1 (2006): 49-67. + """ + return jnp.linalg.norm(weight_neuron * mask, 2) / jnp.sqrt(mask.sum()) + + +# vectorize the norm function above +# [(n_neurons, n_features), (n_groups, n_features)] -> (n_neurons, n_groups) +_vmap_norm2_masked_1 = jax.vmap(_norm2_masked, in_axes=(0, None), out_axes=0) +_vmap_norm2_masked_2 = jax.vmap(_vmap_norm2_masked_1, in_axes=(None, 0), out_axes=1) + + +def prox_group_lasso( + params: Tuple[jnp.ndarray, jnp.ndarray], + alpha: float, + mask: jnp.ndarray, + scaling: float = 1.0, +) -> Tuple[jnp.ndarray, jnp.ndarray]: + """Proximal gradient for group lasso. + + Parameters + ---------- + params: + Weights, shape (n_neurons, n_features) + alpha: + The regularization hyperparameter. + mask: + ND array of 0,1 as float32, feature mask. size (n_groups x n_features) + scaling: + The scaling factor for the group-lasso (it will be set + depending on the step-size). + Returns + ------- + The rescaled weights. + """ + weights, intercepts = params + # returns a (n_neurons, n_groups) matrix of norm 2s. + l2_norm = _vmap_norm2_masked_2(weights, mask) + factor = 1 - alpha * scaling / l2_norm + factor = jax.nn.relu(factor) + return weights * (factor @ mask), intercepts From 0912e91108cda0e78dd1f7ca41d901c4a906c352 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 8 Sep 2023 20:26:47 -0400 Subject: [PATCH 057/250] added tests --- pyproject.toml | 2 +- src/neurostatslib/__init__.py | 11 +- src/neurostatslib/base_class.py | 7 +- src/neurostatslib/glm.py | 462 ++++++++++++++++++++++- src/neurostatslib/observation_noise.py | 108 +++++- src/neurostatslib/proximal_operator.py | 23 +- src/neurostatslib/{loss.py => solver.py} | 154 ++++---- tests/conftest.py | 17 +- tests/test_base_class.py | 4 +- tests/test_observation_noise.py | 77 ++++ tests/test_solver.py | 179 +++++++++ 11 files changed, 954 insertions(+), 90 deletions(-) rename src/neurostatslib/{loss.py => solver.py} (61%) create mode 100644 tests/test_observation_noise.py create mode 100644 tests/test_solver.py diff --git a/pyproject.toml b/pyproject.toml index 20245ec3..4c4144e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,7 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] -addopts = "--cov=neurostatslib" # Additional options to pass to pytest, enabling coverage for the 'neurostatslib' package +#addopts = "--cov=neurostatslib" # Additional options to pass to pytest, enabling coverage for the 'neurostatslib' package testpaths = ["tests"] # Specify the directory where test files are located [tool.coverage.report] diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index 0e5a2718..37792f85 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -1,3 +1,12 @@ #!/usr/bin/env python3 -from . import base_class, basis, exceptions, glm, sample_points, utils +from . import ( + base_class, + basis, + exceptions, + glm, + observation_noise, + sample_points, + solver, + utils, +) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 7cde502f..6852950a 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -138,9 +138,9 @@ def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Dev ) return target_device - @staticmethod def device_put( - *args: jnp.ndarray, device: xla_client.Device + self, + *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] ) -> Union[Any, jnp.ndarray]: """Send arrays to device. @@ -152,13 +152,14 @@ def device_put( *args: NDArray device: - A target device, such as that returned by `select_target_device`. + A target device between "cpu", "tpu", "gpu". Returns ------- : The arrays on the desired device. """ + device = self.select_target_device(device) return tuple( jax.device_put(arg, device) if arg.device_buffer.device() != device else arg for arg in args diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 7a1aa804..dfc24158 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,7 +1,7 @@ """GLM core module.""" import abc import inspect -from typing import Callable, Literal, Optional, Tuple, Union +from typing import Callable, Literal, Optional, Tuple, Type, Union import jax import jax.numpy as jnp @@ -10,9 +10,465 @@ from .base_class import _BaseRegressor from .exceptions import NotFittedError +from .observation_noise import NoiseModel, PoissonNoiseModel +from .solver import Solver from .utils import convolve_1d_trials +class GLM(_BaseRegressor): + def __init__( + self, + noise_model: NoiseModel, + solver: Solver, + score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + data_type: Union[Type[jnp.float32], Type[jnp.float64]] = jnp.float32 + ): + super().__init__() + self.noise_model = noise_model + self.solver = solver + self.inverse_link_function = noise_model.inverse_link_function + + if score_type not in ["log-likelihood", "pseudo-r2"]: + raise NotImplementedError( + f"Scoring method {score_type} not implemented! " + f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." + ) + + if not jax.config.values["jax_enable_x64"] and (data_type == jnp.float64): + raise TypeError("JAX is currently not set up to support `jnp.float64`. " + "To enable 64-bit precision, use " + "`jax.config.update(\"jax_enable_x64\", True)` " + "before your computations.") + self.data_type = data_type + self.score_type = score_type + self.baseline_link_fr_ = None + self.basis_coeff_ = None + # scale parameter (=1 for poisson and Gaussian, needs to be estimated for Gamma) + # the estimate of scale does not affect the ML estimate of the other parameter + self.scale = 1.0 + self.solver_state = None + + def _check_is_fit(self): + """Ensure the instance has been fitted.""" + if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): + raise NotFittedError( + "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." + ) + + def _predict( + self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray + ) -> jnp.ndarray: + """ + Predict firing rates given predictors and parameters. + + Parameters + ---------- + params : + Tuple containing the spike basis coefficients and bias terms. + X : + Predictors. Shape (n_time_bins, n_neurons, n_features). + + Returns + ------- + jnp.ndarray + The predicted firing rates. Shape (n_time_bins, n_neurons). + """ + Ws, bs = params + return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) + + def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + """Predict firing rates based on fit parameters. + + Parameters + ---------- + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features). + + Returns + ------- + predicted_firing_rates : jnp.ndarray + The predicted firing rates with shape (n_neurons, n_time_bins). + + Raises + ------ + NotFittedError + If ``fit`` has not been called first with this instance. + ValueError + - If `params` is not a JAX pytree of size two. + - If weights and bias terms in `params` don't have the expected dimensions. + - If the number of neurons in the model parameters and in the inputs do not match. + - If `X` is not three-dimensional. + - If there's an inconsistent number of features between spike basis coefficients and `X`. + + See Also + -------- + [score](../glm/#neurostatslib.glm.PoissonGLM.score) + Score predicted firing rates against target spike counts. + """ + # check that the model is fitted + self._check_is_fit() + # extract model params + Ws = self.basis_coeff_ + bs = self.baseline_link_fr_ + + (X,) = self._convert_to_jnp_ndarray(X, data_type=self.data_type) + + # check input dimensionality + self._check_input_dimensionality(X=X) + # check consistency between X and params + self._check_input_and_params_consistency((Ws, bs), X=X) + return self._predict((Ws, bs), X) + + def _score( + self, + params: Tuple[jnp.ndarray, jnp.ndarray], + X: jnp.ndarray, + target_activity: jnp.ndarray + ) -> jnp.ndarray: + r"""Score the predicted firing rates against target neural activity. + + This computes the negative log-likelihood up to a constant term. + + Note that you can end up with infinities in here if there are zeros in + ``predicted_firing_rates``. We raise a warning in that case. + + Parameters + ---------- + params : + Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features). + target_activity : + The target activity to compare against. Shape (n_time_bins, n_neurons). + + Returns + ------- + : + The Poisson negative log-likehood. Shape (1,). + + """ + predicted_rate = self._predict(params, X) + return self.noise_model.negative_log_likelihood(predicted_rate, target_activity) + + def score( + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + ) -> jnp.ndarray: + r"""Score the predicted firing rates (based on fit) to the target spike counts. + + This computes the GLM mean log-likelihood or the pseudo-$R^2$, thus the higher the + number the better. + + The pseudo-$R^2$ can be computed as follows, + + $$ + \begin{aligned} + R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ + &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) + - \log \text{LL}(\bar{\lambda}| y)}, + \end{aligned} + $$ + + where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is + the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model + predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate. See [1]. + + Parameters + ---------- + X : + The exogenous variables. Shape (n_time_bins, n_neurons, n_features) + y : + Neural activity arranged in a matrix. n_neurons must be the same as + during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). + + Returns + ------- + score : (1,) + The Poisson log-likelihood or the pseudo-$R^2$ of the current model. + + Raises + ------ + NotFittedError + If ``fit`` has not been called first with this instance. + ValueError + If attempting to simulate a different number of neurons than were + present during fitting (i.e., if ``init_y.shape[0] != + self.baseline_link_fr_.shape[0]``). + + Notes + ----- + The log-likelihood is not on a standard scale, its value is influenced by many factors, + among which the number of model parameters. The log-likelihood can assume both positive + and negative values. + + The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure + of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. + The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. + + Refer to the concrete subclass docstrings `_score` for the specific likelihood equations. + + References + ---------- + [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. + Routledge, 2013. + + """ + # ignore the last time point from predict, because that corresponds to + # the next time step, which we have no observed data for + self._check_is_fit() + Ws = self.basis_coeff_ + bs = self.baseline_link_fr_ + + X, y = self._convert_to_jnp_ndarray(X, y, data_type=self.data_type) + + self._check_input_dimensionality(X, y) + self._check_input_n_timepoints(X, y) + self._check_input_and_params_consistency((Ws, bs), X=X, y=y) + if self.score_type == "log-likelihood": + norm_constant = jax.scipy.special.gammaln(y + 1).mean() + score = -self._score((Ws, bs), X, y) - norm_constant + else: + score = self.noise_model.pseudo_r2(self._predict((Ws, bs), X), y) + + return score + + def fit( + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "cpu", + ): + """Fit GLM to neural activity. + + Following scikit-learn API, the solutions are stored as attributes + ``basis_coeff_`` and ``baseline_link_fr``. + + Parameters + ---------- + X : + Predictors, shape (n_time_bins, n_neurons, n_features) + y : + Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). + init_params : + Initial values for the spike basis coefficients and bias terms. If + None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) + device: + Device used for optimizing model parameters. + + Raises + ------ + ValueError + - If `init_params` is not of length two. + - If dimensionality of `init_params` are not correct. + - If the number of neurons in the model parameters and in the inputs do not match. + - If `X` is not three-dimensional. + - If spike_data is not two-dimensional. + - If solver returns at least one NaN parameter, which means it found + an invalid solution. Try tuning optimization hyperparameters. + TypeError + - If `init_params` are not array-like + - If `init_params[i]` cannot be converted to jnp.ndarray for all i + """ + # convert to jnp.ndarray & perform checks + X, y, init_params = self._preprocess_fit(X, y, init_params, data_type=self.data_type) + + # send to device + X, y = self.device_put(X, y, device=device) + init_params = self.device_put(*init_params, device=device) + + # Run optimization + runner = self.solver.instantiate_solver(self._score) + params, state = runner(init_params, X, y) + + if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): + raise ValueError( + "Solver returned at least one NaN parameter, so solution is invalid!" + " Try tuning optimization hyperparameters." + ) + + # Store parameters + self.basis_coeff_ = params[0] + self.baseline_link_fr_ = params[1] + # note that this will include an error value, which is not the same as + # the output of loss. I believe it's the output of + # solver.l2_optimality_error + self.solver_state = state + + def simulate( + self, + random_key: jax.random.PRNGKeyArray, + n_timesteps: int, + init_y: Union[NDArray, jnp.ndarray], + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + device: Literal["cpu", "gpu", "tpu"] = "cpu", + ): + """ + Simulate spike trains using the GLM as a recurrent network. + + This function projects neural activity into the future, employing the fitted + parameters of the GLM. It is capable of simulating activity based on a combination + of historical spike activity and external feedforward inputs like convolved currents, light + intensities, etc. + + Parameters + ---------- + random_key : + PRNGKey for seeding the simulation. + n_timesteps : + Duration of the simulation in terms of time steps. + init_y : + Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. + Expected shape: (window_size, n_neurons). + coupling_basis_matrix : + Basis matrix for coupling, representing between-neuron couplings + and auto-correlations. Expected shape: (window_size, n_basis_coupling). + feedforward_input : + External input matrix to the model, representing factors like convolved currents, + light intensities, etc. When not provided, the simulation is done with coupling-only. + Expected shape: (n_timesteps, n_neurons, n_basis_input). + device : + Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. + + Returns + ------- + simulated_obs : + Simulated observations (spike counts for PoissonGLMs) for each neuron over time. + Shape: (n_neurons, n_timesteps). + firing_rates : + Simulated firing rates for each neuron over time. + Shape: (n_neurons, n_timesteps). + + Raises + ------ + NotFittedError + If the model hasn't been fitted prior to calling this method. + ValueError + - If the instance has not been previously fitted. + - If there's an inconsistency between the number of neurons in model parameters. + - If the number of neurons in input arguments doesn't match with model parameters. + - For an invalid computational device selection. + + + See Also + -------- + predict : Method to predict firing rates based on the model's parameters. + + Notes + ----- + The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients + (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. + The remaining coefficients correspond to the weights for the feed-forward input. + + + The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` + to ensure consistency in the model's input feature dimensionality. + """ + # check if the model is fit + self._check_is_fit() + + # convert to jnp.ndarray + init_y, coupling_basis_matrix, feedforward_input = self._convert_to_jnp_ndarray( + init_y, coupling_basis_matrix, feedforward_input, data_type=jnp.float32 + ) + + # Transfer data to the target device + init_y, coupling_basis_matrix, feedforward_input = self.device_put( + init_y, coupling_basis_matrix, feedforward_input, device=device + ) + + n_basis_coupling = coupling_basis_matrix.shape[1] + n_neurons = self.baseline_link_fr_.shape[0] + + # add an empty input (simulate with coupling-only) + if feedforward_input is None: + feedforward_input = jnp.zeros( + (n_timesteps, n_neurons, 0), dtype=jnp.float32 + ) + + Ws = self.basis_coeff_ + bs = self.baseline_link_fr_ + + self._check_input_dimensionality(feedforward_input, init_y) + + if ( + feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] + != Ws.shape[1] + ): + raise ValueError( + "The number of feed forward input features " + "and the number of recurrent features must add up to " + "the overall model features." + f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " + f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " + f"provided instead." + ) + + self._check_input_and_params_consistency( + (Ws[:, n_basis_coupling * n_neurons:], bs), + X=feedforward_input, + y=init_y, + ) + + if init_y.shape[0] != coupling_basis_matrix.shape[0]: + raise ValueError( + "`init_y` and `coupling_basis_matrix`" + " should have the same window size! " + f"`init_y` window size: {init_y.shape[1]}, " + f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" + ) + + if feedforward_input.shape[0] != n_timesteps: + raise ValueError( + "`feedforward_input` must be of length `n_timesteps`. " + f"`feedforward_input` has length {len(feedforward_input)}, " + f"`n_timesteps` is {n_timesteps} instead!" + ) + subkeys = jax.random.split(random_key, num=n_timesteps) + + def scan_fn( + data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray + ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: + """Scan over time steps and simulate spikes and firing rates. + + This function simulates the spikes and firing rates for each time step + based on the previous spike data, feedforward input, and model coefficients. + """ + spikes, chunk = data + + # Convolve the spike data with the coupling basis matrix + conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] + + # Extract the corresponding slice of the feedforward input for the current time step + input_slice = jax.lax.dynamic_slice( + feedforward_input, + (chunk, 0, 0), + (1, feedforward_input.shape[1], feedforward_input.shape[2]), + ) + + # Reshape the convolved spikes and concatenate with the input slice to form the model input + conv_spk = jnp.tile( + conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] + ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) + X = jnp.concatenate([conv_spk, input_slice], axis=2) + + # Predict the firing rate using the model coefficients + firing_rate = self._predict((Ws, bs), X) + + # Simulate activity based on the predicted firing rate + new_spikes = self.noise_model.emission_probability(key, firing_rate) + + # Prepare the spikes for the next iteration (keeping the most recent spikes) + concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 + return concat_spikes, (new_spikes, firing_rate) + + _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) + simulated_spikes, firing_rates = outputs + return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) + + class _BaseGLM(_BaseRegressor, abc.ABC): """Abstract base class for Poisson GLMs. @@ -244,8 +700,8 @@ def _safe_score( ---------- X : The exogenous variables. Shape (n_time_bins, n_neurons, n_features) - spike_data : - Spike counts arranged in a matrix. n_neurons must be the same as + y : + Neural activity arranged in a matrix. n_neurons must be the same as during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). score_type: String indicating the type of scoring to return. Options are: diff --git a/src/neurostatslib/observation_noise.py b/src/neurostatslib/observation_noise.py index c1ca611a..c26c8dc0 100644 --- a/src/neurostatslib/observation_noise.py +++ b/src/neurostatslib/observation_noise.py @@ -1,48 +1,124 @@ import abc -from typing import Tuple +from typing import Union import jax import jax.numpy as jnp -import jaxopt from .base_class import _Base +KeyArray = Union[jnp.ndarray, jax.random.PRNGKeyArray] + class NoiseModel(_Base, abc.ABC): + FLOAT_EPS = jnp.finfo(jnp.float32).eps + def __init__(self, inverse_link_function, **kwargs): super().__init__(**kwargs) + if not callable(inverse_link_function): + raise ValueError("inverse_link_function must be a callable!") self.inverse_link_function = inverse_link_function @abc.abstractmethod - def log_likelihood(self, params, X, y): + def negative_log_likelihood(self, firing_rate, y): pass + @staticmethod @abc.abstractmethod - def emission_probability(self, rate, **kwargs): + def emission_probability( + key: KeyArray, firing_rate: jnp.ndarray, **kwargs + ) -> jnp.ndarray: pass - def _predict(self, params, X): - Ws, bs = params - return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) + @abc.abstractmethod + def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray): + pass + def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): + r"""Pseudo-R^2 calculation for a GLM. -class PoissonNoiseModel(_Base, NoiseModel): - def __init__(self, inverse_link_function, **kwargs): - super().__init__(inverse_link_function=inverse_link_function, **kwargs) + The Pseudo-R^2 metric gives a sense of how well the model fits the data, + relative to a null (or baseline) model. + + Parameters + ---------- + predicted_rate: + The mean neural activity. + y: + The neural activity. + + Returns + ------- + : + The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, + whereas a value closer to 0 suggests that the model doesn't improve much over the null model. + + """ + res_dev_t = self.residual_deviance(predicted_rate, y) + resid_deviance = jnp.sum(res_dev_t**2) - def log_likelihood( + null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() + null_dev_t = self.residual_deviance(null_mu, y) + null_deviance = jnp.sum(null_dev_t**2) + + return (null_deviance - resid_deviance) / null_deviance + + +class PoissonNoiseModel(NoiseModel): + def __init__(self, inverse_link_function): + super().__init__(inverse_link_function=inverse_link_function) + + def negative_log_likelihood( self, - params: Tuple[jnp.ndarray, jnp.ndarray], - X: jnp.ndarray, + predicted_rate: jnp.ndarray, y: jnp.ndarray, ): - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) + predicted_firing_rates = jnp.clip(predicted_rate, a_min=self.FLOAT_EPS) x = y * jnp.log(predicted_firing_rates) # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) @staticmethod def emission_probability( - key: jax.random.PRNGKey, firing_rate: jnp.ndarray + key: KeyArray, predicted_rate: jnp.ndarray, **kwargs + ) -> jnp.ndarray: + return jax.random.poisson(key, predicted_rate) + + def residual_deviance( + self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray ) -> jnp.ndarray: - return jax.random.poisson(key, firing_rate) + r"""Compute the residual deviance for a Poisson model. + + Parameters + ---------- + predicted_rate: + The predicted firing rates. + spike_counts: + The spike counts. + + Returns + ------- + : + The residual deviance of the model. + + Notes + ----- + Deviance is a measure of the goodness of fit of a statistical model. + For a Poisson model, the residual deviance is computed as: + + $$ + \begin{aligned} + D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) + - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ + &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) + \end{aligned} + $$ + where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model + log-likelihood. Lower values of deviance indicate a better fit. + + """ + # this takes care of 0s in the log + ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) + resid_dev = 2 * ( + spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) + ) + return resid_dev diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index e8680a52..b169f3f8 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Any, Optional, Tuple import jax import jax.numpy as jnp @@ -64,3 +64,24 @@ def prox_group_lasso( factor = 1 - alpha * scaling / l2_norm factor = jax.nn.relu(factor) return weights * (factor @ mask), intercepts + + +def prox_lasso(x: Any, + l1reg: Optional[Any] = None, + scaling: float = 1.0) -> Any: + + def fun(u, v): return jnp.sign(u) * jax.nn.relu(jnp.abs(u) - v * scaling) + + if l1reg is None: + l1reg = 1.0 + + if isinstance(x, tuple): + + l1reg = tuple(l1reg for _ in x) + + return tuple(fun(u, v) for u, v in zip(x, l1reg)) + + else: + if isinstance(l1reg, float): + l1reg = l1reg * jnp.ones_like(x) + return fun(x, l1reg) \ No newline at end of file diff --git a/src/neurostatslib/loss.py b/src/neurostatslib/solver.py similarity index 61% rename from src/neurostatslib/loss.py rename to src/neurostatslib/solver.py index 84a76601..add810c5 100644 --- a/src/neurostatslib/loss.py +++ b/src/neurostatslib/solver.py @@ -1,39 +1,33 @@ import abc +import inspect from typing import Callable, Optional, Tuple import jax.numpy as jnp import jaxopt from .base_class import _Base -from .proximal_operator import prox_group_lasso +from .proximal_operator import prox_group_lasso, prox_lasso -class Solver( - jaxopt.GradientDescent, - jaxopt.BFGS, - jaxopt.LBFGS, - jaxopt.ScipyMinimize, - jaxopt.NonlinearCG, - jaxopt.ScipyBoundedMinimize, - jaxopt.LBFGSB, - jaxopt.ProximalGradient, -): - """Class grouping any solver we allow. - - We allow the following solvers: - - Unconstrained: GradientDescent, BFGS, LBFGS, ScipyMinimize, NonlinearCG - - Box-Bounded: ScipyBoundedMinimize, LBFGSB - - Non-Smooth: ProximalGradient - - """ - - -class Regularizer(_Base, abc.ABC): +class Solver(_Base, abc.ABC): allowed_solvers = [] - def __init__(self, alpha: float = 0.0, **kwargs): + def __init__( + self, + solver_name: str, + solver_kwargs: Optional[dict] = None, + alpha: Optional[float] = None, + **kwargs, + ): super().__init__(**kwargs) + self._check_solver(solver_name) self.alpha = alpha + self.solver_name = solver_name + if solver_kwargs is None: + self.solver_kwargs = dict() + else: + self.solver_kwargs = solver_kwargs + self._check_solver_kwargs(self.solver_name, self.solver_kwargs) def _check_solver(self, solver_name: str): if solver_name not in self.allowed_solvers: @@ -43,45 +37,56 @@ def _check_solver(self, solver_name: str): f"Allowed solvers are {self.allowed_solvers}." ) + @staticmethod + def _check_solver_kwargs(solver_name, solver_kwargs): + solver_args = inspect.getfullargspec(getattr(jaxopt, solver_name)).args + undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) + if undefined_kwargs: + raise NameError( + f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" + ) + + @staticmethod + def _check_is_callable(func): + if not callable(func): + raise TypeError("The loss function must a Callable!") + @abc.abstractmethod def instantiate_solver( self, - solver_name: str, loss: Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray ], - solver_kwargs: Optional[dict] = None, ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: pass -class UnRegularized(Regularizer): +class UnRegularizedSolver(Solver): allowed_solvers = [ "GradientDescent", "BFGS", "LBFGS", "ScipyMinimize", - "NonlinearCG" "ScipyBoundedMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", "LBFGSB", ] - def __init__(self): - super().__init__() + def __init__(self, solver_name: str, solver_kwargs: Optional[dict] = None): + super().__init__(solver_name, solver_kwargs=solver_kwargs) def instantiate_solver( self, - solver_name: str, loss: Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray ], - solver_kwargs: Optional[dict] = None, ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_solver(solver_name) - solver = getattr(jaxopt, solver_name)(fun=loss, **solver_kwargs) + self._check_is_callable(loss) + solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) def solver_run( init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray @@ -91,38 +96,38 @@ def solver_run( return solver_run -class Ridge(Regularizer): +class RidgeSolver(Solver): allowed_solvers = [ "GradientDescent", "BFGS", "LBFGS", "ScipyMinimize", - "NonlinearCG" "ScipyBoundedMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", "LBFGSB", ] - def __init__(self, alpha: float): - super().__init__() + def __init__( + self, solver_name: str, solver_kwargs: Optional[dict] = None, alpha: float = 1.0 + ): + super().__init__(solver_name, solver_kwargs=solver_kwargs, alpha=alpha) def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]): return 0.5 * self.alpha * jnp.sum(jnp.power(params[0], 2)) / params[1].shape[0] def instantiate_solver( self, - solver_name: str, loss: Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray - ], - solver_kwargs: Optional[dict] = None, + ] ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_solver(solver_name) - + self._check_is_callable(loss) def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) - solver = getattr(jaxopt, solver_name)(fun=penalized_loss, **solver_kwargs) + solver = getattr(jaxopt, self.solver_name)(fun=penalized_loss, **self.solver_kwargs) def solver_run( init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray @@ -132,15 +137,17 @@ def solver_run( return solver_run -class ProxGradientRegularizer(Regularizer, abc.ABC): +class ProxGradientSolver(Solver, abc.ABC): allowed_solvers = ["ProximalGradient"] - def __init__(self, alpha, mask: jnp.ndarray): - super().__init__(alpha) - if not jnp.all((mask == 1) | (mask == 0)): - raise ValueError("mask must be an jnp.ndarray of 0s and 1s!") - if jnp.any(jnp.sum(mask, axis=0) != 1): - raise ValueError("Each feature must be assigned to a group!") + def __init__( + self, + solver_name: str, + solver_kwargs: Optional[dict] = None, + alpha: float = 1.0, + mask: Optional[jnp.ndarray] = None, + ): + super().__init__(solver_name, solver_kwargs=solver_kwargs, alpha=alpha) self.mask = mask @abc.abstractmethod @@ -154,21 +161,20 @@ def prox_operator( def instantiate_solver( self, - solver_name: str, loss: Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray ], - solver_kwargs: Optional[dict] = None, ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_solver(solver_name) - if solver_kwargs is None: - solver_kwargs = dict() + self._check_is_callable(loss) - solver = getattr(jaxopt, solver_name)( - fun=loss, prox=self.prox_operator, **solver_kwargs + def loss_kwarg(params, X=jnp.zeros(1), y=jnp.zeros(1)): + return loss(params, X, y) + + solver = getattr(jaxopt, self.solver_name)( + fun=loss_kwarg, prox=self.prox_operator, **self.solver_kwargs ) def solver_run( @@ -179,9 +185,17 @@ def solver_run( return solver_run -class LassoRegularizer(ProxGradientRegularizer): - def __init__(self, alpha, mask: jnp.ndarray): - super().__init__(alpha=alpha, mask=mask) +class LassoSolver(ProxGradientSolver): + def __init__( + self, + solver_name: str, + solver_kwargs: Optional[dict] = None, + alpha: float = 1.0, + mask: Optional[jnp.ndarray] = None, + ): + super().__init__( + solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask + ) def prox_operator( self, @@ -190,12 +204,26 @@ def prox_operator( scaling: float = 1.0, ) -> Tuple[jnp.ndarray, jnp.ndarray]: Ws, bs = params + return jaxopt.prox.prox_lasso(Ws, l1reg=alpha, scaling=scaling), bs -class GroupLassoRegularizer(ProxGradientRegularizer): - def __init__(self, alpha, mask: jnp.ndarray): - super().__init__(alpha=alpha, mask=mask) + +class GroupLassoSolver(ProxGradientSolver): + def __init__( + self, + solver_name: str, + mask: jnp.ndarray, + solver_kwargs: Optional[dict] = None, + alpha: float = 1.0, + ): + super().__init__( + solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask + ) + if not jnp.all((mask == 1) | (mask == 0)): + raise ValueError("mask must be an jnp.ndarray of 0s and 1s!") + if jnp.any(jnp.sum(mask, axis=0) != 1): + raise ValueError("Each feature must be assigned to a group!") def prox_operator( self, diff --git a/tests/conftest.py b/tests/conftest.py index ba0d16ad..b93ac9d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,7 +55,9 @@ def poissonGLM_model_instantiation(): X = np.random.normal(size=(100, 1, 5)) b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) - model = nsl.glm.PoissonGLM(inverse_link_function=jax.numpy.exp) + noise_model = nsl.observation_noise.PoissonNoiseModel(jnp.exp) + solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) + model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -89,3 +91,16 @@ def poissonGLM_coupled_model_config_simulate(): init_spikes = jnp.asarray(config_dict["init_spikes"]) return model, coupling_basis, feedforward_input, init_spikes, jax.random.PRNGKey(123) + +@pytest.fixture +def jaxopt_solvers(): + return [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", + "LBFGSB", + "ProximalGradient" + ] diff --git a/tests/test_base_class.py b/tests/test_base_class.py index a900a7e8..c9ac98df 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -1,7 +1,9 @@ -import pytest from typing import Union + import jax.numpy as jnp +import pytest from numpy.typing import NDArray + from neurostatslib.base_class import _BaseRegressor diff --git a/tests/test_observation_noise.py b/tests/test_observation_noise.py new file mode 100644 index 00000000..8f0aa363 --- /dev/null +++ b/tests/test_observation_noise.py @@ -0,0 +1,77 @@ +import jax +import jax.numpy as jnp +import numpy as np +import pytest +import scipy.stats as sts +import statsmodels.api as sm + +import neurostatslib as nsl + + +class TestPoissonNoiseModel: + cls = nsl.observation_noise.PoissonNoiseModel + + @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) + def test_initialization(self, link_function): + """Check that the noise model initializes when a callable is passed.""" + raise_exception = not callable(link_function) + if raise_exception: + with pytest.raises(ValueError, match="inverse_link_function must be a callable"): + self.cls(link_function) + else: + self.cls(link_function) + + def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): + """ + Compare fitted parameters to statsmodels. + Assesses if the model estimates are close to statsmodels' results. + """ + _, y, model, _, firing_rate = poissonGLM_model_instantiation + dev = sm.families.Poisson().deviance(y, firing_rate) + dev_model = model.noise_model.residual_deviance(firing_rate, y).sum() + if not np.allclose(dev, dev_model): + raise ValueError("Deviance doesn't match statsmodels!") + + def test_loglikelihood_against_scipy(self, poissonGLM_model_instantiation): + """ + Compare log-likelihood to scipy. + Assesses if the model estimates are close to statsmodels' results. + """ + _, y, model, _, firing_rate = poissonGLM_model_instantiation + ll_model = - model.noise_model.negative_log_likelihood(firing_rate, y).sum()\ + - jax.scipy.special.gammaln(y + 1).mean() + ll_scipy = sts.poisson(firing_rate).logpmf(y).mean() + if not np.allclose(ll_model, ll_scipy): + raise ValueError("Log-likelihood doesn't match scipy!") + + + def test_pseudo_r2_range(self, poissonGLM_model_instantiation): + """ + Compute the pseudo-r2 and check that is < 1. + """ + _, y, model, _, firing_rate = poissonGLM_model_instantiation + pseudo_r2 = model.noise_model.pseudo_r2(firing_rate, y) + if (pseudo_r2 > 1) or (pseudo_r2 < 0): + raise ValueError(f"pseudo-r2 of {pseudo_r2} outside the [0,1] range!") + + + def test_pseudo_r2_mean(self, poissonGLM_model_instantiation): + """ + Check that the pseudo-r2 of the null model is 0. + """ + _, y, model, _, _ = poissonGLM_model_instantiation + pseudo_r2 = model.noise_model.pseudo_r2(y.mean(), y) + if not np.allclose(pseudo_r2, 0): + raise ValueError(f"pseudo-r2 of {pseudo_r2} for the null model. Should be equal to 0!") + + def test_emission_probability(selfself, poissonGLM_model_instantiation): + """ + Test the poisson emission probability. + + Check that the emission probability is set to jax.random.poisson. + """ + _, _, model, _, _ = poissonGLM_model_instantiation + key_array = jax.random.PRNGKey(123) + counts = model.noise_model.emission_probability(key_array, np.arange(1, 11)) + if not jnp.all(counts == jax.random.poisson(key_array, np.arange(1, 11))): + raise ValueError("The emission probability should output the results of a call to jax.random.poisson.") diff --git a/tests/test_solver.py b/tests/test_solver.py new file mode 100644 index 00000000..7627d5dc --- /dev/null +++ b/tests/test_solver.py @@ -0,0 +1,179 @@ +import jax +import jax.numpy as jnp +import numpy as np +import pytest +from sklearn.linear_model import PoissonRegressor +import statsmodels.api as sm + +import neurostatslib as nsl + + +class TestSolver: + cls = nsl.solver.Solver + def test_abstract_nature_of_solver(self): + """Test that Solver can't be instantiated.""" + with pytest.raises(TypeError, match="TypeError: Can't instantiate abstract class Solver"): + self.cls("GradientDescent") + +class TestRidgeSolver: + cls = nsl.solver.RidgeSolver + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_init_solver_name(self, solver_name): + """Test RidgeSolver acceptable solvers.""" + acceptable_solvers = [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", + "LBFGSB" + ] + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + self.cls(solver_name) + else: + self.cls(solver_name) + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) + @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) + def test_init_solver_kwargs(self, solver_name, solver_kwargs): + """Test RidgeSolver acceptable kwargs.""" + + raise_exception = "tols" in list(solver_kwargs.keys()) + if raise_exception: + with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + self.cls(solver_name, solver_kwargs=solver_kwargs) + else: + self.cls(solver_name, solver_kwargs=solver_kwargs) + + @pytest.mark.parametrize("loss", [jnp.exp, np.exp, 1, None, {}]) + def test_loss_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + if raise_exception: + with pytest.raises(TypeError, match="The loss function must a Callable"): + self.cls("GradientDescent").instantiate_solver(loss) + else: + self.cls("GradientDescent").instantiate_solver(loss) + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) + def test_run_solver(self, solver_name, poissonGLM_model_instantiation): + """Test that the solver runs.""" + + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + runner = self.cls("GradientDescent").instantiate_solver(model._score) + runner((true_params[0]*0., true_params[1]), X, y) + + def test_solver_output_match(self, poissonGLM_model_instantiation): + """Test that different solvers converge to the same solution.""" + jax.config.update("jax_enable_x64", True) + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set precision to float64 for accurate matching of the results + model.data_type = jnp.float64 + runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver(model._score) + runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver(model._score) + runner_scipy = self.cls("ScipyMinimize", {"method": "BFGS", "tol": 10**-12}).instantiate_solver(model._score) + weights_gd, intercepts_gd = runner_gd((true_params[0] * 0., true_params[1]), X, y)[0] + weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] + weights_scipy, intercepts_scipy = runner_scipy((true_params[0] * 0., true_params[1]), X, y)[0] + + match_weights = np.allclose(weights_gd, weights_bfgs) and \ + np.allclose(weights_gd, weights_scipy) + match_intercepts = np.allclose(intercepts_gd, intercepts_bfgs) and \ + np.allclose(intercepts_gd, intercepts_scipy) + if (not match_weights) or (not match_intercepts): + raise ValueError("Convex estimators should converge to the same numerical value.") + + def test_solver_match_sklearn(self, poissonGLM_model_instantiation): + """Test that different solvers converge to the same solution.""" + jax.config.update("jax_enable_x64", True) + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set precision to float64 for accurate matching of the results + model.data_type = jnp.float64 + solver = self.cls("GradientDescent", {"tol": 10**-12}) + runner_bfgs = solver.instantiate_solver(model._score) + weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] + model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=solver.alpha) + model_skl.fit(X[:,0], y[:, 0]) + + match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) + match_intercepts = np.allclose(model_skl.intercept_, intercepts_bfgs.flatten()) + if (not match_weights) or (not match_intercepts): + raise ValueError("Ridge GLM solver estimate does not match sklearn!") + + +class TestLassoSolver: + cls = nsl.solver.LassoSolver + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_init_solver_name(self, solver_name): + """Test RidgeSolver acceptable solvers.""" + acceptable_solvers = [ + "ProximalGradient" + ] + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + self.cls(solver_name) + else: + self.cls(solver_name) + + @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) + def test_init_solver_kwargs(self, solver_kwargs): + """Test RidgeSolver acceptable kwargs.""" + raise_exception = "tols" in list(solver_kwargs.keys()) + if raise_exception: + with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + self.cls("ProximalGradient", solver_kwargs=solver_kwargs) + else: + self.cls("ProximalGradient", solver_kwargs=solver_kwargs) + + @pytest.mark.parametrize("loss", [jnp.exp, jax.nn.relu, 1, None, {}]) + def test_loss_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + if raise_exception: + with pytest.raises(TypeError, match="The loss function must a Callable"): + self.cls("ProximalGradient").instantiate_solver(loss) + else: + self.cls("ProximalGradient").instantiate_solver(loss) + + def test_run_solver(self, poissonGLM_model_instantiation): + """Test that the solver runs.""" + + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + runner = self.cls("ProximalGradient").instantiate_solver(model._score) + runner((true_params[0]*0., true_params[1]), X, y) + + def test_solver_match_sklearn(self, poissonGLM_model_instantiation): + """Test that different solvers converge to the same solution.""" + jax.config.update("jax_enable_x64", True) + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set precision to float64 for accurate matching of the results + model.data_type = jnp.float64 + solver = self.cls("ProximalGradient", {"tol": 10**-12}) + runner = solver.instantiate_solver(model._score) + weights, intercepts = runner((true_params[0] * 0., true_params[1]), X, y)[0] + + # instantiate the glm with statsmodels + glm_sm = sm.GLM(endog=y[:, 0], + exog=sm.add_constant(X[:, 0]), + family=sm.families.Poisson()) + + # regularize everything except intercept + alpha_sm = np.ones(X.shape[2] + 1) * solver.alpha + alpha_sm[0] = 0 + + # pure lasso = elastic net with L1 weight = 1 + res_sm = glm_sm.fit_regularized(method="elastic_net", + alpha=alpha_sm, + L1_wt=1., cnvrg_tol=10**-12) + # compare params + sm_params = res_sm.params + glm_params = jnp.hstack((intercepts, weights.flatten())) + match_weights = np.allclose(sm_params, glm_params) + if not match_weights: + raise ValueError("Lasso GLM solver estimate does not match statsmodels!") From 7e613d92c61a39a20162d132596f9d612fd31d5a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 8 Sep 2023 22:16:43 -0400 Subject: [PATCH 058/250] added comprehensive testing --- pyproject.toml | 1 - src/neurostatslib/proximal_operator.py | 26 +--- src/neurostatslib/solver.py | 31 +++- tests/conftest.py | 40 +++++ tests/test_proximal_operator.py | 46 ++++++ tests/test_solver.py | 196 ++++++++++++++++++++++++- tox.ini | 2 +- 7 files changed, 308 insertions(+), 34 deletions(-) create mode 100644 tests/test_proximal_operator.py diff --git a/pyproject.toml b/pyproject.toml index 4c4144e2..236dd2ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,7 +99,6 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] -#addopts = "--cov=neurostatslib" # Additional options to pass to pytest, enabling coverage for the 'neurostatslib' package testpaths = ["tests"] # Specify the directory where test files are located [tool.coverage.report] diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index b169f3f8..2ec1158a 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -63,25 +63,7 @@ def prox_group_lasso( l2_norm = _vmap_norm2_masked_2(weights, mask) factor = 1 - alpha * scaling / l2_norm factor = jax.nn.relu(factor) - return weights * (factor @ mask), intercepts - - -def prox_lasso(x: Any, - l1reg: Optional[Any] = None, - scaling: float = 1.0) -> Any: - - def fun(u, v): return jnp.sign(u) * jax.nn.relu(jnp.abs(u) - v * scaling) - - if l1reg is None: - l1reg = 1.0 - - if isinstance(x, tuple): - - l1reg = tuple(l1reg for _ in x) - - return tuple(fun(u, v) for u, v in zip(x, l1reg)) - - else: - if isinstance(l1reg, float): - l1reg = l1reg * jnp.ones_like(x) - return fun(x, l1reg) \ No newline at end of file + # Avoid shrinkage of features that do not belong to any group + # by setting the shrinkage factor to 1. + not_regularized = jnp.outer(jnp.ones(factor.shape[0]), 1 - mask.sum(axis=0)) + return weights * (factor @ mask + not_regularized), intercepts diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index add810c5..cfc3a444 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -6,7 +6,7 @@ import jaxopt from .base_class import _Base -from .proximal_operator import prox_group_lasso, prox_lasso +from .proximal_operator import prox_group_lasso class Solver(_Base, abc.ABC): @@ -204,11 +204,9 @@ def prox_operator( scaling: float = 1.0, ) -> Tuple[jnp.ndarray, jnp.ndarray]: Ws, bs = params - return jaxopt.prox.prox_lasso(Ws, l1reg=alpha, scaling=scaling), bs - class GroupLassoSolver(ProxGradientSolver): def __init__( self, @@ -220,10 +218,29 @@ def __init__( super().__init__( solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask ) - if not jnp.all((mask == 1) | (mask == 0)): - raise ValueError("mask must be an jnp.ndarray of 0s and 1s!") - if jnp.any(jnp.sum(mask, axis=0) != 1): - raise ValueError("Each feature must be assigned to a group!") + self._check_mask() + + def _check_mask(self): + if self.mask.ndim != 2: + raise ValueError("`mask` must be 2-dimensional. " + f"{self.mask.ndim} dimensional mask provided instead!") + + if self.mask.shape[0] == 0: + raise ValueError(f"Empty mask provided! Mask has shape {self.mask.shape}.") + + if jnp.any((self.mask != 1) & (self.mask != 0)): + raise ValueError("Mask elements be 0s and 1s!") + + if self.mask.sum() == 0: + raise ValueError("Empty mask provided!") + + if jnp.any(self.mask.sum(axis=0) > 1): + raise ValueError("Incorrect group assignment. Some of the features are assigned " + "to more then one group.") + + if not jnp.issubdtype(self.mask.dtype, jnp.floating): + raise ValueError("Mask should be a floating point jnp.ndarray. " + f"Data type {self.mask.dtype} provided instead!") def prox_operator( self, diff --git a/tests/conftest.py b/tests/conftest.py index b93ac9d6..a02a9a42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,3 +104,43 @@ def jaxopt_solvers(): "LBFGSB", "ProximalGradient" ] + +@pytest.fixture +def group_sparse_poisson_glm_model_instantiation(): + """Set up a Poisson GLM for testing purposes with group sparse weights. + + This fixture initializes a Poisson GLM with random, group sparse, parameters, simulates its response, and + returns the test data, expected output, the model instance, true parameters, and the rate + of response + + Returns: + tuple: A tuple containing: + - X (numpy.ndarray): Simulated input data. + - np.random.poisson(rate) (numpy.ndarray): Simulated spike responses. + - model (nsl.glm.PoissonGLM): Initialized model instance. + - (w_true, b_true) (tuple): True weight and bias parameters. + - rate (jax.numpy.ndarray): Simulated rate of response. + """ + np.random.seed(123) + X = np.random.normal(size=(100, 1, 5)) + b_true = np.zeros((1, )) + w_true = np.random.normal(size=(1, 5)) + w_true[0, 1:4] = 0. + noise_model = nsl.observation_noise.PoissonNoiseModel(jnp.exp) + solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) + model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") + rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) + return X, np.random.poisson(rate), model, (w_true, b_true), rate + +@pytest.fixture +def example_data_prox_operator(): + n_neurons = 3 + n_features = 4 + n_groups = 2 + + params = (jnp.ones((n_neurons, n_features)), jnp.zeros(n_neurons)) + alpha = 0.1 + mask = jnp.array([[1, 0, 1, 0], [0, 1, 0, 1]], dtype=jnp.float32) + scaling = 0.5 + + return params, alpha, mask, scaling \ No newline at end of file diff --git a/tests/test_proximal_operator.py b/tests/test_proximal_operator.py new file mode 100644 index 00000000..96ba47a5 --- /dev/null +++ b/tests/test_proximal_operator.py @@ -0,0 +1,46 @@ +import jax.numpy as jnp + +from neurostatslib.proximal_operator import _vmap_norm2_masked_2, prox_group_lasso + + +def test_prox_group_lasso_returns_tuple(example_data_prox_operator): + """Test whether prox_group_lasso returns a tuple.""" + params, alpha, mask, scaling = example_data_prox_operator + updated_params = prox_group_lasso(params, alpha, mask, scaling) + assert isinstance(updated_params, tuple) + +def test_prox_group_lasso_tuple_length(example_data_prox_operator): + """Test whether the tuple returned by prox_group_lasso has a length of 2.""" + params, alpha, mask, scaling = example_data_prox_operator + updated_params = prox_group_lasso(params, alpha, mask, scaling) + assert len(updated_params) == 2 + +def test_prox_group_lasso_weights_shape(example_data_prox_operator): + """Test whether the shape of the weights in prox_group_lasso is correct.""" + params, alpha, mask, scaling = example_data_prox_operator + updated_params = prox_group_lasso(params, alpha, mask, scaling) + assert updated_params[0].shape == params[0].shape + +def test_prox_group_lasso_intercepts_shape(example_data_prox_operator): + """Test whether the shape of the intercepts in prox_group_lasso is correct.""" + params, alpha, mask, scaling = example_data_prox_operator + updated_params = prox_group_lasso(params, alpha, mask, scaling) + assert updated_params[1].shape == params[1].shape + +def test_vmap_norm2_masked_2_returns_array(example_data_prox_operator): + """Test whether _vmap_norm2_masked_2 returns a NumPy array.""" + params, _, mask, _ = example_data_prox_operator + l2_norm = _vmap_norm2_masked_2(params[0], mask) + assert isinstance(l2_norm, jnp.ndarray) + +def test_vmap_norm2_masked_2_shape(example_data_prox_operator): + """Test whether the shape of the result from _vmap_norm2_masked_2 is correct.""" + params, _, mask, _ = example_data_prox_operator + l2_norm = _vmap_norm2_masked_2(params[0], mask) + assert l2_norm.shape == (params[0].shape[0], mask.shape[0]) + +def test_vmap_norm2_masked_2_non_negative(example_data_prox_operator): + """Test whether all elements of the result from _vmap_norm2_masked_2 are non-negative.""" + params, _, mask, _ = example_data_prox_operator + l2_norm = _vmap_norm2_masked_2(params[0], mask) + assert jnp.all(l2_norm >= 0) \ No newline at end of file diff --git a/tests/test_solver.py b/tests/test_solver.py index 7627d5dc..b3dae4ae 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -2,8 +2,8 @@ import jax.numpy as jnp import numpy as np import pytest -from sklearn.linear_model import PoissonRegressor import statsmodels.api as sm +from sklearn.linear_model import PoissonRegressor import neurostatslib as nsl @@ -12,7 +12,7 @@ class TestSolver: cls = nsl.solver.Solver def test_abstract_nature_of_solver(self): """Test that Solver can't be instantiated.""" - with pytest.raises(TypeError, match="TypeError: Can't instantiate abstract class Solver"): + with pytest.raises(TypeError, match="Can't instantiate abstract class Solver"): self.cls("GradientDescent") class TestRidgeSolver: @@ -148,7 +148,7 @@ def test_run_solver(self, poissonGLM_model_instantiation): runner = self.cls("ProximalGradient").instantiate_solver(model._score) runner((true_params[0]*0., true_params[1]), X, y) - def test_solver_match_sklearn(self, poissonGLM_model_instantiation): + def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): """Test that different solvers converge to the same solution.""" jax.config.update("jax_enable_x64", True) X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -177,3 +177,193 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): match_weights = np.allclose(sm_params, glm_params) if not match_weights: raise ValueError("Lasso GLM solver estimate does not match statsmodels!") + + +class TestGroupLassoSolver: + cls = nsl.solver.GroupLassoSolver + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_init_solver_name(self, solver_name): + """Test RidgeSolver acceptable solvers.""" + acceptable_solvers = [ + "ProximalGradient" + ] + raise_exception = solver_name not in acceptable_solvers + + # create a valid mask + mask = np.zeros((2, 10)) + mask[0, :5] = 1 + mask[1, 5:] = 1 + mask = jnp.asarray(mask) + + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + self.cls(solver_name, mask) + else: + self.cls(solver_name, mask) + + @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) + def test_init_solver_kwargs(self, solver_kwargs): + """Test RidgeSolver acceptable kwargs.""" + raise_exception = "tols" in list(solver_kwargs.keys()) + + # create a valid mask + mask = np.zeros((2, 10)) + mask[0, :5] = 1 + mask[0, 1:] = 1 + mask = jnp.asarray(mask) + + if raise_exception: + with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) + else: + self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) + + @pytest.mark.parametrize("loss", [jnp.exp, jax.nn.relu, 1, None, {}]) + def test_loss_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + + # create a valid mask + mask = np.zeros((2, 10)) + mask[0, :5] = 1 + mask[1, 5:] = 1 + mask = jnp.asarray(mask) + + if raise_exception: + with pytest.raises(TypeError, match="The loss function must a Callable"): + self.cls("ProximalGradient", mask).instantiate_solver(loss) + else: + self.cls("ProximalGradient", mask).instantiate_solver(loss) + + def test_run_solver(self, poissonGLM_model_instantiation): + """Test that the solver runs.""" + + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + + # create a valid mask + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + mask = jnp.asarray(mask) + + runner = self.cls("ProximalGradient", mask).instantiate_solver(model._score) + runner((true_params[0]*0., true_params[1]), X, y) + + @pytest.mark.parametrize("n_groups_assign", [0, 1, 2]) + def test_mask_validity_groups(self, + n_groups_assign, + group_sparse_poisson_glm_model_instantiation): + """Test that mask assigns at most 1 group to each weight.""" + raise_exception = n_groups_assign > 1 + X, y, model, true_params, firing_rate = group_sparse_poisson_glm_model_instantiation + + # create a valid mask + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + + # change assignment + if n_groups_assign == 0: + mask[:, 3] = 0 + elif n_groups_assign == 2: + mask[:, 3] = 1 + + mask = jnp.asarray(mask) + + if raise_exception: + with pytest.raises(ValueError, match="Incorrect group assignment. " + "Some of the features"): + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + else: + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + + + + @pytest.mark.parametrize("set_entry", [0, 1, -1, 2, 2.5]) + def test_mask_validity_entries(self, set_entry, poissonGLM_model_instantiation): + """Test that mask is composed of 0s and 1s.""" + raise_exception = set_entry not in {0, 1} + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + + # create a valid mask + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + # assign an entry + mask[1, 2] = set_entry + mask = jnp.asarray(mask, dtype=jnp.float32) + + if raise_exception: + with pytest.raises(ValueError, match="Mask elements be 0s and 1s"): + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + else: + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + + @pytest.mark.parametrize("n_dim", [0, 1, 2, 3]) + def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): + """Test that mask is composed of 0s and 1s.""" + + raise_exception = n_dim != 2 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + + # create a valid mask + if n_dim == 0: + mask = np.array([]) + elif n_dim == 1: + mask = np.ones((1,)) + elif n_dim == 2: + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + else: + mask = np.zeros((2, X.shape[2]) + (1, ) * (n_dim-2)) + mask[0, :2] = 1 + mask[1, 2:] = 1 + + mask = jnp.asarray(mask, dtype=jnp.float32) + + if raise_exception: + with pytest.raises(ValueError, match="`mask` must be 2-dimensional"): + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + else: + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + + @pytest.mark.parametrize("n_groups", [0, 1, 2]) + def test_mask_n_groups(self, n_groups, poissonGLM_model_instantiation): + """Test that mask has at least 1 group.""" + raise_exception = n_groups < 1 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + + # create a mask + mask = np.zeros((n_groups, X.shape[2])) + if n_groups > 0: + for i in range(n_groups-1): + mask[i, i: i+1] = 1 + mask[-1, n_groups-1:] = 1 + + mask = jnp.asarray(mask, dtype=jnp.float32) + + if raise_exception: + with pytest.raises(ValueError, match=r"Empty mask provided! Mask has "): + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + else: + self.cls("ProximalGradient", mask).instantiate_solver(model._score) + + def test_group_sparsity_enforcement(self, group_sparse_poisson_glm_model_instantiation): + """Test that group lasso works on a simple dataset.""" + X, y, model, true_params, firing_rate = group_sparse_poisson_glm_model_instantiation + zeros_true = true_params[0].flatten() == 0 + mask = np.zeros((2, X.shape[2])) + mask[0, zeros_true] = 1 + mask[1, ~zeros_true] = 1 + mask = jnp.asarray(mask, dtype=jnp.float32) + + runner = self.cls("ProximalGradient", mask).instantiate_solver(model._score) + params, _ = runner((true_params[0]*0., true_params[1]), X, y) + + zeros_est = params[0] == 0 + if not np.all(zeros_est == zeros_true): + raise ValueError("GroupLasso failed to zero-out the parameter group!") + + diff --git a/tox.ini b/tox.ini index df1d07c2..fc61991e 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ commands = black --check src isort --check src flake8 --config={toxinidir}/tox.ini src - pytest + pytest --cov [gh-actions] python = From 7cfd5c24884b0a3859938f09ea71250656210341 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 8 Sep 2023 22:18:58 -0400 Subject: [PATCH 059/250] add code of conduct --- CODE_OF_CONDUCT.md | 125 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 CODE_OF_CONDUCT.md diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..5bc75d45 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,125 @@ +## Our Mission +We aspire to build a community-driven, open library dedicated to neuroscientific analysis. As such, we foster friendly discussions encompassing both the contents of the package and the underlying theory. + +## Core Values + - **Inclusivity**: Every contribution is valuable. We recognize and appreciate the diversity of our community members and their contributions. + - **Equality**: We are committed to treating everyone with fairness and impartiality. Discrimination or favoritism has no place in our community. + - **Professionalism**: We expect our community members to maintain a high standard of conduct. Inappropriate or disrespectful language will not be tolerated. + +## Contribution +We welcome code submissions, improvements to documentation, tutorials, active participation in discussions, and revisions to pull requests. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[edoardo.balzani87@gmail.com](mailto:edoardo.balzani87@gmail.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations + + +We follow the principles of openness, respect, and consideration of others of the Python Software Foundation: https://www.python.org/psf/codeofconduct/ From b22de56958cf8f942e954a91f5cce99b25619d1f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Sun, 10 Sep 2023 09:30:12 -0400 Subject: [PATCH 060/250] fixed typing --- src/neurostatslib/base_class.py | 2 +- src/neurostatslib/glm.py | 13 ++++++++----- src/neurostatslib/solver.py | 10 ++++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 6852950a..d669ff8c 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -46,7 +46,7 @@ def get_params(self, deep=True): out[key] = getattr(self, key) return out - def set_params(self, **params): + def set_params(self, **params: Any): """Set the parameters of this estimator. The method works on simple estimators as well as on nested objects diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index dfc24158..8b71a1ae 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,7 +1,7 @@ """GLM core module.""" import abc import inspect -from typing import Callable, Literal, Optional, Tuple, Type, Union +from typing import Any, Callable, Literal, Optional, Tuple, Type, Union import jax import jax.numpy as jnp @@ -21,7 +21,8 @@ def __init__( noise_model: NoiseModel, solver: Solver, score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - data_type: Union[Type[jnp.float32], Type[jnp.float64]] = jnp.float32 + data_type: Union[Type[jnp.float32], Type[jnp.float64]] = jnp.float32, + **kwargs: Any ): super().__init__() self.noise_model = noise_model @@ -39,6 +40,7 @@ def __init__( "To enable 64-bit precision, use " "`jax.config.update(\"jax_enable_x64\", True)` " "before your computations.") + self.data_type = data_type self.score_type = score_type self.baseline_link_fr_ = None @@ -144,7 +146,7 @@ def _score( Returns ------- : - The Poisson negative log-likehood. Shape (1,). + The model negative log-likehood. Shape (1,). """ predicted_rate = self._predict(params, X) @@ -185,7 +187,7 @@ def score( Returns ------- score : (1,) - The Poisson log-likelihood or the pseudo-$R^2$ of the current model. + The log-likelihood or the pseudo-$R^2$ of the current model. Raises ------ @@ -206,7 +208,8 @@ def score( of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. - Refer to the concrete subclass docstrings `_score` for the specific likelihood equations. + Refer to the `nsl.observation_noise.NoiseModel` concrete subclasses for the specific likelihood equations. + References ---------- diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index cfc3a444..d77a434b 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -1,9 +1,10 @@ import abc import inspect -from typing import Callable, Optional, Tuple +from typing import Callable, Optional, Tuple, Union import jax.numpy as jnp import jaxopt +from numpy.typing import NDArray from .base_class import _Base from .proximal_operator import prox_group_lasso @@ -145,7 +146,7 @@ def __init__( solver_name: str, solver_kwargs: Optional[dict] = None, alpha: float = 1.0, - mask: Optional[jnp.ndarray] = None, + mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): super().__init__(solver_name, solver_kwargs=solver_kwargs, alpha=alpha) self.mask = mask @@ -191,7 +192,7 @@ def __init__( solver_name: str, solver_kwargs: Optional[dict] = None, alpha: float = 1.0, - mask: Optional[jnp.ndarray] = None, + mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): super().__init__( solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask @@ -211,7 +212,7 @@ class GroupLassoSolver(ProxGradientSolver): def __init__( self, solver_name: str, - mask: jnp.ndarray, + mask: Union[jnp.ndarray, NDArray], solver_kwargs: Optional[dict] = None, alpha: float = 1.0, ): @@ -221,6 +222,7 @@ def __init__( self._check_mask() def _check_mask(self): + self.mask = jnp.asarray(self.mask) if self.mask.ndim != 2: raise ValueError("`mask` must be 2-dimensional. " f"{self.mask.ndim} dimensional mask provided instead!") From 4eddb67281242522849182958f12b104352d2422 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Sun, 10 Sep 2023 10:06:33 -0400 Subject: [PATCH 061/250] use jax config as default dtype --- src/neurostatslib/glm.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 8b71a1ae..cf6e4c65 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -21,7 +21,7 @@ def __init__( noise_model: NoiseModel, solver: Solver, score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - data_type: Union[Type[jnp.float32], Type[jnp.float64]] = jnp.float32, + data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, **kwargs: Any ): super().__init__() @@ -41,7 +41,15 @@ def __init__( "`jax.config.update(\"jax_enable_x64\", True)` " "before your computations.") - self.data_type = data_type + if data_type is None: + # set to jnp.float64, if float64 are enabled + if jax.config.jax_enable_x64: + self.data_type = jnp.float64 + else: + self.data_type = jnp.float32 + else: + self.data_type = data_type + self.score_type = score_type self.baseline_link_fr_ = None self.basis_coeff_ = None From 79b599723b72af3d0faa8fcac89e61638bd1b78c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Sun, 10 Sep 2023 10:30:55 -0400 Subject: [PATCH 062/250] make sure the mask is configured before the runner is called --- src/neurostatslib/glm.py | 20 +++++++++++++------- src/neurostatslib/solver.py | 1 - 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index cf6e4c65..c67183d1 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -11,7 +11,7 @@ from .base_class import _BaseRegressor from .exceptions import NotFittedError from .observation_noise import NoiseModel, PoissonNoiseModel -from .solver import Solver +from .solver import Solver, GroupLassoSolver from .utils import convolve_1d_trials @@ -44,11 +44,11 @@ def __init__( if data_type is None: # set to jnp.float64, if float64 are enabled if jax.config.jax_enable_x64: - self.data_type = jnp.float64 + self._data_type = jnp.float64 else: - self.data_type = jnp.float32 + self._data_type = jnp.float32 else: - self.data_type = data_type + self._data_type = data_type self.score_type = score_type self.baseline_link_fr_ = None @@ -121,7 +121,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - (X,) = self._convert_to_jnp_ndarray(X, data_type=self.data_type) + (X,) = self._convert_to_jnp_ndarray(X, data_type=self._data_type) # check input dimensionality self._check_input_dimensionality(X=X) @@ -231,7 +231,7 @@ def score( Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - X, y = self._convert_to_jnp_ndarray(X, y, data_type=self.data_type) + X, y = self._convert_to_jnp_ndarray(X, y, data_type=self._data_type) self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) @@ -283,12 +283,18 @@ def fit( - If `init_params[i]` cannot be converted to jnp.ndarray for all i """ # convert to jnp.ndarray & perform checks - X, y, init_params = self._preprocess_fit(X, y, init_params, data_type=self.data_type) + X, y, init_params = self._preprocess_fit(X, y, init_params, data_type=self._data_type) # send to device X, y = self.device_put(X, y, device=device) init_params = self.device_put(*init_params, device=device) + # Make sure mask is of the same floating type, + # and put to the correct device. + if isinstance(self.solver, GroupLassoSolver): + self.solver.mask = jnp.asarray(self.solver.mask, dtype=self._data_type) + self.solver.mask = self.device_put(self.solver.mask, device=device) + # Run optimization runner = self.solver.instantiate_solver(self._score) params, state = runner(init_params, X, y) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index d77a434b..eb860815 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -222,7 +222,6 @@ def __init__( self._check_mask() def _check_mask(self): - self.mask = jnp.asarray(self.mask) if self.mask.ndim != 2: raise ValueError("`mask` must be 2-dimensional. " f"{self.mask.ndim} dimensional mask provided instead!") From 75042150d3ca7e1bf25c743816975db179d9b5a1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Sun, 10 Sep 2023 14:07:01 -0400 Subject: [PATCH 063/250] removed unused deps --- src/neurostatslib/base_class.py | 3 +- src/neurostatslib/glm.py | 48 +++++----- src/neurostatslib/proximal_operator.py | 2 +- src/neurostatslib/solver.py | 118 +++++++++++++------------ 4 files changed, 91 insertions(+), 80 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index d669ff8c..a81ececb 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -139,8 +139,7 @@ def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Dev return target_device def device_put( - self, - *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] + self, *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] ) -> Union[Any, jnp.ndarray]: """Send arrays to device. diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index c67183d1..39451491 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -10,8 +10,8 @@ from .base_class import _BaseRegressor from .exceptions import NotFittedError -from .observation_noise import NoiseModel, PoissonNoiseModel -from .solver import Solver, GroupLassoSolver +from .observation_noise import NoiseModel +from .solver import GroupLassoSolver, Solver from .utils import convolve_1d_trials @@ -22,7 +22,7 @@ def __init__( solver: Solver, score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, - **kwargs: Any + **kwargs: Any, ): super().__init__() self.noise_model = noise_model @@ -36,19 +36,21 @@ def __init__( ) if not jax.config.values["jax_enable_x64"] and (data_type == jnp.float64): - raise TypeError("JAX is currently not set up to support `jnp.float64`. " - "To enable 64-bit precision, use " - "`jax.config.update(\"jax_enable_x64\", True)` " - "before your computations.") + raise TypeError( + "JAX is currently not set up to support `jnp.float64`. " + "To enable 64-bit precision, use " + '`jax.config.update("jax_enable_x64", True)` ' + "before your computations." + ) if data_type is None: # set to jnp.float64, if float64 are enabled if jax.config.jax_enable_x64: - self._data_type = jnp.float64 + self.data_type = jnp.float64 else: - self._data_type = jnp.float32 + self.data_type = jnp.float32 else: - self._data_type = data_type + self.data_type = data_type self.score_type = score_type self.baseline_link_fr_ = None @@ -121,7 +123,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - (X,) = self._convert_to_jnp_ndarray(X, data_type=self._data_type) + (X,) = self._convert_to_jnp_ndarray(X, data_type=self.data_type) # check input dimensionality self._check_input_dimensionality(X=X) @@ -133,7 +135,7 @@ def _score( self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, - target_activity: jnp.ndarray + y: jnp.ndarray, ) -> jnp.ndarray: r"""Score the predicted firing rates against target neural activity. @@ -148,7 +150,7 @@ def _score( Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). X : The exogenous variables. Shape (n_time_bins, n_neurons, n_features). - target_activity : + y : The target activity to compare against. Shape (n_time_bins, n_neurons). Returns @@ -158,7 +160,7 @@ def _score( """ predicted_rate = self._predict(params, X) - return self.noise_model.negative_log_likelihood(predicted_rate, target_activity) + return self.noise_model.negative_log_likelihood(predicted_rate, y) def score( self, @@ -231,7 +233,7 @@ def score( Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - X, y = self._convert_to_jnp_ndarray(X, y, data_type=self._data_type) + X, y = self._convert_to_jnp_ndarray(X, y, data_type=self.data_type) self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) @@ -283,7 +285,9 @@ def fit( - If `init_params[i]` cannot be converted to jnp.ndarray for all i """ # convert to jnp.ndarray & perform checks - X, y, init_params = self._preprocess_fit(X, y, init_params, data_type=self._data_type) + X, y, init_params = self._preprocess_fit( + X, y, init_params, data_type=self.data_type + ) # send to device X, y = self.device_put(X, y, device=device) @@ -292,8 +296,8 @@ def fit( # Make sure mask is of the same floating type, # and put to the correct device. if isinstance(self.solver, GroupLassoSolver): - self.solver.mask = jnp.asarray(self.solver.mask, dtype=self._data_type) - self.solver.mask = self.device_put(self.solver.mask, device=device) + self.solver.mask = jnp.asarray(self.solver.mask, dtype=self.data_type) + self.solver.mask = self.device_put(self.solver.mask, device=device)[0] # Run optimization runner = self.solver.instantiate_solver(self._score) @@ -411,8 +415,8 @@ def simulate( self._check_input_dimensionality(feedforward_input, init_y) if ( - feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] - != Ws.shape[1] + feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] + != Ws.shape[1] ): raise ValueError( "The number of feed forward input features " @@ -424,7 +428,7 @@ def simulate( ) self._check_input_and_params_consistency( - (Ws[:, n_basis_coupling * n_neurons:], bs), + (Ws[:, n_basis_coupling * n_neurons :], bs), X=feedforward_input, y=init_y, ) @@ -446,7 +450,7 @@ def simulate( subkeys = jax.random.split(random_key, num=n_timesteps) def scan_fn( - data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray + data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: """Scan over time steps and simulate spikes and firing rates. diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index 2ec1158a..39d4d254 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Tuple +from typing import Tuple import jax import jax.numpy as jnp diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index eb860815..adda061e 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -63,6 +63,22 @@ def instantiate_solver( ]: pass + def get_runner( + self, + solver_kwargs: dict, + run_kwargs: dict, + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep + ]: + solver = getattr(jaxopt, self.solver_name)(**solver_kwargs) + + def solver_run( + init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray + ) -> jaxopt.OptStep: + return solver.run(init_params, X=X, y=y, **run_kwargs) + + return solver_run + class UnRegularizedSolver(Solver): allowed_solvers = [ @@ -86,15 +102,9 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_is_callable(loss) - solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) - - def solver_run( - init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray - ) -> jaxopt.OptStep: - return solver.run(init_params, X=X, y=y) - - return solver_run + solver_kwargs = self.solver_kwargs.copy() + solver_kwargs["fun"] = loss + return self.get_runner(solver_kwargs, {}) class RidgeSolver(Solver): @@ -120,22 +130,18 @@ def instantiate_solver( self, loss: Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray - ] + ], ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: self._check_is_callable(loss) + def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) - solver = getattr(jaxopt, self.solver_name)(fun=penalized_loss, **self.solver_kwargs) - - def solver_run( - init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray - ) -> jaxopt.OptStep: - return solver.run(init_params, X=X, y=y) - - return solver_run + solver_kwargs = self.solver_kwargs.copy() + solver_kwargs["fun"] = penalized_loss + return self.get_runner(solver_kwargs, {}) class ProxGradientSolver(Solver, abc.ABC): @@ -152,12 +158,11 @@ def __init__( self.mask = mask @abc.abstractmethod - def prox_operator( + def get_prox_operator( self, - params: Tuple[jnp.ndarray, jnp.ndarray], - l2reg: float, - scaling: float = 1.0, - ) -> Tuple[jnp.ndarray, jnp.ndarray]: + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] + ]: pass def instantiate_solver( @@ -168,22 +173,15 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_is_callable(loss) - def loss_kwarg(params, X=jnp.zeros(1), y=jnp.zeros(1)): - return loss(params, X, y) - - solver = getattr(jaxopt, self.solver_name)( - fun=loss_kwarg, prox=self.prox_operator, **self.solver_kwargs - ) + solver_kwargs = self.solver_kwargs.copy() + solver_kwargs["fun"] = loss + solver_kwargs["prox"] = self.get_prox_operator() - def solver_run( - init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray - ) -> jaxopt.OptStep: - return solver.run(init_params, X=X, y=y, hyperparams_prox=self.alpha) + run_kwargs = dict(hyperparams_prox=self.alpha) - return solver_run + return self.get_runner(solver_kwargs, run_kwargs) class LassoSolver(ProxGradientSolver): @@ -192,20 +190,22 @@ def __init__( solver_name: str, solver_kwargs: Optional[dict] = None, alpha: float = 1.0, - mask: Optional[Union[NDArray, jnp.ndarray]] = None, + mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): super().__init__( solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask ) - def prox_operator( + def get_prox_operator( self, - params: Tuple[jnp.ndarray, jnp.ndarray], - alpha: float, - scaling: float = 1.0, - ) -> Tuple[jnp.ndarray, jnp.ndarray]: - Ws, bs = params - return jaxopt.prox.prox_lasso(Ws, l1reg=alpha, scaling=scaling), bs + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] + ]: + def prox_op(params, l1reg, scaling=1.0): + Ws, bs = params + return jaxopt.prox.prox_lasso(Ws, l1reg, scaling=scaling), bs + + return prox_op class GroupLassoSolver(ProxGradientSolver): @@ -223,8 +223,10 @@ def __init__( def _check_mask(self): if self.mask.ndim != 2: - raise ValueError("`mask` must be 2-dimensional. " - f"{self.mask.ndim} dimensional mask provided instead!") + raise ValueError( + "`mask` must be 2-dimensional. " + f"{self.mask.ndim} dimensional mask provided instead!" + ) if self.mask.shape[0] == 0: raise ValueError(f"Empty mask provided! Mask has shape {self.mask.shape}.") @@ -236,17 +238,23 @@ def _check_mask(self): raise ValueError("Empty mask provided!") if jnp.any(self.mask.sum(axis=0) > 1): - raise ValueError("Incorrect group assignment. Some of the features are assigned " - "to more then one group.") + raise ValueError( + "Incorrect group assignment. Some of the features are assigned " + "to more then one group." + ) if not jnp.issubdtype(self.mask.dtype, jnp.floating): - raise ValueError("Mask should be a floating point jnp.ndarray. " - f"Data type {self.mask.dtype} provided instead!") + raise ValueError( + "Mask should be a floating point jnp.ndarray. " + f"Data type {self.mask.dtype} provided instead!" + ) - def prox_operator( + def get_prox_operator( self, - params: Tuple[jnp.ndarray, jnp.ndarray], - alpha: float, - scaling: float = 1.0, - ) -> Tuple[jnp.ndarray, jnp.ndarray]: - return prox_group_lasso(params, alpha=alpha, mask=self.mask, scaling=scaling) + ) -> Callable[ + [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] + ]: + def prox_op(params, alpha, scaling=1.0): + return prox_group_lasso(params, alpha, mask=self.mask, scaling=scaling) + + return prox_op From f341c86ee5d10f9888fc638ccda15ac6fba06159 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Sep 2023 19:09:35 -0400 Subject: [PATCH 064/250] glm edited after discussion --- src/neurostatslib/__init__.py | 2 +- src/neurostatslib/base_class.py | 38 +- src/neurostatslib/glm.py | 2052 +++++++++-------- .../{observation_noise.py => noise_model.py} | 10 +- src/neurostatslib/solver.py | 17 +- tests/conftest.py | 36 +- tests/test_glm.py | 134 +- ...servation_noise.py => test_noise_model.py} | 2 +- 8 files changed, 1160 insertions(+), 1131 deletions(-) rename src/neurostatslib/{observation_noise.py => noise_model.py} (95%) rename tests/{test_observation_noise.py => test_noise_model.py} (98%) diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index 37792f85..b6f82569 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -5,7 +5,7 @@ basis, exceptions, glm, - observation_noise, + noise_model, sample_points, solver, utils, diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index a81ececb..5f2aa80a 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -218,7 +218,11 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: @abc.abstractmethod def score( - self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray] + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + # may include score_type or other additional model dependent kwargs + **kwargs, ) -> jnp.ndarray: pass @@ -226,11 +230,10 @@ def score( def simulate( self, random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_spikes: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray], - feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + feed_forward_input: Union[NDArray, jnp.ndarray], device: Literal["cpu", "gpu", "tpu"] = "cpu", + # feed-forward input and/coupling basis + **kwargs, ): pass @@ -431,7 +434,7 @@ def _preprocess_fit( if self._has_invalid_entry(X): raise ValueError("Input X contains a NaNs or Infs!") - elif self._has_invalid_entry(y): + if self._has_invalid_entry(y): raise ValueError("Input y contains a NaNs or Infs!") _, n_neurons = y.shape @@ -453,3 +456,26 @@ def _preprocess_fit( self._check_input_and_params_consistency(init_params, X, y) return X, y, init_params + + def _preprocess_simulate( + self, + feedforward_input: Union[NDArray, jnp.ndarray], + params_f: Tuple[jnp.ndarray, jnp.ndarray], + init_y: Optional[jnp.ndarray] = None, + params_r: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + data_type: jnp.dtype = jnp.float32, + ) -> Tuple[jnp.ndarray, ...]: + (feedforward_input,) = self._convert_to_jnp_ndarray( + feedforward_input, data_type=data_type + ) + self._check_input_dimensionality(X=feedforward_input) + self._check_input_and_params_consistency(params_f, X=feedforward_input) + + if self._has_invalid_entry(feedforward_input): + raise ValueError("feedforward_input contains a NaNs or Infs!") + if init_y is not None: + self._check_input_dimensionality(y=init_y) + self._check_input_and_params_consistency(params_r, y=init_y) + return feedforward_input, init_y + + return (feedforward_input,) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 39451491..902e2bfe 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,40 +1,43 @@ """GLM core module.""" -import abc -import inspect -from typing import Any, Callable, Literal, Optional, Tuple, Type, Union +from typing import Any, Literal, Optional, Tuple, Type, Union import jax import jax.numpy as jnp -import jaxopt -from numpy.typing import ArrayLike, NDArray +from numpy.typing import NDArray +from . import noise_model as nsm +from . import solver as slv from .base_class import _BaseRegressor from .exceptions import NotFittedError -from .observation_noise import NoiseModel -from .solver import GroupLassoSolver, Solver from .utils import convolve_1d_trials class GLM(_BaseRegressor): def __init__( self, - noise_model: NoiseModel, - solver: Solver, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", + noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), + solver: slv.Solver = slv.RidgeSolver("GradientDescent"), data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, **kwargs: Any, ): super().__init__() + + if solver.__class__.__name__ not in slv.__all__: + raise TypeError( + "The provided `solver` should be one of the implemented solvers in `neurostatslib.solver`. " + f"Available options are: {slv.__all__}." + ) + + if noise_model.__class__.__name__ not in nsm.__all__: + raise TypeError( + "The provided `noise_model` should be one of the implemented models in `neurostatslib.noise_model`. " + f"Available options are: {nsm.__all__}." + ) + self.noise_model = noise_model self.solver = solver self.inverse_link_function = noise_model.inverse_link_function - if score_type not in ["log-likelihood", "pseudo-r2"]: - raise NotImplementedError( - f"Scoring method {score_type} not implemented! " - f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." - ) - if not jax.config.values["jax_enable_x64"] and (data_type == jnp.float64): raise TypeError( "JAX is currently not set up to support `jnp.float64`. " @@ -52,7 +55,6 @@ def __init__( else: self.data_type = data_type - self.score_type = score_type self.baseline_link_fr_ = None self.basis_coeff_ = None # scale parameter (=1 for poisson and Gaussian, needs to be estimated for Gamma) @@ -60,7 +62,7 @@ def __init__( self.scale = 1.0 self.solver_state = None - def _check_is_fit(self): + def _check_is_fit(self): # check scale. """Ensure the instance has been fitted.""" if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): raise NotFittedError( @@ -131,7 +133,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: self._check_input_and_params_consistency((Ws, bs), X=X) return self._predict((Ws, bs), X) - def _score( + def _score( # call _negative_log_likelihood self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, @@ -166,10 +168,11 @@ def score( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], + score_type: Literal["log-likelihood", "pseudo-r2"] = "pseudo-r2", ) -> jnp.ndarray: r"""Score the predicted firing rates (based on fit) to the target spike counts. - This computes the GLM mean log-likelihood or the pseudo-$R^2$, thus the higher the + This computes the GLM pseudo-$R^2$ or the mean log-likelihood, thus the higher the number the better. The pseudo-$R^2$ can be computed as follows, @@ -193,6 +196,8 @@ def score( y : Neural activity arranged in a matrix. n_neurons must be the same as during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). + score_type : + Type of scoring: either log-likelihood or pseudo-r2. Returns ------- @@ -218,7 +223,7 @@ def score( of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. - Refer to the `nsl.observation_noise.NoiseModel` concrete subclasses for the specific likelihood equations. + Refer to the `nsl.noise_model.NoiseModel` concrete subclasses for the specific likelihood equations. References @@ -227,6 +232,11 @@ def score( Routledge, 2013. """ + if score_type not in ["log-likelihood", "pseudo-r2"]: + raise NotImplementedError( + f"Scoring method {score_type} not implemented! " + f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." + ) # ignore the last time point from predict, because that corresponds to # the next time step, which we have no observed data for self._check_is_fit() @@ -238,7 +248,7 @@ def score( self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) self._check_input_and_params_consistency((Ws, bs), X=X, y=y) - if self.score_type == "log-likelihood": + if score_type == "log-likelihood": norm_constant = jax.scipy.special.gammaln(y + 1).mean() score = -self._score((Ws, bs), X, y) - norm_constant else: @@ -295,13 +305,16 @@ def fit( # Make sure mask is of the same floating type, # and put to the correct device. - if isinstance(self.solver, GroupLassoSolver): + if isinstance(self.solver, slv.GroupLassoSolver): self.solver.mask = jnp.asarray(self.solver.mask, dtype=self.data_type) self.solver.mask = self.device_put(self.solver.mask, device=device)[0] # Run optimization runner = self.solver.instantiate_solver(self._score) params, state = runner(init_params, X, y) + # if any noise model other than Poisson are used + # one should set the scale parameter too. + # self.noise_model.set_scale(params) if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): raise ValueError( @@ -320,11 +333,39 @@ def fit( def simulate( self, random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_y: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray], - feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, + feedforward_input: Union[NDArray, jnp.ndarray], + device: Literal["cpu", "gpu", "tpu"] = "cpu", + # feed-forward input and/coupling basis + **kwargs, + ): + # check if the model is fit + self._check_is_fit() + Ws, bs = self.basis_coeff_, self.baseline_link_fr_ + (feedforward_input,) = self._preprocess_simulate( + feedforward_input, params_f=(Ws, bs) + ) + + return self.noise_model.emission_probability( + key=random_key, predicted_rate=self._predict((Ws, bs), feedforward_input) + ) + + +class GLMRecurrent(GLM): + def __init__( + self, + noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), + solver: slv.Solver = slv.RidgeSolver(), + data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, + ): + super().__init__(noise_model=noise_model, solver=solver, data_type=data_type) + + def simulate( + self, + random_key: jax.random.PRNGKeyArray, + feedforward_input: Union[NDArray, jnp.ndarray], device: Literal["cpu", "gpu", "tpu"] = "cpu", + coupling_basis_matrix: Union[NDArray, jnp.ndarray] = None, + init_y: Union[NDArray, jnp.ndarray] = None, ): """ Simulate spike trains using the GLM as a recurrent network. @@ -338,8 +379,7 @@ def simulate( ---------- random_key : PRNGKey for seeding the simulation. - n_timesteps : - Duration of the simulation in terms of time steps. + init_y : Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). @@ -356,10 +396,10 @@ def simulate( Returns ------- simulated_obs : - Simulated observations (spike counts for PoissonGLMs) for each neuron over time. + Simulated activity (spike counts for PoissonGLMs) for each neuron over time. Shape: (n_neurons, n_timesteps). firing_rates : - Simulated firing rates for each neuron over time. + Simulated rates for each neuron over time. Shape: (n_neurons, n_timesteps). Raises @@ -387,49 +427,59 @@ def simulate( The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure consistency in the model's input feature dimensionality. """ + + if coupling_basis_matrix is None: + raise ValueError( + "GLMRecurrent simulate method requires a coupling basis" + " matrix in order to generate spikes!" + ) + if init_y is None: + raise ValueError( + "GLMRecurrent simulate method requires the initial activity " + "init_y to start-off the simulation!" + ) + # check if the model is fit self._check_is_fit() # convert to jnp.ndarray - init_y, coupling_basis_matrix, feedforward_input = self._convert_to_jnp_ndarray( - init_y, coupling_basis_matrix, feedforward_input, data_type=jnp.float32 - ) - - # Transfer data to the target device - init_y, coupling_basis_matrix, feedforward_input = self.device_put( - init_y, coupling_basis_matrix, feedforward_input, device=device + (coupling_basis_matrix,) = self._convert_to_jnp_ndarray( + coupling_basis_matrix, data_type=self.data_type ) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] - # add an empty input (simulate with coupling-only) - if feedforward_input is None: - feedforward_input = jnp.zeros( - (n_timesteps, n_neurons, 0), dtype=jnp.float32 - ) - - Ws = self.basis_coeff_ + # split weights in recurrent and feed-forward + Wf = self.basis_coeff_[:, n_basis_coupling * n_neurons :] + Wr = self.basis_coeff_[:, : n_basis_coupling * n_neurons] bs = self.baseline_link_fr_ - self._check_input_dimensionality(feedforward_input, init_y) + feedforward_input, init_y = self._preprocess_simulate( + feedforward_input, params_f=(Wf, bs), init_y=init_y, params_r=(Wr, bs) + ) + + # Transfer data to the target device + init_y, coupling_basis_matrix, feedforward_input = self.device_put( + init_y, coupling_basis_matrix, feedforward_input, device=device + ) if ( feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] - != Ws.shape[1] + != self.basis_coeff_.shape[1] ): raise ValueError( "The number of feed forward input features " "and the number of recurrent features must add up to " "the overall model features." - f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " + f"The total number of feature of the model is {self.basis_coeff_.shape[1]}." + f" {feedforward_input.shape[1]} " f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " f"provided instead." ) self._check_input_and_params_consistency( - (Ws[:, n_basis_coupling * n_neurons :], bs), - X=feedforward_input, + (Wr, bs), y=init_y, ) @@ -441,13 +491,9 @@ def simulate( f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" ) - if feedforward_input.shape[0] != n_timesteps: - raise ValueError( - "`feedforward_input` must be of length `n_timesteps`. " - f"`feedforward_input` has length {len(feedforward_input)}, " - f"`n_timesteps` is {n_timesteps} instead!" - ) - subkeys = jax.random.split(random_key, num=n_timesteps) + subkeys = jax.random.split(random_key, num=feedforward_input.shape[0]) + # (n_samples, n_neurons) + feed_forward_contrib = jnp.einsum("ik,tik->ti", Wf, feedforward_input) def scan_fn( data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray @@ -464,19 +510,22 @@ def scan_fn( # Extract the corresponding slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( - feedforward_input, - (chunk, 0, 0), - (1, feedforward_input.shape[1], feedforward_input.shape[2]), + feed_forward_contrib, + (chunk, 0), + (1, feed_forward_contrib.shape[1]), ) # Reshape the convolved spikes and concatenate with the input slice to form the model input conv_spk = jnp.tile( conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) - X = jnp.concatenate([conv_spk, input_slice], axis=2) # Predict the firing rate using the model coefficients - firing_rate = self._predict((Ws, bs), X) + # Doesn't use predict because the non-linearity needs + # to be applied after we add the feed forward input + firing_rate = self.inverse_link_function( + jnp.einsum("ik,tik->ti", Wr, conv_spk) + input_slice + bs[None, :] + ) # Simulate activity based on the predicted firing rate new_spikes = self.noise_model.emission_probability(key, firing_rate) @@ -490,942 +539,943 @@ def scan_fn( return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) -class _BaseGLM(_BaseRegressor, abc.ABC): - """Abstract base class for Poisson GLMs. - - Provides methods for score computation, simulation, and prediction. - Must be subclassed with a method for fitting to data. - - Parameters - ---------- - solver_name - Name of the solver to use when fitting the GLM. Must be an attribute of - ``jaxopt``. - solver_kwargs - Dictionary of keyword arguments to pass to the solver during its - initialization. - inverse_link_function - Function to transform outputs of convolution with basis to firing rate. - Must accept any number as input and return all non-negative values. - kwargs: - Additional keyword arguments. ``kwargs`` may depend on the concrete - subclass implementation (e.g. alpha, the regularization hyperparamter, will be present for - penalized GLMs but not for the un-penalized case). - - """ - - def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - **kwargs, - ): - self.solver_name = solver_name - try: - solver_args = inspect.getfullargspec(getattr(jaxopt, solver_name)).args - except AttributeError: - raise AttributeError( - f"module jaxopt has no attribute {solver_name}, pick a different solver!" - ) - - undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) - if undefined_kwargs: - raise NameError( - f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" - ) - - if score_type not in ["log-likelihood", "pseudo-r2"]: - raise NotImplementedError( - f"Scoring method {score_type} not implemented! " - f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." - ) - self.score_type = score_type - self.solver_kwargs = solver_kwargs - - if not callable(inverse_link_function): - raise ValueError("inverse_link_function must be a callable!") - - self.inverse_link_function = inverse_link_function - # set additional kwargs e.g. regularization hyperparameters and so on... - super().__init__(**kwargs) - # initialize parameters to None - self.baseline_link_fr_ = None - self.basis_coeff_ = None - - def _predict( - self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray - ) -> jnp.ndarray: - """ - Predict firing rates given predictors and parameters. - - Parameters - ---------- - params : - Tuple containing the spike basis coefficients and bias terms. - X : - Predictors. Shape (n_time_bins, n_neurons, n_features). - - Returns - ------- - jnp.ndarray - The predicted firing rates. Shape (n_time_bins, n_neurons). - """ - Ws, bs = params - return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) - - @abc.abstractmethod - def residual_deviance(self, predicted_rate, y): - r"""Compute the residual deviance for a GLM model. - - Parameters - ---------- - predicted_rate: - The predicted rate of the GLM. - y: - The observations. - - Returns - ------- - The residual deviance of the model. - - Notes - ----- - Deviance is a measure of the goodness of fit of a statistical model. - For a Poisson model, the residual deviance is computed as: - - $$ - \begin{aligned} - D(y, \hat{y}) &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) - \end{aligned} - $$ - where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model - log-likelihood. Lower values of deviance indicate a better fit. - - """ - pass - - def _pseudo_r2(self, params, X, y): - r"""Pseudo-R^2 calculation for a GLM. - - The Pseudo-R^2 metric gives a sense of how well the model fits the data, - relative to a null (or baseline) model. - - Parameters - ---------- - params : - Tuple containing the spike basis coefficients and bias terms. - X: - The predictors. - y: - The neural activity. - - Returns - ------- - : - The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, - whereas a value closer to 0 suggests that the model doesn't improve much over the null model. - - """ - mu = self._predict(params, X) - res_dev_t = self.residual_deviance(mu, y) - resid_deviance = jnp.sum(res_dev_t**2) - - null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() - null_dev_t = self.residual_deviance(null_mu, y) - null_deviance = jnp.sum(null_dev_t**2) - - return (null_deviance - resid_deviance) / null_deviance - - def _check_is_fit(self): - """Ensure the instance has been fitted.""" - if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): - raise NotFittedError( - "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." - ) - - def _safe_predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: - """Predict firing rates based on fit parameters. - - Parameters - ---------- - X : - The exogenous variables. Shape (n_time_bins, n_neurons, n_features). - - Returns - ------- - predicted_firing_rates : jnp.ndarray - The predicted firing rates with shape (n_neurons, n_time_bins). - - Raises - ------ - NotFittedError - If ``fit`` has not been called first with this instance. - ValueError - - If `params` is not a JAX pytree of size two. - - If weights and bias terms in `params` don't have the expected dimensions. - - If the number of neurons in the model parameters and in the inputs do not match. - - If `X` is not three-dimensional. - - If there's an inconsistent number of features between spike basis coefficients and `X`. - - See Also - -------- - score - Score predicted firing rates against target spike counts. - """ - # check that the model is fitted - self._check_is_fit() - # extract model params - Ws = self.basis_coeff_ - bs = self.baseline_link_fr_ - - (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) - - # check input dimensionality - self._check_input_dimensionality(X=X) - # check consistency between X and params - self._check_input_and_params_consistency((Ws, bs), X=X) - - return self._predict((Ws, bs), X) - - def _safe_score( - self, - X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray], - score_func: Callable[ - [jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]], jnp.ndarray - ], - score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None, - ) -> jnp.ndarray: - r"""Score the predicted firing rates (based on fit) to the target spike counts. - - This computes the GLM mean log-likelihood or the pseudo-$R^2$, thus the higher the - number the better. - - The pseudo-$R^2$ can be computed as follows, - - $$ - \begin{aligned} - R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ - &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) - - \log \text{LL}(\bar{\lambda}| y)}, - \end{aligned} - $$ - - where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is - the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model - predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate. See [1]. - - Parameters - ---------- - X : - The exogenous variables. Shape (n_time_bins, n_neurons, n_features) - y : - Neural activity arranged in a matrix. n_neurons must be the same as - during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). - score_type: - String indicating the type of scoring to return. Options are: - - `log-likelihood` for the model log-likelihood. - - `pseudo-r2` for the model pseudo-$R^2$. - Default is defined at class initialization. - - Returns - ------- - score : (1,) - The Poisson log-likelihood or the pseudo-$R^2$ of the current model. - - Raises - ------ - NotFittedError - If ``fit`` has not been called first with this instance. - ValueError - If attempting to simulate a different number of neurons than were - present during fitting (i.e., if ``init_y.shape[0] != - self.baseline_link_fr_.shape[0]``). - - Notes - ----- - The log-likelihood is not on a standard scale, its value is influenced by many factors, - among which the number of model parameters. The log-likelihood can assume both positive - and negative values. - - The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure - of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. - The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. - - Refer to the concrete subclass docstrings `_score` for the specific likelihood equations. - - References - ---------- - [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. - Routledge, 2013. - - """ - # ignore the last time point from predict, because that corresponds to - # the next time step, which we have no observed data for - self._check_is_fit() - Ws = self.basis_coeff_ - bs = self.baseline_link_fr_ - - X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) - - self._check_input_dimensionality(X, y) - self._check_input_n_timepoints(X, y) - self._check_input_and_params_consistency((Ws, bs), X=X, y=y) - - if score_type is None: - score_type = self.score_type - - if score_type == "log-likelihood": - score = -(score_func(X, y, (Ws, bs))) - elif score_type == "pseudo-r2": - score = self._pseudo_r2((Ws, bs), X, y) - else: - # this should happen only if one manually set score_type - raise NotImplementedError( - f"Scoring method {score_type} not implemented! " - f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." - ) - return score - - def _safe_fit( - self, - X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray], - loss: Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32 - ], - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "gpu", - ): - """Fit GLM to neuroal activity. - - Following scikit-learn API, the solutions are stored as attributes - ``basis_coeff_`` and ``baseline_link_fr``. - - Parameters - ---------- - X : - Predictors, shape (n_time_bins, n_neurons, n_features) - y : - Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). - loss: - The loss function to be minimized. - init_params : - Initial values for the spike basis coefficients and bias terms. If - None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) - device: - Device used for optimizing model parameters. - - Raises - ------ - ValueError - - If `init_params` is not of length two. - - If dimensionality of `init_params` are not correct. - - If the number of neurons in the model parameters and in the inputs do not match. - - If `X` is not three-dimensional. - - If spike_data is not two-dimensional. - - If solver returns at least one NaN parameter, which means it found - an invalid solution. Try tuning optimization hyperparameters. - TypeError - - If `init_params` are not array-like - - If `init_params[i]` cannot be converted to jnp.ndarray for all i - """ - # convert to jnp.ndarray & perform checks - X, y, init_params = self._preprocess_fit(X, y, init_params) - - # send to device - target_device = self.select_target_device(device) - X, y = self.device_put(X, y, device=target_device) - init_params = self.device_put(*init_params, device=target_device) - - # Run optimization - solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) - params, state = solver.run(init_params, X=X, y=y) - - if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): - raise ValueError( - "Solver returned at least one NaN parameter, so solution is invalid!" - " Try tuning optimization hyperparameters." - ) - - # Store parameters - self.basis_coeff_ = params[0] - self.baseline_link_fr_ = params[1] - # note that this will include an error value, which is not the same as - # the output of loss. I believe it's the output of - # solver.l2_optimality_error - self.solver_state = state - self.solver = solver - - def _safe_simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_y: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray], - random_function: Callable[[jax.random.PRNGKeyArray, ArrayLike], jnp.ndarray], - feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu", - ) -> Tuple[jnp.ndarray, jnp.ndarray]: - """ - Simulate spike trains using the GLM as a recurrent network. - - This function projects neural activity into the future, employing the fitted - parameters of the GLM. It is capable of simulating activity based on a combination - of historical spike activity and external feedforward inputs like convolved currents, light - intensities, etc. - - Parameters - ---------- - random_key : - PRNGKey for seeding the simulation. - n_timesteps : - Duration of the simulation in terms of time steps. - init_y : - Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. - Expected shape: (window_size, n_neurons). - coupling_basis_matrix : - Basis matrix for coupling, representing between-neuron couplings - and auto-correlations. Expected shape: (window_size, n_basis_coupling). - random_function : - A probability emission function, like jax.random.poisson, which takes as input a random.PRNGKeyArray - and the mean rate, and samples observations, (spike counts for a poisson). - feedforward_input : - External input matrix to the model, representing factors like convolved currents, - light intensities, etc. When not provided, the simulation is done with coupling-only. - Expected shape: (n_timesteps, n_neurons, n_basis_input). - device : - Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. - - Returns - ------- - simulated_obs : - Simulated observations (spike counts for PoissonGLMs) for each neuron over time. - Shape: (n_neurons, n_timesteps). - firing_rates : - Simulated firing rates for each neuron over time. - Shape: (n_neurons, n_timesteps). - - Raises - ------ - NotFittedError - If the model hasn't been fitted prior to calling this method. - ValueError - - If the instance has not been previously fitted. - - If there's an inconsistency between the number of neurons in model parameters. - - If the number of neurons in input arguments doesn't match with model parameters. - - For an invalid computational device selection. - - - See Also - -------- - predict : Method to predict firing rates based on the model's parameters. - - Notes - ----- - The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients - (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. - The remaining coefficients correspond to the weights for the feed-forward input. - - - The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` - to ensure consistency in the model's input feature dimensionality. - """ - target_device = self.select_target_device(device) - # check if the model is fit - self._check_is_fit() - - # convert to jnp.ndarray - init_y, coupling_basis_matrix, feedforward_input = self._convert_to_jnp_ndarray( - init_y, coupling_basis_matrix, feedforward_input, data_type=jnp.float32 - ) - - # Transfer data to the target device - init_y, coupling_basis_matrix, feedforward_input = self.device_put( - init_y, coupling_basis_matrix, feedforward_input, device=target_device - ) - - n_basis_coupling = coupling_basis_matrix.shape[1] - n_neurons = self.baseline_link_fr_.shape[0] - - # add an empty input (simulate with coupling-only) - if feedforward_input is None: - feedforward_input = jnp.zeros( - (n_timesteps, n_neurons, 0), dtype=jnp.float32 - ) - - Ws = self.basis_coeff_ - bs = self.baseline_link_fr_ - - self._check_input_dimensionality(feedforward_input, init_y) - - if ( - feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] - != Ws.shape[1] - ): - raise ValueError( - "The number of feed forward input features " - "and the number of recurrent features must add up to " - "the overall model features." - f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " - f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " - f"provided instead." - ) - - self._check_input_and_params_consistency( - (Ws[:, n_basis_coupling * n_neurons :], bs), - X=feedforward_input, - y=init_y, - ) - - if init_y.shape[0] != coupling_basis_matrix.shape[0]: - raise ValueError( - "`init_y` and `coupling_basis_matrix`" - " should have the same window size! " - f"`init_y` window size: {init_y.shape[1]}, " - f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" - ) - - if feedforward_input.shape[0] != n_timesteps: - raise ValueError( - "`feedforward_input` must be of length `n_timesteps`. " - f"`feedforward_input` has length {len(feedforward_input)}, " - f"`n_timesteps` is {n_timesteps} instead!" - ) - subkeys = jax.random.split(random_key, num=n_timesteps) - - def scan_fn( - data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray - ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: - """Scan over time steps and simulate spikes and firing rates. - - This function simulates the spikes and firing rates for each time step - based on the previous spike data, feedforward input, and model coefficients. - """ - spikes, chunk = data - - # Convolve the spike data with the coupling basis matrix - conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] - - # Extract the corresponding slice of the feedforward input for the current time step - input_slice = jax.lax.dynamic_slice( - feedforward_input, - (chunk, 0, 0), - (1, feedforward_input.shape[1], feedforward_input.shape[2]), - ) - - # Reshape the convolved spikes and concatenate with the input slice to form the model input - conv_spk = jnp.tile( - conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] - ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) - X = jnp.concatenate([conv_spk, input_slice], axis=2) - - # Predict the firing rate using the model coefficients - firing_rate = self._predict((Ws, bs), X) - - # Simulate spikes based on the predicted firing rate - new_spikes = random_function(key, firing_rate) - - # Prepare the spikes for the next iteration (keeping the most recent spikes) - concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 - return concat_spikes, (new_spikes, firing_rate) - - _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) - simulated_spikes, firing_rates = outputs - return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) - - -class PoissonGLM(_BaseGLM): - """Un-regularized Poisson-GLM. - - The class fits the un-penalized maximum likelihood Poisson GLM parameter estimate. - - Parameters - ---------- - solver_name - Name of the solver to use when fitting the GLM. Must be an attribute of - ``jaxopt``. - solver_kwargs - Dictionary of keyword arguments to pass to the solver during its - initialization. - inverse_link_function - Function to transform outputs of convolution with basis to firing rate. - Must accept any number as input and return all non-negative values. - - Attributes - ---------- - solver - jaxopt solver, set during ``fit()`` - solver_state - state of the solver, set during ``fit()`` - basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) - Solutions for the spike basis coefficients, set during ``fit()`` - baseline_link_fr : jnp.ndarray, (n_neurons,) - Solutions for bias terms, set during ``fit()`` - """ - - def __init__( - self, - solver_name: str = "GradientDescent", - solver_kwargs: dict = dict(), - inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - ): - super().__init__( - solver_name=solver_name, - solver_kwargs=solver_kwargs, - inverse_link_function=inverse_link_function, - score_type=score_type, - ) - - def _score( - self, - X: jnp.ndarray, - target_spikes: jnp.ndarray, - params: Tuple[jnp.ndarray, jnp.ndarray], - ) -> jnp.ndarray: - r"""Score the predicted firing rates against target spike counts. - - This computes the Poisson negative log-likelihood up to a constant. - - Note that you can end up with infinities in here if there are zeros in - ``predicted_firing_rates``. We raise a warning in that case. - - The formula for the Poisson mean log-likelihood is the following, - - $$ - \begin{aligned} - \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} - [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ - &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] - \end{aligned} - $$ - - Because $\Gamma(k+1)=k!$, see - https://en.wikipedia.org/wiki/Gamma_function. - - Parameters - ---------- - X : - The exogenous variables. Shape (n_time_bins, n_neurons, n_features). - target_spikes : - The target spikes to compare against. Shape (n_time_bins, n_neurons). - params : - Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). - - Returns - ------- - : - The Poisson negative log-likehood. Shape (1,). - - Notes - ----- - The Poisson probability mass function is: - - $$ - \frac{\lambda^k \exp(-\lambda)}{k!} - $$ - - But the $k!$ term is not a function of the parameters and can be disregarded - when computing the loss-function. Thus, the negative log of it is: - - $$ - -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] - &= -k\log(\lambda)-\lambda + \text{const} - $$ - - """ - # Avoid the edge-case of 0*log(0), much faster than - # where on large arrays. - predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) - x = target_spikes * jnp.log(predicted_firing_rates) - # see above for derivation of this. - return jnp.mean(predicted_firing_rates - x) - - def residual_deviance( - self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray - ) -> jnp.ndarray: - r"""Compute the residual deviance for a Poisson model. - - Parameters - ---------- - predicted_rate: - The predicted firing rates. - spike_counts: - The spike counts. - - Returns - ------- - : - The residual deviance of the model. - - Notes - ----- - Deviance is a measure of the goodness of fit of a statistical model. - For a Poisson model, the residual deviance is computed as: - - $$ - \begin{aligned} - D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) - - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ - &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) - \end{aligned} - $$ - where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model - log-likelihood. Lower values of deviance indicate a better fit. - - """ - # this takes care of 0s in the log - ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) - resid_dev = 2 * ( - spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) - ) - return resid_dev - - def predict(self, X: Union[NDArray, jnp.ndarray]): - """Predict firing rates based on fit parameters. - - Parameters - ---------- - X : - The exogenous variables. Shape (n_time_bins, n_neurons, n_features). - - Returns - ------- - predicted_firing_rates : jnp.ndarray - The predicted firing rates with shape (n_neurons, n_time_bins). - - Raises - ------ - NotFittedError - If ``fit`` has not been called first with this instance. - ValueError - - If `params` is not a JAX pytree of size two. - - If weights and bias terms in `params` don't have the expected dimensions. - - If the number of neurons in the model parameters and in the inputs do not match. - - If `X` is not three-dimensional. - - If there's an inconsistent number of features between spike basis coefficients and `X`. - - See Also - -------- - [score](../glm/#neurostatslib.glm.PoissonGLM.score) - Score predicted firing rates against target spike counts. - """ - return self._safe_predict(X) - - def score( - self, - X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray], - score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", - ) -> jnp.ndarray: - r"""Score the predicted firing rates (based on fit) to the target spike counts. - - This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the - number the better. - - The formula for the mean log-likelihood is the following, - - $$ - \begin{aligned} - \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} - [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ - &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] - \end{aligned} - $$ - - Because $\Gamma(k+1)=k!$, see - https://en.wikipedia.org/wiki/Gamma_function. - - The pseudo-$R^2$ can be computed as follows, - - $$ - \begin{aligned} - R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ - &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) - - \log \text{LL}(\bar{\lambda}| y)}, - \end{aligned} - $$ - - where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for - the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate - of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. See [1]. - - Parameters - ---------- - X : - The exogenous variables. Shape (n_time_bins, n_neurons, n_features) - y : - Spike counts arranged in a matrix. n_neurons must be the same as - during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). - score_type: - String indicating the type of scoring to return. Options are: - - `log-likelihood` for the model log-likelihood. - - `pseudo-r2` for the model pseudo-$R^2$. - Default is defined at class initialization. - - Returns - ------- - score : - The Poisson log-likelihood or the pseudo-$R^2$ of the current model. - - Raises - ------ - NotFittedError - If ``fit`` has not been called first with this instance. - ValueError - If attempting to simulate a different number of neurons than were - present during fitting (i.e., if ``init_y.shape[0] != - self.baseline_link_fr_.shape[0]``). - - Notes - ----- - The log-likelihood is not on a standard scale, its value is influenced by many factors, - among which the number of model parameters. The log-likelihood can assume both positive - and negative values. - - The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure - of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. - The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. - - References - ---------- - [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. - Routledge, 2013. - - """ - norm_constant = jax.scipy.special.gammaln(y + 1).mean() - return ( - super()._safe_score(X=X, y=y, score_type=score_type, score_func=self._score) - - norm_constant - ) - - def fit( - self, - X: Union[NDArray, jnp.ndarray], - y: Union[NDArray, jnp.ndarray], - init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "gpu", - ): - """Fit GLM to spiking data. - - Following scikit-learn API, the solutions are stored as attributes - ``basis_coeff_`` and ``baseline_link_fr``. - - Parameters - ---------- - X : - Predictors, shape (n_time_bins, n_neurons, n_features) - y : - Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). - init_params : - Initial values for the spike basis coefficients and bias terms. If - None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) - device: - Device used for optimizing model parameters. - - Raises - ------ - ValueError - - If `init_params` is not of length two. - - If dimensionality of `init_params` are not correct. - - If the number of neurons in the model parameters and in the inputs do not match. - - If `X` is not three-dimensional. - - If spike_data is not two-dimensional. - - If solver returns at least one NaN parameter, which means it found - an invalid solution. Try tuning optimization hyperparameters. - TypeError - - If `init_params` are not array-like - - If `init_params[i]` cannot be converted to jnp.ndarray for all i - - """ - - def loss(params, X, y): - return self._score(X, y, params) - - self._safe_fit(X=X, y=y, loss=loss, init_params=init_params, device=device) - - def simulate( - self, - random_key: jax.random.PRNGKeyArray, - n_timesteps: int, - init_y: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray], - feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu", - ) -> Tuple[jnp.ndarray, jnp.ndarray]: - """ - Simulate spike trains using the Poisson-GLM as a recurrent network. - - This function projects spike trains into the future, employing the fitted - parameters of the GLM. It is capable of simulating spike trains based on a combination - of historical spike activity and external feedforward inputs like convolved currents, light - intensities, etc. - - - Parameters - ---------- - random_key : - PRNGKey for seeding the simulation. - n_timesteps : - Duration of the simulation in terms of time steps. - init_y : - Initial spike counts matrix that kickstarts the simulation. - Expected shape: (window_size, n_neurons). - coupling_basis_matrix : - Basis matrix for coupling, representing between-neuron couplings - and auto-correlations. Expected shape: (window_size, n_basis_coupling). - feedforward_input : - External input matrix to the model, representing factors like convolved currents, - light intensities, etc. When not provided, the simulation is done with coupling-only. - Expected shape: (n_timesteps, n_neurons, n_basis_input). - device : - Computation device to use ('cpu', 'gpu' or 'tpu'). Default is 'cpu'. - - Returns - ------- - simulated_spikes : - Simulated spike counts for each neuron over time. - Shape: (n_neurons, n_timesteps). - firing_rates : - Simulated firing rates for each neuron over time. - Shape: (n_neurons, n_timesteps). - - Raises - ------ - NotFittedError - If the model hasn't been fitted prior to calling this method. - ValueError - - If the instance has not been previously fitted. - - If there's an inconsistency between the number of neurons in model parameters. - - If the number of neurons in input arguments doesn't match with model parameters. - - For an invalid computational device selection. - - - See Also - -------- - [predict](../glm/#neurostatslib.glm.PoissonGLM.predict) : Method to predict firing rates based on - the model's parameters. - - Notes - ----- - The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients - (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. - The remaining coefficients correspond to the weights for the feed-forward input. - - - The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` - to ensure consistency in the model's input feature dimensionality. - """ - simulated_spikes, firing_rates = super()._safe_simulate( - random_key=random_key, - n_timesteps=n_timesteps, - init_y=init_y, - coupling_basis_matrix=coupling_basis_matrix, - random_function=jax.random.poisson, - feedforward_input=feedforward_input, - device=device, - ) - return simulated_spikes, firing_rates +# class _BaseGLM(_BaseRegressor, abc.ABC): +# """Abstract base class for Poisson GLMs. +# +# Provides methods for score computation, simulation, and prediction. +# Must be subclassed with a method for fitting to data. +# +# Parameters +# ---------- +# solver_name +# Name of the solver to use when fitting the GLM. Must be an attribute of +# ``jaxopt``. +# solver_kwargs +# Dictionary of keyword arguments to pass to the solver during its +# initialization. +# inverse_link_function +# Function to transform outputs of convolution with basis to firing rate. +# Must accept any number as input and return all non-negative values. +# kwargs: +# Additional keyword arguments. ``kwargs`` may depend on the concrete +# subclass implementation (e.g. alpha, the regularization hyperparamter, will be present for +# penalized GLMs but not for the un-penalized case). +# +# """ +# +# def __init__( +# self, +# solver_name: str = "GradientDescent", +# solver_kwargs: dict = dict(), +# inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, +# score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", +# **kwargs, +# ): +# self.solver_name = solver_name +# try: +# solver_args = inspect.getfullargspec(getattr(jaxopt, solver_name)).args +# except AttributeError: +# raise AttributeError( +# f"module jaxopt has no attribute {solver_name}, pick a different solver!" +# ) +# +# undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) +# if undefined_kwargs: +# raise NameError( +# f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" +# ) +# +# if score_type not in ["log-likelihood", "pseudo-r2"]: +# raise NotImplementedError( +# f"Scoring method {score_type} not implemented! " +# f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." +# ) +# self.score_type = score_type +# self.solver_kwargs = solver_kwargs +# +# if not callable(inverse_link_function): +# raise ValueError("inverse_link_function must be a callable!") +# +# self.inverse_link_function = inverse_link_function +# # set additional kwargs e.g. regularization hyperparameters and so on... +# super().__init__(**kwargs) +# # initialize parameters to None +# self.baseline_link_fr_ = None +# self.basis_coeff_ = None +# +# def _predict( +# self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray +# ) -> jnp.ndarray: +# """ +# Predict firing rates given predictors and parameters. +# +# Parameters +# ---------- +# params : +# Tuple containing the spike basis coefficients and bias terms. +# X : +# Predictors. Shape (n_time_bins, n_neurons, n_features). +# +# Returns +# ------- +# jnp.ndarray +# The predicted firing rates. Shape (n_time_bins, n_neurons). +# """ +# Ws, bs = params +# return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) +# +# @abc.abstractmethod +# def residual_deviance(self, predicted_rate, y): +# r"""Compute the residual deviance for a GLM model. +# +# Parameters +# ---------- +# predicted_rate: +# The predicted rate of the GLM. +# y: +# The observations. +# +# Returns +# ------- +# The residual deviance of the model. +# +# Notes +# ----- +# Deviance is a measure of the goodness of fit of a statistical model. +# For a Poisson model, the residual deviance is computed as: +# +# $$ +# \begin{aligned} +# D(y, \hat{y}) &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) +# \end{aligned} +# $$ +# where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model +# log-likelihood. Lower values of deviance indicate a better fit. +# +# """ +# pass +# +# def _pseudo_r2(self, params, X, y): +# r"""Pseudo-R^2 calculation for a GLM. +# +# The Pseudo-R^2 metric gives a sense of how well the model fits the data, +# relative to a null (or baseline) model. +# +# Parameters +# ---------- +# params : +# Tuple containing the spike basis coefficients and bias terms. +# X: +# The predictors. +# y: +# The neural activity. +# +# Returns +# ------- +# : +# The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, +# whereas a value closer to 0 suggests that the model doesn't improve much over the null model. +# +# """ +# mu = self._predict(params, X) +# res_dev_t = self.residual_deviance(mu, y) +# resid_deviance = jnp.sum(res_dev_t**2) +# +# null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() +# null_dev_t = self.residual_deviance(null_mu, y) +# null_deviance = jnp.sum(null_dev_t**2) +# +# return (null_deviance - resid_deviance) / null_deviance +# +# def _check_is_fit(self): +# """Ensure the instance has been fitted.""" +# if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): +# raise NotFittedError( +# "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." +# ) +# +# def _safe_predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: +# """Predict firing rates based on fit parameters. +# +# Parameters +# ---------- +# X : +# The exogenous variables. Shape (n_time_bins, n_neurons, n_features). +# +# Returns +# ------- +# predicted_firing_rates : jnp.ndarray +# The predicted firing rates with shape (n_neurons, n_time_bins). +# +# Raises +# ------ +# NotFittedError +# If ``fit`` has not been called first with this instance. +# ValueError +# - If `params` is not a JAX pytree of size two. +# - If weights and bias terms in `params` don't have the expected dimensions. +# - If the number of neurons in the model parameters and in the inputs do not match. +# - If `X` is not three-dimensional. +# - If there's an inconsistent number of features between spike basis coefficients and `X`. +# +# See Also +# -------- +# score +# Score predicted firing rates against target spike counts. +# """ +# # check that the model is fitted +# self._check_is_fit() +# # extract model params +# Ws = self.basis_coeff_ +# bs = self.baseline_link_fr_ +# +# (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) +# +# # check input dimensionality +# self._check_input_dimensionality(X=X) +# # check consistency between X and params +# self._check_input_and_params_consistency((Ws, bs), X=X) +# +# return self._predict((Ws, bs), X) +# +# def _safe_score( +# self, +# X: Union[NDArray, jnp.ndarray], +# y: Union[NDArray, jnp.ndarray], +# score_func: Callable[ +# [jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]], jnp.ndarray +# ], +# score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None, +# ) -> jnp.ndarray: +# r"""Score the predicted firing rates (based on fit) to the target spike counts. +# +# This computes the GLM mean log-likelihood or the pseudo-$R^2$, thus the higher the +# number the better. +# +# The pseudo-$R^2$ can be computed as follows, +# +# $$ +# \begin{aligned} +# R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ +# &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) +# - \log \text{LL}(\bar{\lambda}| y)}, +# \end{aligned} +# $$ +# +# where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is +# the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model +# predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate. See [1]. +# +# Parameters +# ---------- +# X : +# The exogenous variables. Shape (n_time_bins, n_neurons, n_features) +# y : +# Neural activity arranged in a matrix. n_neurons must be the same as +# during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). +# score_type: +# String indicating the type of scoring to return. Options are: +# - `log-likelihood` for the model log-likelihood. +# - `pseudo-r2` for the model pseudo-$R^2$. +# Default is defined at class initialization. +# +# Returns +# ------- +# score : (1,) +# The Poisson log-likelihood or the pseudo-$R^2$ of the current model. +# +# Raises +# ------ +# NotFittedError +# If ``fit`` has not been called first with this instance. +# ValueError +# If attempting to simulate a different number of neurons than were +# present during fitting (i.e., if ``init_y.shape[0] != +# self.baseline_link_fr_.shape[0]``). +# +# Notes +# ----- +# The log-likelihood is not on a standard scale, its value is influenced by many factors, +# among which the number of model parameters. The log-likelihood can assume both positive +# and negative values. +# +# The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure +# of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. +# The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. +# +# Refer to the concrete subclass docstrings `_score` for the specific likelihood equations. +# +# References +# ---------- +# [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. +# Routledge, 2013. +# +# """ +# # ignore the last time point from predict, because that corresponds to +# # the next time step, which we have no observed data for +# self._check_is_fit() +# Ws = self.basis_coeff_ +# bs = self.baseline_link_fr_ +# +# X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) +# +# self._check_input_dimensionality(X, y) +# self._check_input_n_timepoints(X, y) +# self._check_input_and_params_consistency((Ws, bs), X=X, y=y) +# +# if score_type is None: +# score_type = self.score_type +# +# if score_type == "log-likelihood": +# score = -(score_func(X, y, (Ws, bs))) +# elif score_type == "pseudo-r2": +# score = self._pseudo_r2((Ws, bs), X, y) +# else: +# # this should happen only if one manually set score_type +# raise NotImplementedError( +# f"Scoring method {score_type} not implemented! " +# f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." +# ) +# return score +# +# def _safe_fit( +# self, +# X: Union[NDArray, jnp.ndarray], +# y: Union[NDArray, jnp.ndarray], +# loss: Callable[ +# [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32 +# ], +# init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, +# device: Literal["cpu", "gpu", "tpu"] = "gpu", +# ): +# """Fit GLM to neuroal activity. +# +# Following scikit-learn API, the solutions are stored as attributes +# ``basis_coeff_`` and ``baseline_link_fr``. +# +# Parameters +# ---------- +# X : +# Predictors, shape (n_time_bins, n_neurons, n_features) +# y : +# Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). +# loss: +# The loss function to be minimized. +# init_params : +# Initial values for the spike basis coefficients and bias terms. If +# None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) +# device: +# Device used for optimizing model parameters. +# +# Raises +# ------ +# ValueError +# - If `init_params` is not of length two. +# - If dimensionality of `init_params` are not correct. +# - If the number of neurons in the model parameters and in the inputs do not match. +# - If `X` is not three-dimensional. +# - If spike_data is not two-dimensional. +# - If solver returns at least one NaN parameter, which means it found +# an invalid solution. Try tuning optimization hyperparameters. +# TypeError +# - If `init_params` are not array-like +# - If `init_params[i]` cannot be converted to jnp.ndarray for all i +# """ +# # convert to jnp.ndarray & perform checks +# X, y, init_params = self._preprocess_fit(X, y, init_params) +# +# # send to device +# target_device = self.select_target_device(device) +# X, y = self.device_put(X, y, device=target_device) +# init_params = self.device_put(*init_params, device=target_device) +# +# # Run optimization +# solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) +# params, state = solver.run(init_params, X=X, y=y) +# +# if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): +# raise ValueError( +# "Solver returned at least one NaN parameter, so solution is invalid!" +# " Try tuning optimization hyperparameters." +# ) +# +# # Store parameters +# self.basis_coeff_ = params[0] +# self.baseline_link_fr_ = params[1] +# # note that this will include an error value, which is not the same as +# # the output of loss. I believe it's the output of +# # solver.l2_optimality_error +# self.solver_state = state +# self.solver = solver +# +# def _safe_simulate( +# self, +# random_key: jax.random.PRNGKeyArray, +# n_timesteps: int, +# init_y: Union[NDArray, jnp.ndarray], +# coupling_basis_matrix: Union[NDArray, jnp.ndarray], +# random_function: Callable[[jax.random.PRNGKeyArray, ArrayLike], jnp.ndarray], +# feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, +# device: Literal["cpu", "gpu", "tpu"] = "cpu", +# ) -> Tuple[jnp.ndarray, jnp.ndarray]: +# """ +# Simulate spike trains using the GLM as a recurrent network. +# +# This function projects neural activity into the future, employing the fitted +# parameters of the GLM. It is capable of simulating activity based on a combination +# of historical spike activity and external feedforward inputs like convolved currents, light +# intensities, etc. +# +# Parameters +# ---------- +# random_key : +# PRNGKey for seeding the simulation. +# n_timesteps : +# Duration of the simulation in terms of time steps. +# init_y : +# Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. +# Expected shape: (window_size, n_neurons). +# coupling_basis_matrix : +# Basis matrix for coupling, representing between-neuron couplings +# and auto-correlations. Expected shape: (window_size, n_basis_coupling). +# random_function : +# A probability emission function, like jax.random.poisson, which takes as input a random.PRNGKeyArray +# and the mean rate, and samples observations, (spike counts for a poisson). +# feedforward_input : +# External input matrix to the model, representing factors like convolved currents, +# light intensities, etc. When not provided, the simulation is done with coupling-only. +# Expected shape: (n_timesteps, n_neurons, n_basis_input). +# device : +# Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. +# +# Returns +# ------- +# simulated_obs : +# Simulated observations (spike counts for PoissonGLMs) for each neuron over time. +# Shape: (n_neurons, n_timesteps). +# firing_rates : +# Simulated firing rates for each neuron over time. +# Shape: (n_neurons, n_timesteps). +# +# Raises +# ------ +# NotFittedError +# If the model hasn't been fitted prior to calling this method. +# ValueError +# - If the instance has not been previously fitted. +# - If there's an inconsistency between the number of neurons in model parameters. +# - If the number of neurons in input arguments doesn't match with model parameters. +# - For an invalid computational device selection. +# +# +# See Also +# -------- +# predict : Method to predict firing rates based on the model's parameters. +# +# Notes +# ----- +# The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients +# (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. +# The remaining coefficients correspond to the weights for the feed-forward input. +# +# +# The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` +# to ensure consistency in the model's input feature dimensionality. +# """ +# target_device = self.select_target_device(device) +# # check if the model is fit +# self._check_is_fit() +# +# # convert to jnp.ndarray +# init_y, coupling_basis_matrix, feedforward_input = self._convert_to_jnp_ndarray( +# init_y, coupling_basis_matrix, feedforward_input, data_type=jnp.float32 +# ) +# +# # Transfer data to the target device +# init_y, coupling_basis_matrix, feedforward_input = self.device_put( +# init_y, coupling_basis_matrix, feedforward_input, device=target_device +# ) +# +# n_basis_coupling = coupling_basis_matrix.shape[1] +# n_neurons = self.baseline_link_fr_.shape[0] +# +# # add an empty input (simulate with coupling-only) +# if feedforward_input is None: +# feedforward_input = jnp.zeros( +# (n_timesteps, n_neurons, 0), dtype=jnp.float32 +# ) +# +# Ws = self.basis_coeff_ +# bs = self.baseline_link_fr_ +# +# self._check_input_dimensionality(feedforward_input, init_y) +# +# if ( +# feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] +# != Ws.shape[1] +# ): +# raise ValueError( +# "The number of feed forward input features " +# "and the number of recurrent features must add up to " +# "the overall model features." +# f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " +# f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " +# f"provided instead." +# ) +# +# self._check_input_and_params_consistency( +# (Ws[:, n_basis_coupling * n_neurons :], bs), +# X=feedforward_input, +# y=init_y, +# ) +# +# if init_y.shape[0] != coupling_basis_matrix.shape[0]: +# raise ValueError( +# "`init_y` and `coupling_basis_matrix`" +# " should have the same window size! " +# f"`init_y` window size: {init_y.shape[1]}, " +# f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" +# ) +# +# if feedforward_input.shape[0] != n_timesteps: +# raise ValueError( +# "`feedforward_input` must be of length `n_timesteps`. " +# f"`feedforward_input` has length {len(feedforward_input)}, " +# f"`n_timesteps` is {n_timesteps} instead!" +# ) +# subkeys = jax.random.split(random_key, num=n_timesteps) +# +# def scan_fn( +# data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray +# ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: +# """Scan over time steps and simulate spikes and firing rates. +# +# This function simulates the spikes and firing rates for each time step +# based on the previous spike data, feedforward input, and model coefficients. +# """ +# spikes, chunk = data +# +# # Convolve the spike data with the coupling basis matrix +# conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] +# +# # Extract the corresponding slice of the feedforward input for the current time step +# input_slice = jax.lax.dynamic_slice( +# feedforward_input, +# (chunk, 0, 0), +# (1, feedforward_input.shape[1], feedforward_input.shape[2]), +# ) +# +# # Reshape the convolved spikes and concatenate with the input slice to form the model input +# conv_spk = jnp.tile( +# conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] +# ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) +# X = jnp.concatenate([conv_spk, input_slice], axis=2) +# +# # Predict the firing rate using the model coefficients +# firing_rate = self._predict((Ws, bs), X) +# +# # Simulate spikes based on the predicted firing rate +# new_spikes = random_function(key, firing_rate) +# +# # Prepare the spikes for the next iteration (keeping the most recent spikes) +# concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 +# return concat_spikes, (new_spikes, firing_rate) +# +# _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) +# simulated_spikes, firing_rates = outputs +# return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) +# +# +# class PoissonGLM(_BaseGLM): +# """Un-regularized Poisson-GLM. +# +# The class fits the un-penalized maximum likelihood Poisson GLM parameter estimate. +# +# Parameters +# ---------- +# solver_name +# Name of the solver to use when fitting the GLM. Must be an attribute of +# ``jaxopt``. +# solver_kwargs +# Dictionary of keyword arguments to pass to the solver during its +# initialization. +# inverse_link_function +# Function to transform outputs of convolution with basis to firing rate. +# Must accept any number as input and return all non-negative values. +# +# Attributes +# ---------- +# solver +# jaxopt solver, set during ``fit()`` +# solver_state +# state of the solver, set during ``fit()`` +# basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) +# Solutions for the spike basis coefficients, set during ``fit()`` +# baseline_link_fr : jnp.ndarray, (n_neurons,) +# Solutions for bias terms, set during ``fit()`` +# """ +# +# def __init__( +# self, +# solver_name: str = "GradientDescent", +# solver_kwargs: dict = dict(), +# inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, +# score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", +# ): +# super().__init__( +# solver_name=solver_name, +# solver_kwargs=solver_kwargs, +# inverse_link_function=inverse_link_function, +# score_type=score_type, +# ) +# +# def _score( +# self, +# X: jnp.ndarray, +# target_spikes: jnp.ndarray, +# params: Tuple[jnp.ndarray, jnp.ndarray], +# ) -> jnp.ndarray: +# r"""Score the predicted firing rates against target spike counts. +# +# This computes the Poisson negative log-likelihood up to a constant. +# +# Note that you can end up with infinities in here if there are zeros in +# ``predicted_firing_rates``. We raise a warning in that case. +# +# The formula for the Poisson mean log-likelihood is the following, +# +# $$ +# \begin{aligned} +# \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} +# [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ +# &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] +# \end{aligned} +# $$ +# +# Because $\Gamma(k+1)=k!$, see +# https://en.wikipedia.org/wiki/Gamma_function. +# +# Parameters +# ---------- +# X : +# The exogenous variables. Shape (n_time_bins, n_neurons, n_features). +# target_spikes : +# The target spikes to compare against. Shape (n_time_bins, n_neurons). +# params : +# Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). +# +# Returns +# ------- +# : +# The Poisson negative log-likehood. Shape (1,). +# +# Notes +# ----- +# The Poisson probability mass function is: +# +# $$ +# \frac{\lambda^k \exp(-\lambda)}{k!} +# $$ +# +# But the $k!$ term is not a function of the parameters and can be disregarded +# when computing the loss-function. Thus, the negative log of it is: +# +# $$ +# -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] +# &= -k\log(\lambda)-\lambda + \text{const} +# $$ +# +# """ +# # Avoid the edge-case of 0*log(0), much faster than +# # where on large arrays. +# predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) +# x = target_spikes * jnp.log(predicted_firing_rates) +# # see above for derivation of this. +# return jnp.mean(predicted_firing_rates - x) +# +# def residual_deviance( +# self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray +# ) -> jnp.ndarray: +# r"""Compute the residual deviance for a Poisson model. +# +# Parameters +# ---------- +# predicted_rate: +# The predicted firing rates. +# spike_counts: +# The spike counts. +# +# Returns +# ------- +# : +# The residual deviance of the model. +# +# Notes +# ----- +# Deviance is a measure of the goodness of fit of a statistical model. +# For a Poisson model, the residual deviance is computed as: +# +# $$ +# \begin{aligned} +# D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) +# - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ +# &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - +# \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) +# \end{aligned} +# $$ +# where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model +# log-likelihood. Lower values of deviance indicate a better fit. +# +# """ +# # this takes care of 0s in the log +# ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) +# resid_dev = 2 * ( +# spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) +# ) +# return resid_dev +# +# def predict(self, X: Union[NDArray, jnp.ndarray]): +# """Predict firing rates based on fit parameters. +# +# Parameters +# ---------- +# X : +# The exogenous variables. Shape (n_time_bins, n_neurons, n_features). +# +# Returns +# ------- +# predicted_firing_rates : jnp.ndarray +# The predicted firing rates with shape (n_neurons, n_time_bins). +# +# Raises +# ------ +# NotFittedError +# If ``fit`` has not been called first with this instance. +# ValueError +# - If `params` is not a JAX pytree of size two. +# - If weights and bias terms in `params` don't have the expected dimensions. +# - If the number of neurons in the model parameters and in the inputs do not match. +# - If `X` is not three-dimensional. +# - If there's an inconsistent number of features between spike basis coefficients and `X`. +# +# See Also +# -------- +# [score](../glm/#neurostatslib.glm.PoissonGLM.score) +# Score predicted firing rates against target spike counts. +# """ +# return self._safe_predict(X) +# +# def score( +# self, +# X: Union[NDArray, jnp.ndarray], +# y: Union[NDArray, jnp.ndarray], +# score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", +# ) -> jnp.ndarray: +# r"""Score the predicted firing rates (based on fit) to the target spike counts. +# +# This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the +# number the better. +# +# The formula for the mean log-likelihood is the following, +# +# $$ +# \begin{aligned} +# \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} +# [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ +# &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] +# \end{aligned} +# $$ +# +# Because $\Gamma(k+1)=k!$, see +# https://en.wikipedia.org/wiki/Gamma_function. +# +# The pseudo-$R^2$ can be computed as follows, +# +# $$ +# \begin{aligned} +# R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ +# &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) +# - \log \text{LL}(\bar{\lambda}| y)}, +# \end{aligned} +# $$ +# +# where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for +# the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate +# of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. See [1]. +# +# Parameters +# ---------- +# X : +# The exogenous variables. Shape (n_time_bins, n_neurons, n_features) +# y : +# Spike counts arranged in a matrix. n_neurons must be the same as +# during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). +# score_type: +# String indicating the type of scoring to return. Options are: +# - `log-likelihood` for the model log-likelihood. +# - `pseudo-r2` for the model pseudo-$R^2$. +# Default is defined at class initialization. +# +# Returns +# ------- +# score : +# The Poisson log-likelihood or the pseudo-$R^2$ of the current model. +# +# Raises +# ------ +# NotFittedError +# If ``fit`` has not been called first with this instance. +# ValueError +# If attempting to simulate a different number of neurons than were +# present during fitting (i.e., if ``init_y.shape[0] != +# self.baseline_link_fr_.shape[0]``). +# +# Notes +# ----- +# The log-likelihood is not on a standard scale, its value is influenced by many factors, +# among which the number of model parameters. The log-likelihood can assume both positive +# and negative values. +# +# The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure +# of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. +# The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. +# +# References +# ---------- +# [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. +# Routledge, 2013. +# +# """ +# norm_constant = jax.scipy.special.gammaln(y + 1).mean() +# return ( +# super()._safe_score(X=X, y=y, score_type=score_type, score_func=self._score) +# - norm_constant +# ) +# +# def fit( +# self, +# X: Union[NDArray, jnp.ndarray], +# y: Union[NDArray, jnp.ndarray], +# init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, +# device: Literal["cpu", "gpu", "tpu"] = "gpu", +# ): +# """Fit GLM to spiking data. +# +# Following scikit-learn API, the solutions are stored as attributes +# ``basis_coeff_`` and ``baseline_link_fr``. +# +# Parameters +# ---------- +# X : +# Predictors, shape (n_time_bins, n_neurons, n_features) +# y : +# Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). +# init_params : +# Initial values for the spike basis coefficients and bias terms. If +# None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) +# device: +# Device used for optimizing model parameters. +# +# Raises +# ------ +# ValueError +# - If `init_params` is not of length two. +# - If dimensionality of `init_params` are not correct. +# - If the number of neurons in the model parameters and in the inputs do not match. +# - If `X` is not three-dimensional. +# - If spike_data is not two-dimensional. +# - If solver returns at least one NaN parameter, which means it found +# an invalid solution. Try tuning optimization hyperparameters. +# TypeError +# - If `init_params` are not array-like +# - If `init_params[i]` cannot be converted to jnp.ndarray for all i +# +# """ +# +# def loss(params, X, y): +# return self._score(X, y, params) +# +# self._safe_fit(X=X, y=y, loss=loss, init_params=init_params, device=device) +# +# def simulate( +# self, +# random_key: jax.random.PRNGKeyArray, +# n_timesteps: int, +# init_y: Union[NDArray, jnp.ndarray], +# coupling_basis_matrix: Union[NDArray, jnp.ndarray], +# feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, +# device: Literal["cpu", "gpu", "tpu"] = "cpu", +# ) -> Tuple[jnp.ndarray, jnp.ndarray]: +# """ +# Simulate spike trains using the Poisson-GLM as a recurrent network. +# +# This function projects spike trains into the future, employing the fitted +# parameters of the GLM. It is capable of simulating spike trains based on a combination +# of historical spike activity and external feedforward inputs like convolved currents, light +# intensities, etc. +# +# +# Parameters +# ---------- +# random_key : +# PRNGKey for seeding the simulation. +# n_timesteps : +# Duration of the simulation in terms of time steps. +# init_y : +# Initial spike counts matrix that kickstarts the simulation. +# Expected shape: (window_size, n_neurons). +# coupling_basis_matrix : +# Basis matrix for coupling, representing between-neuron couplings +# and auto-correlations. Expected shape: (window_size, n_basis_coupling). +# feedforward_input : +# External input matrix to the model, representing factors like convolved currents, +# light intensities, etc. When not provided, the simulation is done with coupling-only. +# Expected shape: (n_timesteps, n_neurons, n_basis_input). +# device : +# Computation device to use ('cpu', 'gpu' or 'tpu'). Default is 'cpu'. +# +# Returns +# ------- +# simulated_spikes : +# Simulated spike counts for each neuron over time. +# Shape: (n_neurons, n_timesteps). +# firing_rates : +# Simulated firing rates for each neuron over time. +# Shape: (n_neurons, n_timesteps). +# +# Raises +# ------ +# NotFittedError +# If the model hasn't been fitted prior to calling this method. +# ValueError +# - If the instance has not been previously fitted. +# - If there's an inconsistency between the number of neurons in model parameters. +# - If the number of neurons in input arguments doesn't match with model parameters. +# - For an invalid computational device selection. +# +# +# See Also +# -------- +# [predict](../glm/#neurostatslib.glm.PoissonGLM.predict) : Method to predict firing rates based on +# the model's parameters. +# +# Notes +# ----- +# The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients +# (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. +# The remaining coefficients correspond to the weights for the feed-forward input. +# +# +# The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` +# to ensure consistency in the model's input feature dimensionality. +# """ +# simulated_spikes, firing_rates = super()._safe_simulate( +# random_key=random_key, +# n_timesteps=n_timesteps, +# init_y=init_y, +# coupling_basis_matrix=coupling_basis_matrix, +# random_function=jax.random.poisson, +# feedforward_input=feedforward_input, +# device=device, +# ) +# return simulated_spikes, firing_rates diff --git a/src/neurostatslib/observation_noise.py b/src/neurostatslib/noise_model.py similarity index 95% rename from src/neurostatslib/observation_noise.py rename to src/neurostatslib/noise_model.py index c26c8dc0..3a69019c 100644 --- a/src/neurostatslib/observation_noise.py +++ b/src/neurostatslib/noise_model.py @@ -8,6 +8,12 @@ KeyArray = Union[jnp.ndarray, jax.random.PRNGKeyArray] +__all__ = ["PoissonNoiseModel"] + + +def __dir__(): + return __all__ + class NoiseModel(_Base, abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps @@ -17,6 +23,7 @@ def __init__(self, inverse_link_function, **kwargs): if not callable(inverse_link_function): raise ValueError("inverse_link_function must be a callable!") self.inverse_link_function = inverse_link_function + self._scale = None @abc.abstractmethod def negative_log_likelihood(self, firing_rate, y): @@ -64,8 +71,9 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): class PoissonNoiseModel(NoiseModel): - def __init__(self, inverse_link_function): + def __init__(self, inverse_link_function=jnp.exp): super().__init__(inverse_link_function=inverse_link_function) + self._scale = 1 def negative_log_likelihood( self, diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index adda061e..3db45302 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -9,6 +9,12 @@ from .base_class import _Base from .proximal_operator import prox_group_lasso +__all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] + + +def __dir__() -> list[str]: + return __all__ + class Solver(_Base, abc.ABC): allowed_solvers = [] @@ -91,7 +97,9 @@ class UnRegularizedSolver(Solver): "LBFGSB", ] - def __init__(self, solver_name: str, solver_kwargs: Optional[dict] = None): + def __init__( + self, solver_name: str = "GradientDescent", solver_kwargs: Optional[dict] = None + ): super().__init__(solver_name, solver_kwargs=solver_kwargs) def instantiate_solver( @@ -119,7 +127,10 @@ class RidgeSolver(Solver): ] def __init__( - self, solver_name: str, solver_kwargs: Optional[dict] = None, alpha: float = 1.0 + self, + solver_name: str = "GradientDescent", + solver_kwargs: Optional[dict] = None, + alpha: float = 1.0, ): super().__init__(solver_name, solver_kwargs=solver_kwargs, alpha=alpha) @@ -187,7 +198,7 @@ def instantiate_solver( class LassoSolver(ProxGradientSolver): def __init__( self, - solver_name: str, + solver_name: str = "ProximalGradient", solver_kwargs: Optional[dict] = None, alpha: float = 1.0, mask: Optional[Union[NDArray, jnp.ndarray]] = None, diff --git a/tests/conftest.py b/tests/conftest.py index a02a9a42..b284ed41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,7 +55,7 @@ def poissonGLM_model_instantiation(): X = np.random.normal(size=(100, 1, 5)) b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) - noise_model = nsl.observation_noise.PoissonNoiseModel(jnp.exp) + noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) @@ -83,7 +83,9 @@ def poissonGLM_coupled_model_config_simulate(): "simulate_coupled_neurons_params.yml"), "r") as fh: config_dict = yaml.safe_load(fh) - model = nsl.glm.PoissonGLM(inverse_link_function=jax.numpy.exp) + noise = nsl.noise_model.PoissonNoiseModel(jnp.exp) + solver = nsl.solver.RidgeSolver("BFGS", alpha=0.1) + model = nsl.glm.GLMRecurrent(noise_model=noise, solver=solver) model.basis_coeff_ = jnp.asarray(config_dict["basis_coeff_"]) model.baseline_link_fr_ = jnp.asarray(config_dict["baseline_link_fr_"]) coupling_basis = jnp.asarray(config_dict["coupling_basis"]) @@ -91,7 +93,6 @@ def poissonGLM_coupled_model_config_simulate(): init_spikes = jnp.asarray(config_dict["init_spikes"]) return model, coupling_basis, feedforward_input, init_spikes, jax.random.PRNGKey(123) - @pytest.fixture def jaxopt_solvers(): return [ @@ -105,6 +106,7 @@ def jaxopt_solvers(): "ProximalGradient" ] + @pytest.fixture def group_sparse_poisson_glm_model_instantiation(): """Set up a Poisson GLM for testing purposes with group sparse weights. @@ -126,21 +128,43 @@ def group_sparse_poisson_glm_model_instantiation(): b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) w_true[0, 1:4] = 0. - noise_model = nsl.observation_noise.PoissonNoiseModel(jnp.exp) + noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate + @pytest.fixture def example_data_prox_operator(): n_neurons = 3 n_features = 4 - n_groups = 2 params = (jnp.ones((n_neurons, n_features)), jnp.zeros(n_neurons)) alpha = 0.1 mask = jnp.array([[1, 0, 1, 0], [0, 1, 0, 1]], dtype=jnp.float32) scaling = 0.5 - return params, alpha, mask, scaling \ No newline at end of file + return params, alpha, mask, scaling + +@pytest.fixture +def poisson_noise_model(): + return nsl.noise_model.PoissonNoiseModel(jnp.exp) + + +@pytest.fixture +def ridge_solver(): + return nsl.solver.RidgeSolver(solver_name="LBFGS", alpha=0.1) + + +@pytest.fixture +def lasso_solver(): + return nsl.solver.LassoSolver(solver_name="ProximalGradient", alpha=0.1) + + +@pytest.fixture +def group_lasso_2groups_5features_solver(): + mask = np.zeros((2, 5)) + mask[0, :2] = 1 + mask[1, 2:] = 1 + return nsl.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask, alpha=0.1) diff --git a/tests/test_glm.py b/tests/test_glm.py index fa99643d..4837c56d 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,8 +1,5 @@ -from typing import Callable, Literal - import jax import jax.numpy as jnp -import jaxopt import numpy as np import pytest import statsmodels.api as sm @@ -11,71 +8,39 @@ import neurostatslib as nsl -class TestPoissonGLM: +class TestGLM: """ Unit tests for the PoissonGLM class. """ ####################### # Test model.__init__ ####################### - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize", "NotPresent"]) - def test_init_solver_name(self, solver_name: str): + @pytest.mark.parametrize("solver", [nsl.solver.RidgeSolver("BFGS"), nsl.solver.Solver, 1]) + def test_init_solver_type(self, solver: nsl.solver.Solver, poisson_noise_model): """ Test initialization with different solver names. Check if an appropriate exception is raised when the solver name is not present in jaxopt. """ - try: - getattr(jaxopt, solver_name) - raise_exception = False - except: - raise_exception = True + raise_exception = solver.__class__.__name__ not in nsl.solver.__all__ if raise_exception: - with pytest.raises(AttributeError, match="module jaxopt has no attribute"): - nsl.glm.PoissonGLM(solver_name=solver_name) + with pytest.raises(TypeError, match="The provided `solver` should be one of the implemented"): + nsl.glm.GLM(solver=solver, noise_model=poisson_noise_model) else: - nsl.glm.PoissonGLM(solver_name=solver_name) + nsl.glm.GLM(solver=solver, noise_model=poisson_noise_model) - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ScipyMinimize"]) - @pytest.mark.parametrize("solver_kwargs", [ - {"tol": 1, "verbose": 1, "maxiter": 1}, - {"tol": 1, "maxiter": 1}]) - def test_init_solver_kwargs(self, solver_name, solver_kwargs): + @pytest.mark.parametrize("noise", [nsl.noise_model.PoissonNoiseModel(), nsl.solver.Solver, 1]) + def test_init_noise_type(self, noise: nsl.noise_model.NoiseModel, ridge_solver): """ - Test the initialization of solver with different solver keyword arguments. Check for invalid arguments - combination. + Test initialization with different solver names. Check if an appropriate exception is raised + when the solver name is not present in jaxopt. """ - raise_exception = (solver_name == "ScipyMinimize") & ("verbose" in solver_kwargs) + raise_exception = noise.__class__.__name__ not in nsl.noise_model.__all__ if raise_exception: - with pytest.raises(NameError, match="kwargs {'[a-z]+'} in solver_kwargs not a kwarg"): - nsl.glm.PoissonGLM(solver_name, solver_kwargs=solver_kwargs) - else: - # define glm and instantiate the solver - nsl.glm.PoissonGLM(solver_name, solver_kwargs=solver_kwargs) - getattr(jaxopt, solver_name)(fun=lambda x: x, **solver_kwargs) - - @pytest.mark.parametrize("func", [1, "string", lambda x: x, jnp.exp]) - def test_init_callable(self, func: Callable[[jnp.ndarray], jnp.ndarray]): - """ - Test the initialization with different types of inverse_link_function. Check if a ValueError is raised - when the provided function is not callable. - """ - if not callable(func): - with pytest.raises(ValueError, match="inverse_link_function must be a callable"): - nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) + with pytest.raises(TypeError, match="The provided `noise_model` should be one of the implemented"): + nsl.glm.GLM(solver=ridge_solver, noise_model=noise) else: - nsl.glm.PoissonGLM("BFGS", inverse_link_function=func) + nsl.glm.GLM(solver=ridge_solver, noise_model=noise) - @pytest.mark.parametrize("score_type", [1, "ll", "log-likelihood", "pseudo-r2"]) - def test_init_score_type(self, score_type: Literal["log-likelihood", "pseudo-r2"]): - """ - Test the initialization with different scoring methods. Check if a NotImplementedError is raised - for unsupported scoring methods. - """ - if score_type not in ["log-likelihood", "pseudo-r2"]: - with pytest.raises(NotImplementedError, match=f"Scoring method {score_type} not implemented"): - nsl.glm.PoissonGLM("BFGS", score_type=score_type) - else: - nsl.glm.PoissonGLM("BFGS", score_type=score_type) ####################### # Test model.fit @@ -738,14 +703,12 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, if raise_exception: with pytest.raises(ValueError, match="The number of neuron in the model parameters"): model.simulate(random_key=random_key, - n_timesteps=n_time_points, init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=n_time_points, init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -772,14 +735,12 @@ def test_simulate_input_dimensionality(self, delta_dim, if raise_exception: with pytest.raises(ValueError, match="X must be three-dimensional"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -806,14 +767,12 @@ def test_simulate_y_dimensionality(self, delta_dim, if raise_exception: with pytest.raises(ValueError, match="y must be two-dimensional"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -835,14 +794,12 @@ def test_simulate_n_neuron_match_y(self, delta_n_neuron, if raise_exception: with pytest.raises(ValueError, match="The number of neuron in the model parameters"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -864,14 +821,12 @@ def test_simulate_is_fit(self, is_fit, if raise_exception: with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -894,14 +849,12 @@ def test_simulate_time_point_match_y(self, delta_tp, if raise_exception: with pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -924,14 +877,12 @@ def test_simulate_time_point_match_coupling_basis(self, delta_tp, if raise_exception: with pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -956,16 +907,14 @@ def test_simulate_feature_consistency_input(self, delta_features, feedforward_input.shape[1], feedforward_input.shape[2]+delta_features)) if raise_exception: - with pytest.raises(ValueError, match="The number of feed forward input features"): + with pytest.raises(ValueError, match="Inconsistent number of features"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -989,47 +938,14 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, coupling_basis = jnp.zeros((coupling_basis.shape[0], coupling_basis.shape[1] + delta_features)) if raise_exception: - with pytest.raises(ValueError, match="The number of feed forward input features"): - model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_simulate_input_timepoints(self, delta_tp, - poissonGLM_coupled_model_config_simulate): - """ - Test `simulate` with varying input timepoints. - Ensures that a mismatch between n_timesteps and the timepoints in - `feedforward_input` results in an exception. - """ - raise_exception = delta_tp != 0 - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - n_timesteps = feedforward_input.shape[0] - feedforward_input = jnp.zeros((feedforward_input.shape[0] + delta_tp, - feedforward_input.shape[1], - feedforward_input.shape[2])) - if raise_exception: - with pytest.raises(ValueError, match="`feedforward_input` must be of length"): + with pytest.raises(ValueError, match="Inconsistent number of features"): model.simulate(random_key=random_key, - n_timesteps=n_timesteps, init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device="cpu") else: model.simulate(random_key=random_key, - n_timesteps=n_timesteps, init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -1055,7 +971,6 @@ def test_simulate_device_spec(self, device_spec, if raise_exception: with pytest.raises(ValueError, match=f"Invalid device specification: {device_spec}"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -1063,14 +978,12 @@ def test_simulate_device_spec(self, device_spec, elif raise_warning: with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, device=device_spec) else: model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -1087,10 +1000,9 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): # set model coeff model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] - model.set_params(inverse_link_function=jnp.exp) # get the rate dev = sm.families.Poisson().deviance(y, firing_rate) - dev_model = model.residual_deviance(firing_rate, y).sum() + dev_model = model.noise_model.residual_deviance(firing_rate, y).sum() if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") @@ -1102,9 +1014,9 @@ def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiatio res_sm = glm_sm.fit() fit_params_sm = res_sm.params # use a second order method for precision, match non-linearity - model.set_params(inverse_link_function=jnp.exp, - solver_name="BFGS", - solver_kwargs={"tol": 10**-8}) + model.set_params(noise_model__inverse_link_function=jnp.exp, + solver__solver_name="BFGS", + solver__solver_kwargs={"tol": 10**-8}) model.fit(X, y) fit_params_model = jnp.hstack((model.baseline_link_fr_, model.basis_coeff_.flatten())) @@ -1113,7 +1025,7 @@ def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiatio def test_compatibility_with_sklearn_cv(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - param_grid = {"solver_name": ["BFGS", "GradientDescent"]} + param_grid = {"solver__solver_name": ["BFGS", "GradientDescent"]} GridSearchCV(model, param_grid).fit(X, y) def test_end_to_end_fit_and_simulate(self, @@ -1127,7 +1039,6 @@ def test_end_to_end_fit_and_simulate(self, # generate spike trains spikes, _ = model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, @@ -1161,7 +1072,6 @@ def test_end_to_end_fit_and_simulate(self, # simulate model.simulate(random_key=random_key, - n_timesteps=feedforward_input.shape[0], init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input, diff --git a/tests/test_observation_noise.py b/tests/test_noise_model.py similarity index 98% rename from tests/test_observation_noise.py rename to tests/test_noise_model.py index 8f0aa363..f82a4eed 100644 --- a/tests/test_observation_noise.py +++ b/tests/test_noise_model.py @@ -9,7 +9,7 @@ class TestPoissonNoiseModel: - cls = nsl.observation_noise.PoissonNoiseModel + cls = nsl.noise_model.PoissonNoiseModel @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) def test_initialization(self, link_function): From f56bd4ff5b6184baaa499320c9ad5c69b957eee2 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Sep 2023 19:21:11 -0400 Subject: [PATCH 065/250] fixed example --- src/neurostatslib/exceptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/exceptions.py b/src/neurostatslib/exceptions.py index 82265fbf..9fe3ce0b 100644 --- a/src/neurostatslib/exceptions.py +++ b/src/neurostatslib/exceptions.py @@ -9,10 +9,10 @@ class NotFittedError(ValueError, AttributeError): Examples -------- - >>> from neurostatslib.glm import PoissonGLM + >>> from neurostatslib.glm import GLM >>> from neurostatslib.exceptions import NotFittedError >>> try: - ... PoissonGLM().predict([[[1, 2], [2, 3], [3, 4]]]) + ... GLM().predict([[[1, 2], [2, 3], [3, 4]]]) ... except NotFittedError as e: ... print(repr(e)) From 779bf2dabaeb664bac75e3f6f22b423a2a5fcbeb Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Sep 2023 22:17:06 -0400 Subject: [PATCH 066/250] removed firing and spiking --- src/neurostatslib/glm.py | 96 ++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 39 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 902e2bfe..1a7f8d37 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -85,13 +85,13 @@ def _predict( Returns ------- jnp.ndarray - The predicted firing rates. Shape (n_time_bins, n_neurons). + The predicted rates. Shape (n_time_bins, n_neurons). """ Ws, bs = params return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: - """Predict firing rates based on fit parameters. + """Predict rates based on fit parameters. Parameters ---------- @@ -100,8 +100,8 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Returns ------- - predicted_firing_rates : jnp.ndarray - The predicted firing rates with shape (n_neurons, n_time_bins). + : + The predicted rates with shape (n_neurons, n_time_bins). Raises ------ @@ -116,8 +116,13 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: See Also -------- - [score](../glm/#neurostatslib.glm.PoissonGLM.score) - Score predicted firing rates against target spike counts. + - [score](./#neurostatslib.glm.GLM.score) + Score predicted rates against target spike counts. + - [simulate (feed-forward only)](../glm/#neurostatslib.glm.GLM.simulate) + Simulate neural activity in response to a feed-forward input . + - [simulate (feed-forward + coupling)](../glm/#neurostatslib.glm.GLMRecurrent.simulate) + Simulate neural activity in response to a feed-forward input + using the GLM as a recurrent network. """ # check that the model is fitted self._check_is_fit() @@ -139,12 +144,12 @@ def _score( # call _negative_log_likelihood X: jnp.ndarray, y: jnp.ndarray, ) -> jnp.ndarray: - r"""Score the predicted firing rates against target neural activity. + r"""Score the predicted rates against target neural activity. This computes the negative log-likelihood up to a constant term. Note that you can end up with infinities in here if there are zeros in - ``predicted_firing_rates``. We raise a warning in that case. + ``predicted_rates``. We raise a warning in that case. Parameters ---------- @@ -201,7 +206,7 @@ def score( Returns ------- - score : (1,) + score : The log-likelihood or the pseudo-$R^2$ of the current model. Raises @@ -273,9 +278,9 @@ def fit( X : Predictors, shape (n_time_bins, n_neurons, n_features) y : - Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). + Neural activity arranged in a matrix, shape (n_time_bins, n_neurons). init_params : - Initial values for the spike basis coefficients and bias terms. If + Initial values for the activity basis coefficients and bias terms. If None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) device: Device used for optimizing model parameters. @@ -287,7 +292,7 @@ def fit( - If dimensionality of `init_params` are not correct. - If the number of neurons in the model parameters and in the inputs do not match. - If `X` is not three-dimensional. - - If spike_data is not two-dimensional. + - If `y` is not two-dimensional. - If solver returns at least one NaN parameter, which means it found an invalid solution. Try tuning optimization hyperparameters. TypeError @@ -338,6 +343,19 @@ def simulate( # feed-forward input and/coupling basis **kwargs, ): + """Simulate neural activity in response to a feed-forward input. + + Parameters + ---------- + random_key + feedforward_input + device + kwargs + + Returns + ------- + + """ # check if the model is fit self._check_is_fit() Ws, bs = self.basis_coeff_, self.baseline_link_fr_ @@ -368,34 +386,33 @@ def simulate( init_y: Union[NDArray, jnp.ndarray] = None, ): """ - Simulate spike trains using the GLM as a recurrent network. + Simulate neural activity using the GLM as a recurrent network. This function projects neural activity into the future, employing the fitted parameters of the GLM. It is capable of simulating activity based on a combination - of historical spike activity and external feedforward inputs like convolved currents, light + of historical activity and external feedforward inputs like convolved currents, light intensities, etc. Parameters ---------- random_key : PRNGKey for seeding the simulation. - + feedforward_input : + External input matrix to the model, representing factors like convolved currents, + light intensities, etc. When not provided, the simulation is done with coupling-only. + Expected shape: (n_timesteps, n_neurons, n_basis_input). init_y : Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). coupling_basis_matrix : Basis matrix for coupling, representing between-neuron couplings and auto-correlations. Expected shape: (window_size, n_basis_coupling). - feedforward_input : - External input matrix to the model, representing factors like convolved currents, - light intensities, etc. When not provided, the simulation is done with coupling-only. - Expected shape: (n_timesteps, n_neurons, n_basis_input). device : Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. Returns ------- - simulated_obs : + simulated_activity : Simulated activity (spike counts for PoissonGLMs) for each neuron over time. Shape: (n_neurons, n_timesteps). firing_rates : @@ -415,7 +432,8 @@ def simulate( See Also -------- - predict : Method to predict firing rates based on the model's parameters. + [predict](./#neurostatslib.glm.GLM.predict) : + Method to predict rates based on the model's parameters. Notes ----- @@ -431,7 +449,7 @@ def simulate( if coupling_basis_matrix is None: raise ValueError( "GLMRecurrent simulate method requires a coupling basis" - " matrix in order to generate spikes!" + " matrix in order to generate neural activity!" ) if init_y is None: raise ValueError( @@ -488,7 +506,7 @@ def simulate( "`init_y` and `coupling_basis_matrix`" " should have the same window size! " f"`init_y` window size: {init_y.shape[1]}, " - f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" + f"`coupling_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" ) subkeys = jax.random.split(random_key, num=feedforward_input.shape[0]) @@ -498,15 +516,15 @@ def simulate( def scan_fn( data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: - """Scan over time steps and simulate spikes and firing rates. + """Scan over time steps and simulate activity and rates. - This function simulates the spikes and firing rates for each time step - based on the previous spike data, feedforward input, and model coefficients. + This function simulates the neural activity and firing rates for each time step + based on the previous activity, feedforward input, and model coefficients. """ - spikes, chunk = data + activity, chunk = data - # Convolve the spike data with the coupling basis matrix - conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] + # Convolve the neural activity with the coupling basis matrix + conv_act = convolve_1d_trials(coupling_basis_matrix, activity[None, :, :])[0] # Extract the corresponding slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( @@ -515,28 +533,28 @@ def scan_fn( (1, feed_forward_contrib.shape[1]), ) - # Reshape the convolved spikes and concatenate with the input slice to form the model input - conv_spk = jnp.tile( - conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] - ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) + # Reshape the convolved activity and concatenate with the input slice to form the model input + conv_act = jnp.tile( + conv_act.reshape(conv_act.shape[0], -1), conv_act.shape[1] + ).reshape(conv_act.shape[0], conv_act.shape[1], -1) # Predict the firing rate using the model coefficients # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input firing_rate = self.inverse_link_function( - jnp.einsum("ik,tik->ti", Wr, conv_spk) + input_slice + bs[None, :] + jnp.einsum("ik,tik->ti", Wr, conv_act) + input_slice + bs[None, :] ) # Simulate activity based on the predicted firing rate - new_spikes = self.noise_model.emission_probability(key, firing_rate) + new_act = self.noise_model.emission_probability(key, firing_rate) # Prepare the spikes for the next iteration (keeping the most recent spikes) - concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 - return concat_spikes, (new_spikes, firing_rate) + concat_act = jnp.row_stack((activity[1:], new_act)), chunk + 1 + return concat_act, (new_act, firing_rate) _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) - simulated_spikes, firing_rates = outputs - return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) + simulated_activity, firing_rates = outputs + return jnp.squeeze(simulated_activity, axis=1), jnp.squeeze(firing_rates, axis=1) # class _BaseGLM(_BaseRegressor, abc.ABC): From 6d43fb4201bd1a4270fd272325adca9f39901a64 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 12 Sep 2023 23:08:19 -0400 Subject: [PATCH 067/250] start test refractoring --- docs/examples/README.md | 2 +- docs/examples/coupled_neurons_params.yml | 6292 +++++++++++++++++ docs/examples/plot_ND_basis_function.py | 3 +- ...function.py => plot_a1D_basis_function.py} | 3 +- docs/examples/plot_example_convolution.py | 7 +- docs/examples/plot_glm_demo.py | 264 + src/neurostatslib/solver.py | 12 +- tests/test_glm.py | 222 +- 8 files changed, 6685 insertions(+), 120 deletions(-) create mode 100644 docs/examples/coupled_neurons_params.yml rename docs/examples/{plot_1D_basis_function.py => plot_a1D_basis_function.py} (99%) create mode 100644 docs/examples/plot_glm_demo.py diff --git a/docs/examples/README.md b/docs/examples/README.md index ec619b83..85573549 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1,3 +1,3 @@ # Examples -This will contain tutorials and examples of package usage. \ No newline at end of file +A gallery of tutorials on the current `neurostatslib` functionalities. \ No newline at end of file diff --git a/docs/examples/coupled_neurons_params.yml b/docs/examples/coupled_neurons_params.yml new file mode 100644 index 00000000..1be6c9cc --- /dev/null +++ b/docs/examples/coupled_neurons_params.yml @@ -0,0 +1,6292 @@ +baseline_link_fr_: +- -3.0 +- -3.0 +basis_coeff_: +- - -0.004372 + - -0.02786 + - -0.04582 + - -0.0588 + - -0.06539 + - -0.06396 + - -0.05328 + - -0.03192 + - 0.0002296 + - 0.04143 + - 0.08794 + - 0.1483 + - 0.2053 + - 0.2483 + - 0.2892 + - 0.3093 + - 0.2917 + - 0.2225 + - 0.07357 + - -0.2711 + - -0.006235 + - -0.01047 + - 0.02189 + - 0.058 + - 0.09002 + - 0.1118 + - 0.1209 + - 0.1167 + - 0.09909 + - 0.07044 + - 0.03448 + - -0.01565 + - -0.06823 + - -0.1128 + - -0.1655 + - -0.2176 + - -0.2621 + - -0.2982 + - -0.3255 + - -0.3449 + - 0.5 + - 0.5 +- - -0.004637 + - 0.02223 + - 0.07071 + - 0.09572 + - 0.1012 + - 0.08923 + - 0.06464 + - 0.03076 + - -0.007911 + - -0.04737 + - -0.08429 + - -0.1249 + - -0.1582 + - -0.1827 + - -0.2081 + - -0.23 + - -0.2473 + - -0.2616 + - -0.2741 + - -0.287 + - 0.01127 + - 0.04864 + - 0.0544 + - 0.05082 + - 0.03975 + - 0.02393 + - 0.004725 + - -0.01763 + - -0.04202 + - -0.06744 + - -0.09269 + - -0.1231 + - -0.1522 + - -0.1763 + - -0.2051 + - -0.2348 + - -0.2629 + - -0.2896 + - -0.3149 + - -0.3389 + - 0.5 + - 0.5 +coupling_basis: +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0024979173609873673 + - 0.9975020826390129 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.11451325277931029 + - 0.8854867472206909 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.25013898844998006 + - 0.7498610115500185 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.3122501403134024 + - 0.687749859686596 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.28176761370807446 + - 0.7182323862919272 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.17383844924397923 + - 0.8261615507560222 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.04364762794083282 + - 0.9563523720591665 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9912618171282106 + - 0.008738182871789013 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.7892946476427273 + - 0.21070535235727128 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.3531647741677867 + - 0.6468352258322151 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.011883820048045501 + - 0.9881161799519544 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.7841665801263835 + - 0.21583341987361648 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.17688067665784446 + - 0.8231193233421555 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9253003862638604 + - 0.0746996137361397 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.2549435480705588 + - 0.7450564519294413 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9205258993369989 + - 0.07947410066300109 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.16827351931758228 + - 0.8317264806824178 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.7835282009408713 + - 0.21647179905912872 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.019118847416525586 + - 0.9808811525834744 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.4372031242218587 + - 0.5627968757781414 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9120243919870162 + - 0.08797560801298382 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.044222034278324274 + - 0.9557779657216758 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.40793669708774605 + - 0.5920633029122541 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.8283923698925478 + - 0.17160763010745222 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.9999802058373224 + - 1.9794162677666538e-05 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.1458111022283093 + - 0.8541888977716907 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.4778824971400245 + - 0.5221175028599756 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.803486827077907 + - 0.19651317292209308 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.0 + - 0.9824675828481839 + - 0.017532417151816082 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.029720664099906924 + - 0.9702793359000932 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.19724020774947038 + - 0.8027597922505296 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.44389603578613035 + - 0.5561039642138698 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.6909694421867117 + - 0.30903055781328825 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.8804498633788072 + - 0.1195501366211929 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.0 + - 0.9828262050955638 + - 0.017173794904436157 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.005816278861877466 + - 0.9941837211381226 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.07171948190677246 + - 0.9282805180932275 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.19211081158089233 + - 0.8078891884191077 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.3422365913893123 + - 0.6577634086106878 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.49997219806462273 + - 0.5000278019353773 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.6481581380891199 + - 0.3518418619108801 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.775227808426499 + - 0.22477219157350103 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.8747644272334134 + - 0.12523557276658664 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.9445228823471115 + - 0.05547711765288865 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.9852942394771702 + - 0.014705760522829736 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.0 + - 0.9998405276097415 + - 0.00015947239025848603 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.00798856965539202 + - 0.9920114303446079 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.03392307742054024 + - 0.9660769225794598 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.07373523476821137 + - 0.9262647652317886 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.12352988337197751 + - 0.8764701166280225 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.17990211564285485 + - 0.8200978843571451 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.2399997347398921 + - 0.7600002652601079 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.3015222924967669 + - 0.6984777075032332 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.36268149196393995 + - 0.63731850803606 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.42214108290743424 + - 0.5778589170925659 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.47894873221112266 + - 0.5210512677888774 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.5324679173051469 + - 0.46753208269485313 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.5823146093533313 + - 0.4176853906466687 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.6283012081735033 + - 0.3716987918264968 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.6703886551778314 + - 0.32961134482216864 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.7086466881407022 + - 0.2913533118592979 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.7432216468423799 + - 0.25677835315762026 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.7743109612271127 + - 0.22568903877288732 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.802143356101582 + - 0.197856643898418 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.82696381862707 + - 0.17303618137292998 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.8490224486822571 + - 0.15097755131774288 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.8685664156253453 + - 0.13143358437465474 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.8858343578296817 + - 0.11416564217031833 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9010526715389762 + - 0.09894732846102389 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9144332365128198 + - 0.08556676348718023 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9261722145965264 + - 0.07382778540347357 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9364496329422705 + - 0.06355036705772948 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9454295266061546 + - 0.05457047339384541 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9532604668007324 + - 0.04673953319926766 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9600763426393057 + - 0.039923657360694254 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9659972972699125 + - 0.03400270273008754 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.971130745291511 + - 0.028869254708488945 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.975572418558468 + - 0.024427581441531954 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9794074030288873 + - 0.020592596971112653 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9827111411428311 + - 0.017288858857168965 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9855503831123861 + - 0.014449616887613925 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9879840771076767 + - 0.012015922892323394 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9900641931482845 + - 0.009935806851715523 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9918364789707291 + - 0.008163521029270815 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9933411485659462 + - 0.006658851434053759 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9946135057219054 + - 0.005386494278094567 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9956845059646938 + - 0.004315494035306178 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9965812609202838 + - 0.0034187390797163486 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.997327489436671 + - 0.002672510563328956 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9979439199017871 + - 0.002056080098212898 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9984486481342357 + - 0.0015513518657642722 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9988574550621354 + - 0.0011425449378646424 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9991840881776304 + - 0.0008159118223696749 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.999440510488429 + - 0.0005594895115710874 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9996371204027914 + - 0.00036287959720865404 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.999782945694725 + - 0.00021705430527496627 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9998858144113889 + - 0.00011418558861114869 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.9999525053112863 + - 4.7494688713622946e-05 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 0.99998888016377 + - 1.1119836230089053e-05 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +- - 1.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 + - 0.0 +feedforward_input: +- - - 0.0 + - 1.0 + - - 0.0 + - 1.0 +- - - 0.012578617838741058 + - 0.9999208860571255 + - - 0.012578617838741058 + - 0.9999208860571255 +- - - 0.025155245389375847 + - 0.9996835567465339 + - - 0.025155245389375847 + - 0.9996835567465339 +- - - 0.03772789267871718 + - 0.99928804962034 + - - 0.03772789267871718 + - 0.99928804962034 +- - - 0.05029457036336618 + - 0.9987344272588006 + - - 0.05029457036336618 + - 0.9987344272588006 +- - - 0.06285329004448194 + - 0.9980227772604111 + - - 0.06285329004448194 + - 0.9980227772604111 +- - - 0.07540206458240159 + - 0.9971532122280464 + - - 0.07540206458240159 + - 0.9971532122280464 +- - - 0.08793890841106125 + - 0.9961258697511429 + - - 0.08793890841106125 + - 0.9961258697511429 +- - - 0.10046183785216795 + - 0.9949409123839288 + - - 0.10046183785216795 + - 0.9949409123839288 +- - - 0.11296887142907283 + - 0.9935985276197029 + - - 0.11296887142907283 + - 0.9935985276197029 +- - - 0.12545803018029603 + - 0.9920989278611685 + - - 0.12545803018029603 + - 0.9920989278611685 +- - - 0.13792733797265358 + - 0.9904423503868246 + - - 0.13792733797265358 + - 0.9904423503868246 +- - - 0.1503748218139367 + - 0.9886290573134227 + - - 0.1503748218139367 + - 0.9886290573134227 +- - - 0.1627985121650943 + - 0.986659335554492 + - - 0.1627985121650943 + - 0.986659335554492 +- - - 0.17519644325186898 + - 0.984533496774942 + - - 0.17519644325186898 + - 0.984533496774942 +- - - 0.18756665337583714 + - 0.9822518773417481 + - - 0.18756665337583714 + - 0.9822518773417481 +- - - 0.19990718522480458 + - 0.9798148382707295 + - - 0.19990718522480458 + - 0.9798148382707295 +- - - 0.21221608618250787 + - 0.9772227651694256 + - - 0.21221608618250787 + - 0.9772227651694256 +- - - 0.22449140863757258 + - 0.9744760681760832 + - - 0.22449140863757258 + - 0.9744760681760832 +- - - 0.23673121029167973 + - 0.9715751818947602 + - - 0.23673121029167973 + - 0.9715751818947602 +- - - 0.2489335544668916 + - 0.9685205653265598 + - - 0.2489335544668916 + - 0.9685205653265598 +- - - 0.2610965104120882 + - 0.9653127017970033 + - - 0.2610965104120882 + - 0.9653127017970033 +- - - 0.27321815360846585 + - 0.9619520988795548 + - - 0.27321815360846585 + - 0.9619520988795548 +- - - 0.28529656607404974 + - 0.9584392883153087 + - - 0.28529656607404974 + - 0.9584392883153087 +- - - 0.2973298366671723 + - 0.9547748259288535 + - - 0.2973298366671723 + - 0.9547748259288535 +- - - 0.30931606138886886 + - 0.9509592915403253 + - - 0.30931606138886886 + - 0.9509592915403253 +- - - 0.32125334368414366 + - 0.9469932888736633 + - - 0.32125334368414366 + - 0.9469932888736633 +- - - 0.33313979474205757 + - 0.9428774454610842 + - - 0.33313979474205757 + - 0.9428774454610842 +- - - 0.34497353379459045 + - 0.9386124125437894 + - - 0.34497353379459045 + - 0.9386124125437894 +- - - 0.3567526884142317 + - 0.9341988649689198 + - - 0.3567526884142317 + - 0.9341988649689198 +- - - 0.3684753948102499 + - 0.9296375010827771 + - - 0.3684753948102499 + - 0.9296375010827771 +- - - 0.38013979812359666 + - 0.924929042620325 + - - 0.38013979812359666 + - 0.924929042620325 +- - - 0.3917440527203973 + - 0.9200742345909914 + - - 0.3917440527203973 + - 0.9200742345909914 +- - - 0.4032863224839812 + - 0.915073845160786 + - - 0.4032863224839812 + - 0.915073845160786 +- - - 0.41476478110540693 + - 0.9099286655307568 + - - 0.41476478110540693 + - 0.9099286655307568 +- - - 0.4261776123724353 + - 0.9046395098117981 + - - 0.4261776123724353 + - 0.9046395098117981 +- - - 0.4375230104569043 + - 0.8992072148958368 + - - 0.4375230104569043 + - 0.8992072148958368 +- - - 0.4487991802004621 + - 0.8936326403234123 + - - 0.4487991802004621 + - 0.8936326403234123 +- - - 0.46000433739861096 + - 0.887916668147673 + - - 0.46000433739861096 + - 0.887916668147673 +- - - 0.47113670908301786 + - 0.8820602027948115 + - - 0.47113670908301786 + - 0.8820602027948115 +- - - 0.4821945338020477 + - 0.8760641709209582 + - - 0.4821945338020477 + - 0.8760641709209582 +- - - 0.4931760618994744 + - 0.8699295212655597 + - - 0.4931760618994744 + - 0.8699295212655597 +- - - 0.5040795557913246 + - 0.8636572245012607 + - - 0.5040795557913246 + - 0.8636572245012607 +- - - 0.5149032902408126 + - 0.8572482730803168 + - - 0.5149032902408126 + - 0.8572482730803168 +- - - 0.5256455526313207 + - 0.8507036810775614 + - - 0.5256455526313207 + - 0.8507036810775614 +- - - 0.5363046432373825 + - 0.8440244840299503 + - - 0.5363046432373825 + - 0.8440244840299503 +- - - 0.5468788754936273 + - 0.8372117387727107 + - - 0.5468788754936273 + - 0.8372117387727107 +- - - 0.5573665762616421 + - 0.8302665232721208 + - - 0.5573665762616421 + - 0.8302665232721208 +- - - 0.5677660860947078 + - 0.8231899364549453 + - - 0.5677660860947078 + - 0.8231899364549453 +- - - 0.5780757595003707 + - 0.8159830980345546 + - - 0.5780757595003707 + - 0.8159830980345546 +- - - 0.588293965200805 + - 0.8086471483337551 + - - 0.588293965200805 + - 0.8086471483337551 +- - - 0.5984190863909268 + - 0.8011832481043575 + - - 0.5984190863909268 + - 0.8011832481043575 +- - - 0.608449520994217 + - 0.7935925783435149 + - - 0.608449520994217 + - 0.7935925783435149 +- - - 0.6183836819162153 + - 0.7858763401068549 + - - 0.6183836819162153 + - 0.7858763401068549 +- - - 0.6282199972956423 + - 0.7780357543184395 + - - 0.6282199972956423 + - 0.7780357543184395 +- - - 0.6379569107531118 + - 0.7700720615775812 + - - 0.6379569107531118 + - 0.7700720615775812 +- - - 0.647592881637394 + - 0.7619865219625451 + - - 0.647592881637394 + - 0.7619865219625451 +- - - 0.6571263852691885 + - 0.7537804148311695 + - - 0.6571263852691885 + - 0.7537804148311695 +- - - 0.666555913182372 + - 0.7454550386184362 + - - 0.666555913182372 + - 0.7454550386184362 +- - - 0.675879973362679 + - 0.7370117106310213 + - - 0.675879973362679 + - 0.7370117106310213 +- - - 0.6850970904837809 + - 0.7284517668388609 + - - 0.6850970904837809 + - 0.7284517668388609 +- - - 0.6942058061407225 + - 0.7197765616637636 + - - 0.6942058061407225 + - 0.7197765616637636 +- - - 0.7032046790806838 + - 0.7109874677651024 + - - 0.7032046790806838 + - 0.7109874677651024 +- - - 0.7120922854310254 + - 0.7020858758226226 + - - 0.7120922854310254 + - 0.7020858758226226 +- - - 0.720867218924585 + - 0.6930731943163971 + - - 0.720867218924585 + - 0.6930731943163971 +- - - 0.7295280911221884 + - 0.6839508493039657 + - - 0.7295280911221884 + - 0.6839508493039657 +- - - 0.7380735316323389 + - 0.6747202841946927 + - - 0.7380735316323389 + - 0.6747202841946927 +- - - 0.746502188328052 + - 0.6653829595213794 + - - 0.746502188328052 + - 0.6653829595213794 +- - - 0.7548127275607989 + - 0.6559403527091677 + - - 0.7548127275607989 + - 0.6559403527091677 +- - - 0.7630038343715272 + - 0.6463939578417693 + - - 0.7630038343715272 + - 0.6463939578417693 +- - - 0.7710742126987247 + - 0.6367452854250606 + - - 0.7710742126987247 + - 0.6367452854250606 +- - - 0.7790225855834911 + - 0.6269958621480786 + - - 0.7790225855834911 + - 0.6269958621480786 +- - - 0.7868476953715899 + - 0.6171472306414553 + - - 0.7868476953715899 + - 0.6171472306414553 +- - - 0.7945483039124437 + - 0.6072009492333317 + - - 0.7945483039124437 + - 0.6072009492333317 +- - - 0.8021231927550437 + - 0.5971585917027863 + - - 0.8021231927550437 + - 0.5971585917027863 +- - - 0.809571163340744 + - 0.5870217470308187 + - - 0.809571163340744 + - 0.5870217470308187 +- - - 0.8168910371929053 + - 0.5767920191489297 + - - 0.8168910371929053 + - 0.5767920191489297 +- - - 0.8240816561033644 + - 0.566471026685334 + - - 0.8240816561033644 + - 0.566471026685334 +- - - 0.8311418823156935 + - 0.5560604027088476 + - - 0.8311418823156935 + - 0.5560604027088476 +- - - 0.8380705987052264 + - 0.545561794470492 + - - 0.8380705987052264 + - 0.545561794470492 +- - - 0.8448667089558177 + - 0.5349768631428518 + - - 0.8448667089558177 + - 0.5349768631428518 +- - - 0.8515291377333112 + - 0.5243072835572319 + - - 0.8515291377333112 + - 0.5243072835572319 +- - - 0.8580568308556875 + - 0.5135547439386516 + - - 0.8580568308556875 + - 0.5135547439386516 +- - - 0.8644487554598649 + - 0.5027209456387218 + - - 0.8644487554598649 + - 0.5027209456387218 +- - - 0.8707039001651274 + - 0.4918076028664418 + - - 0.8707039001651274 + - 0.4918076028664418 +- - - 0.8768212752331536 + - 0.4808164424169648 + - - 0.8768212752331536 + - 0.4808164424169648 +- - - 0.8827999127246196 + - 0.4697492033983709 + - - 0.8827999127246196 + - 0.4697492033983709 +- - - 0.8886388666523558 + - 0.45860763695649104 + - - 0.8886388666523558 + - 0.45860763695649104 +- - - 0.8943372131310272 + - 0.4473935059978269 + - - 0.8943372131310272 + - 0.4473935059978269 +- - - 0.8998940505233182 + - 0.4361085849106111 + - - 0.8998940505233182 + - 0.4361085849106111 +- - - 0.9053084995825966 + - 0.42475465928404793 + - - 0.9053084995825966 + - 0.42475465928404793 +- - - 0.9105797035920355 + - 0.4133335256257842 + - - 0.9105797035920355 + - 0.4133335256257842 +- - - 0.9157068285001692 + - 0.4018469910776512 + - - 0.9157068285001692 + - 0.4018469910776512 +- - - 0.920689063052863 + - 0.3902968731297256 + - - 0.920689063052863 + - 0.3902968731297256 +- - - 0.9255256189216778 + - 0.3786849993327503 + - - 0.9255256189216778 + - 0.3786849993327503 +- - - 0.9302157308286042 + - 0.3670132070089654 + - - 0.9302157308286042 + - 0.3670132070089654 +- - - 0.934758656667151 + - 0.35528334296139374 + - - 0.934758656667151 + - 0.35528334296139374 +- - - 0.9391536776197676 + - 0.34349726318162344 + - - 0.9391536776197676 + - 0.34349726318162344 +- - - 0.9434000982715812 + - 0.3316568325561391 + - - 0.9434000982715812 + - 0.3316568325561391 +- - - 0.9474972467204298 + - 0.31976392457124536 + - - 0.9474972467204298 + - 0.31976392457124536 +- - - 0.9514444746831766 + - 0.30782042101662793 + - - 0.9514444746831766 + - 0.30782042101662793 +- - - 0.9552411575982869 + - 0.2958282116876025 + - - 0.9552411575982869 + - 0.2958282116876025 +- - - 0.9588866947246497 + - 0.28378919408609693 + - - 0.9588866947246497 + - 0.28378919408609693 +- - - 0.9623805092366334 + - 0.27170527312041276 + - - 0.9623805092366334 + - 0.27170527312041276 +- - - 0.9657220483153546 + - 0.25957836080381586 + - - 0.9657220483153546 + - 0.25957836080381586 +- - - 0.9689107832361495 + - 0.24741037595200252 + - - 0.9689107832361495 + - 0.24741037595200252 +- - - 0.9719462094522335 + - 0.23520324387949015 + - - 0.9719462094522335 + - 0.23520324387949015 +- - - 0.9748278466745341 + - 0.2229588960949774 + - - 0.9748278466745341 + - 0.2229588960949774 +- - - 0.9775552389476861 + - 0.21067926999572642 + - - 0.9775552389476861 + - 0.21067926999572642 +- - - 0.9801279547221765 + - 0.19836630856101303 + - - 0.9801279547221765 + - 0.19836630856101303 +- - - 0.9825455869226277 + - 0.18602196004469224 + - - 0.9825455869226277 + - 0.18602196004469224 +- - - 0.984807753012208 + - 0.17364817766693041 + - - 0.984807753012208 + - 0.17364817766693041 +- - - 0.98691409505316 + - 0.16124691930515242 + - - 0.98691409505316 + - 0.16124691930515242 +- - - 0.9888642797634357 + - 0.14882014718424924 + - - 0.9888642797634357 + - 0.14882014718424924 +- - - 0.9906579985694317 + - 0.1363698275661 + - - 0.9906579985694317 + - 0.1363698275661 +- - - 0.9922949676548136 + - 0.12389793043845522 + - - 0.9922949676548136 + - 0.12389793043845522 +- - - 0.9937749280054242 + - 0.11140642920322849 + - - 0.9937749280054242 + - 0.11140642920322849 +- - - 0.995097645450266 + - 0.09889730036424986 + - - 0.995097645450266 + - 0.09889730036424986 +- - - 0.9962629106985543 + - 0.08637252321452853 + - - 0.9962629106985543 + - 0.08637252321452853 +- - - 0.9972705393728327 + - 0.07383407952307214 + - - 0.9972705393728327 + - 0.07383407952307214 +- - - 0.9981203720381463 + - 0.06128395322131638 + - - 0.9981203720381463 + - 0.06128395322131638 +- - - 0.9988122742272691 + - 0.04872413008921228 + - - 0.9988122742272691 + - 0.04872413008921228 +- - - 0.9993461364619809 + - 0.036156597441019206 + - - 0.9993461364619809 + - 0.036156597441019206 +- - - 0.9997218742703887 + - 0.023583343810857166 + - - 0.9997218742703887 + - 0.023583343810857166 +- - - 0.9999394282002937 + - 0.011006358638064812 + - - 0.9999394282002937 + - 0.011006358638064812 +- - - 0.9999987638285974 + - -0.001572368047584414 + - - 0.9999987638285974 + - -0.001572368047584414 +- - - 0.9998998717667489 + - -0.014150845940761853 + - - 0.9998998717667489 + - -0.014150845940761853 +- - - 0.9996427676622299 + - -0.026727084775504745 + - - 0.9996427676622299 + - -0.026727084775504745 +- - - 0.9992274921960794 + - -0.03929909464013115 + - - 0.9992274921960794 + - -0.03929909464013115 +- - - 0.9986541110764565 + - -0.0518648862921008 + - - 0.9986541110764565 + - -0.0518648862921008 +- - - 0.9979227150282433 + - -0.06442247147276806 + - - 0.9979227150282433 + - -0.06442247147276806 +- - - 0.9970334197786902 + - -0.07696986322197923 + - - 0.9970334197786902 + - -0.07696986322197923 +- - - 0.9959863660391044 + - -0.08950507619246638 + - - 0.9959863660391044 + - -0.08950507619246638 +- - - 0.9947817194825853 + - -0.10202612696398403 + - - 0.9947817194825853 + - -0.10202612696398403 +- - - 0.9934196707178107 + - -0.11453103435714077 + - - 0.9934196707178107 + - -0.11453103435714077 +- - - 0.991900435258877 + - -0.12701781974687854 + - - 0.991900435258877 + - -0.12701781974687854 +- - - 0.9902242534911986 + - -0.1394845073755453 + - - 0.9902242534911986 + - -0.1394845073755453 +- - - 0.9883913906334728 + - -0.15192912466551547 + - - 0.9883913906334728 + - -0.15192912466551547 +- - - 0.9864021366957146 + - -0.16434970253130593 + - - 0.9864021366957146 + - -0.16434970253130593 +- - - 0.9842568064333687 + - -0.17674427569114137 + - - 0.9842568064333687 + - -0.17674427569114137 +- - - 0.9819557392975067 + - -0.18911088297791617 + - - 0.9819557392975067 + - -0.18911088297791617 +- - - 0.9794992993811165 + - -0.20144756764950503 + - - 0.9794992993811165 + - -0.20144756764950503 +- - - 0.9768878753614926 + - -0.21375237769837538 + - - 0.9768878753614926 + - -0.21375237769837538 +- - - 0.9741218804387363 + - -0.22602336616044894 + - - 0.9741218804387363 + - -0.22602336616044894 +- - - 0.9712017522703763 + - -0.23825859142316483 + - - 0.9712017522703763 + - -0.23825859142316483 +- - - 0.9681279529021188 + - -0.25045611753269825 + - - 0.9681279529021188 + - -0.25045611753269825 +- - - 0.9649009686947391 + - -0.2626140145002818 + - - 0.9649009686947391 + - -0.2626140145002818 +- - - 0.9615213102471255 + - -0.27473035860758266 + - - 0.9615213102471255 + - -0.27473035860758266 +- - - 0.9579895123154889 + - -0.28680323271109 + - - 0.9579895123154889 + - -0.28680323271109 +- - - 0.9543061337287488 + - -0.29883072654545967 + - - 0.9543061337287488 + - -0.29883072654545967 +- - - 0.9504717573001116 + - -0.310810937025771 + - - 0.9504717573001116 + - -0.310810937025771 +- - - 0.9464869897348526 + - -0.32274196854864906 + - - 0.9464869897348526 + - -0.32274196854864906 +- - - 0.9423524615343186 + - -0.33462193329220136 + - - 0.9423524615343186 + - -0.33462193329220136 +- - - 0.9380688268961659 + - -0.3464489515147234 + - - 0.9380688268961659 + - -0.3464489515147234 +- - - 0.9336367636108462 + - -0.3582211518521272 + - - 0.9336367636108462 + - -0.3582211518521272 +- - - 0.9290569729543628 + - -0.369936671614043 + - - 0.9290569729543628 + - -0.369936671614043 +- - - 0.9243301795773085 + - -0.38159365707854837 + - - 0.9243301795773085 + - -0.38159365707854837 +- - - 0.9194571313902055 + - -0.3931902637854787 + - - 0.9194571313902055 + - -0.3931902637854787 +- - - 0.9144385994451658 + - -0.40472465682827324 + - - 0.9144385994451658 + - -0.40472465682827324 +- - - 0.9092753778138886 + - -0.4161950111443075 + - - 0.9092753778138886 + - -0.4161950111443075 +- - - 0.9039682834620162 + - -0.42759951180366895 + - - 0.9039682834620162 + - -0.42759951180366895 +- - - 0.8985181561198674 + - -0.4389363542963303 + - - 0.8985181561198674 + - -0.4389363542963303 +- - - 0.8929258581495686 + - -0.450203744817673 + - - 0.8929258581495686 + - -0.450203744817673 +- - - 0.8871922744086043 + - -0.46139990055231683 + - - 0.8871922744086043 + - -0.46139990055231683 +- - - 0.881318312109807 + - -0.47252304995621186 + - - 0.881318312109807 + - -0.47252304995621186 +- - - 0.8753049006778131 + - -0.4835714330369443 + - - 0.8753049006778131 + - -0.4835714330369443 +- - - 0.869152991601999 + - -0.4945433016322186 + - - 0.869152991601999 + - -0.4945433016322186 +- - - 0.8628635582859312 + - -0.5054369196864643 + - - 0.8628635582859312 + - -0.5054369196864643 +- - - 0.856437595893346 + - -0.5162505635255284 + - - 0.856437595893346 + - -0.5162505635255284 +- - - 0.8498761211906867 + - -0.5269825221294092 + - - 0.8498761211906867 + - -0.5269825221294092 +- - - 0.8431801723862224 + - -0.5376310974029872 + - - 0.8431801723862224 + - -0.5376310974029872 +- - - 0.8363508089657762 + - -0.5481946044447097 + - - 0.8363508089657762 + - -0.5481946044447097 +- - - 0.8293891115250829 + - -0.5586713718131919 + - - 0.8293891115250829 + - -0.5586713718131919 +- - - 0.8222961815988096 + - -0.5690597417916836 + - - 0.8222961815988096 + - -0.5690597417916836 +- - - 0.8150731414862624 + - -0.5793580706503667 + - - 0.8150731414862624 + - -0.5793580706503667 +- - - 0.8077211340738071 + - -0.5895647289064391 + - - 0.8077211340738071 + - -0.5895647289064391 +- - - 0.800241322654032 + - -0.5996781015819448 + - - 0.800241322654032 + - -0.5996781015819448 +- - - 0.7926348907416848 + - -0.6096965884593069 + - - 0.7926348907416848 + - -0.6096965884593069 +- - - 0.7849030418864046 + - -0.6196186043345285 + - - 0.7849030418864046 + - -0.6196186043345285 +- - - 0.7770469994822886 + - -0.6294425792680156 + - - 0.7770469994822886 + - -0.6294425792680156 +- - - 0.769068006574317 + - -0.6391669588329847 + - - 0.769068006574317 + - -0.6391669588329847 +- - - 0.7609673256616678 + - -0.648790204361417 + - - 0.7609673256616678 + - -0.648790204361417 +- - - 0.7527462384979551 + - -0.6583107931875185 + - - 0.7527462384979551 + - -0.6583107931875185 +- - - 0.744406045888419 + - -0.6677272188886485 + - - 0.744406045888419 + - -0.6677272188886485 +- - - 0.7359480674841035 + - -0.6770379915236763 + - - 0.7359480674841035 + - -0.6770379915236763 +- - - 0.7273736415730488 + - -0.6862416378687335 + - - 0.7273736415730488 + - -0.6862416378687335 +- - - 0.7186841248685385 + - -0.6953367016503177 + - - 0.7186841248685385 + - -0.6953367016503177 +- - - 0.7098808922944289 + - -0.7043217437757161 + - - 0.7098808922944289 + - -0.7043217437757161 +- - - 0.7009653367675978 + - -0.7131953425607098 + - - 0.7009653367675978 + - -0.7131953425607098 +- - - 0.6919388689775463 + - -0.7219560939545244 + - - 0.6919388689775463 + - -0.7219560939545244 +- - - 0.6828029171631891 + - -0.7306026117619886 + - - 0.6828029171631891 + - -0.7306026117619886 +- - - 0.673558926886866 + - -0.739133527862871 + - - 0.673558926886866 + - -0.739133527862871 +- - - 0.6642083608056142 + - -0.7475474924283534 + - - 0.6642083608056142 + - -0.7475474924283534 +- - - 0.6547526984397353 + - -0.7558431741346118 + - - 0.6547526984397353 + - -0.7558431741346118 +- - - 0.6451934359386937 + - -0.764019260373469 + - - 0.6451934359386937 + - -0.764019260373469 +- - - 0.6355320858443845 + - -0.7720744574600859 + - - 0.6355320858443845 + - -0.7720744574600859 +- - - 0.6257701768518059 + - -0.7800074908376582 + - - 0.6257701768518059 + - -0.7800074908376582 +- - - 0.6159092535671797 + - -0.7878171052790867 + - - 0.6159092535671797 + - -0.7878171052790867 +- - - 0.6059508762635484 + - -0.7955020650855897 + - - 0.6059508762635484 + - -0.7955020650855897 +- - - 0.5958966206338979 + - -0.8030611542822255 + - - 0.5958966206338979 + - -0.8030611542822255 +- - - 0.5857480775418397 + - -0.8104931768102919 + - - 0.5857480775418397 + - -0.8104931768102919 +- - - 0.5755068527698903 + - -0.8177969567165775 + - - 0.5755068527698903 + - -0.8177969567165775 +- - - 0.5651745667653929 + - -0.8249713383394301 + - - 0.5651745667653929 + - -0.8249713383394301 +- - - 0.5547528543841173 + - -0.8320151864916135 + - - 0.5547528543841173 + - -0.8320151864916135 +- - - 0.5442433646315792 + - -0.8389273866399272 + - - 0.5442433646315792 + - -0.8389273866399272 +- - - 0.5336477604021226 + - -0.8457068450815559 + - - 0.5336477604021226 + - -0.8457068450815559 +- - - 0.5229677182158028 + - -0.8523524891171238 + - - 0.5229677182158028 + - -0.8523524891171238 +- - - 0.5122049279531147 + - -0.8588632672204258 + - - 0.5122049279531147 + - -0.8588632672204258 +- - - 0.5013610925876063 + - -0.865238149204808 + - - 0.5013610925876063 + - -0.865238149204808 +- - - 0.49043792791642066 + - -0.8714761263861723 + - - 0.49043792791642066 + - -0.8714761263861723 +- - - 0.47943716228880995 + - -0.8775762117425775 + - - 0.47943716228880995 + - -0.8775762117425775 +- - - 0.4683605363326608 + - -0.8835374400704151 + - - 0.4683605363326608 + - -0.8835374400704151 +- - - 0.4572098026790794 + - -0.8893588681371302 + - - 0.4572098026790794 + - -0.8893588681371302 +- - - 0.44598672568507636 + - -0.8950395748304677 + - - 0.44598672568507636 + - -0.8950395748304677 +- - - 0.4346930811543961 + - -0.9005786613042182 + - - 0.4346930811543961 + - -0.9005786613042182 +- - - 0.4233306560565345 + - -0.9059752511204399 + - - 0.4233306560565345 + - -0.9059752511204399 +- - - 0.4119012482439928 + - -0.9112284903881356 + - - 0.4119012482439928 + - -0.9112284903881356 +- - - 0.40040666616780407 + - -0.916337547898363 + - - 0.40040666616780407 + - -0.916337547898363 +- - - 0.3888487285913878 + - -0.9213016152557539 + - - 0.3888487285913878 + - -0.9213016152557539 +- - - 0.37722926430277026 + - -0.9261199070064258 + - - 0.37722926430277026 + - -0.9261199070064258 +- - - 0.36555011182521946 + - -0.9307916607622618 + - - 0.36555011182521946 + - -0.9307916607622618 +- - - 0.3538131191263388 + - -0.9353161373215428 + - - 0.3538131191263388 + - -0.9353161373215428 +- - - 0.3420201433256689 + - -0.9396926207859083 + - - 0.3420201433256689 + - -0.9396926207859083 +- - - 0.330173050400837 + - -0.9439204186736329 + - - 0.330173050400837 + - -0.9439204186736329 +- - - 0.3182737148923088 + - -0.9479988620291954 + - - 0.3182737148923088 + - -0.9479988620291954 +- - - 0.3063240196067838 + - -0.9519273055291264 + - - 0.3063240196067838 + - -0.9519273055291264 +- - - 0.29432585531928224 + - -0.9557051275841167 + - - 0.29432585531928224 + - -0.9557051275841167 +- - - 0.2822811204739722 + - -0.9593317304373701 + - - 0.2822811204739722 + - -0.9593317304373701 +- - - 0.27019172088378224 + - -0.9628065402591843 + - - 0.27019172088378224 + - -0.9628065402591843 +- - - 0.25805956942885044 + - -0.9661290072377479 + - - 0.25805956942885044 + - -0.9661290072377479 +- - - 0.24588658575385056 + - -0.9692986056661355 + - - 0.24588658575385056 + - -0.9692986056661355 +- - - 0.23367469596425278 + - -0.9723148340254889 + - - 0.23367469596425278 + - -0.9723148340254889 +- - - 0.22142583232155955 + - -0.975177215064372 + - - 0.22142583232155955 + - -0.975177215064372 +- - - 0.20914193293756786 + - -0.977885295874285 + - - 0.20914193293756786 + - -0.977885295874285 +- - - 0.19682494146770554 + - -0.9804386479613267 + - - 0.19682494146770554 + - -0.9804386479613267 +- - - 0.18447680680349254 + - -0.9828368673139948 + - - 0.18447680680349254 + - -0.9828368673139948 +- - - 0.17209948276416928 + - -0.9850795744671115 + - - 0.17209948276416928 + - -0.9850795744671115 +- - - 0.15969492778754976 + - -0.9871664145618657 + - - 0.15969492778754976 + - -0.9871664145618657 +- - - 0.14726510462014156 + - -0.9890970574019613 + - - 0.14726510462014156 + - -0.9890970574019613 +- - - 0.1348119800065847 + - -0.9908711975058636 + - - 0.1348119800065847 + - -0.9908711975058636 +- - - 0.12233752437845731 + - -0.992488554155135 + - - 0.12233752437845731 + - -0.992488554155135 +- - - 0.1098437115425002 + - -0.9939488714388522 + - - 0.1098437115425002 + - -0.9939488714388522 +- - - 0.09733251836830287 + - -0.9952519182940991 + - - 0.09733251836830287 + - -0.9952519182940991 +- - - 0.0848059244755095 + - -0.9963974885425265 + - - 0.0848059244755095 + - -0.9963974885425265 +- - - 0.07226591192058739 + - -0.9973854009229761 + - - 0.07226591192058739 + - -0.9973854009229761 +- - - 0.05971446488321034 + - -0.9982154991201608 + - - 0.05971446488321034 + - -0.9982154991201608 +- - - 0.04715356935230619 + - -0.9988876517893978 + - - 0.04715356935230619 + - -0.9988876517893978 +- - - 0.034585212811817465 + - -0.9994017525773913 + - - 0.034585212811817465 + - -0.9994017525773913 +- - - 0.022011383926227784 + - -0.9997577201390606 + - - 0.022011383926227784 + - -0.9997577201390606 +- - - 0.009434072225897046 + - -0.999955498150411 + - - 0.009434072225897046 + - -0.999955498150411 +- - - -0.0031447322077359985 + - -0.9999950553174459 + - - -0.0031447322077359985 + - -0.9999950553174459 +- - - -0.015723039057040564 + - -0.9998763853811183 + - - -0.015723039057040564 + - -0.9998763853811183 +- - - -0.02829885808311759 + - -0.9995995071183217 + - - -0.02829885808311759 + - -0.9995995071183217 +- - - -0.04087019944071145 + - -0.9991644643389178 + - - -0.04087019944071145 + - -0.9991644643389178 +- - - -0.053435073993057226 + - -0.9985713258788059 + - - -0.053435073993057226 + - -0.9985713258788059 +- - - -0.06599149362662023 + - -0.9978201855890307 + - - -0.06599149362662023 + - -0.9978201855890307 +- - - -0.07853747156566927 + - -0.996911162320932 + - - -0.07853747156566927 + - -0.996911162320932 +- - - -0.09107102268664041 + - -0.9958443999073396 + - - -0.09107102268664041 + - -0.9958443999073396 +- - - -0.10359016383223883 + - -0.9946200671398149 + - - -0.10359016383223883 + - -0.9946200671398149 +- - - -0.11609291412522968 + - -0.993238357741943 + - - -0.11609291412522968 + - -0.993238357741943 +- - - -0.12857729528186848 + - -0.9916994903386808 + - - -0.12857729528186848 + - -0.9916994903386808 +- - - -0.14104133192491908 + - -0.9900037084217639 + - - -0.14104133192491908 + - -0.9900037084217639 +- - - -0.15348305189621594 + - -0.9881512803111796 + - - -0.15348305189621594 + - -0.9881512803111796 +- - - -0.16590048656871298 + - -0.9861424991127116 + - - -0.16590048656871298 + - -0.9861424991127116 +- - - -0.1782916711579755 + - -0.9839776826715616 + - - -0.1782916711579755 + - -0.9839776826715616 +- - - -0.19065464503306404 + - -0.9816571735220583 + - - -0.19065464503306404 + - -0.9816571735220583 +- - - -0.20298745202676116 + - -0.979181338833458 + - - -0.20298745202676116 + - -0.979181338833458 +- - - -0.2152881407450901 + - -0.9765505703518493 + - - -0.2152881407450901 + - -0.9765505703518493 +- - - -0.2275547648760821 + - -0.9737652843381669 + - - -0.2275547648760821 + - -0.9737652843381669 +- - - -0.23978538349773562 + - -0.9708259215023277 + - - -0.23978538349773562 + - -0.9708259215023277 +- - - -0.25197806138512474 + - -0.967732946933499 + - - -0.25197806138512474 + - -0.967732946933499 +- - - -0.2641308693166058 + - -0.9644868500265071 + - - -0.2641308693166058 + - -0.9644868500265071 +- - - -0.2762418843790738 + - -0.9610881444044029 + - - -0.2762418843790738 + - -0.9610881444044029 +- - - -0.2883091902722216 + - -0.9575373678371909 + - - -0.2883091902722216 + - -0.9575373678371909 +- - - -0.3003308776117502 + - -0.9538350821567405 + - - -0.3003308776117502 + - -0.9538350821567405 +- - - -0.31230504423148914 + - -0.9499818731678872 + - - -0.31230504423148914 + - -0.9499818731678872 +- - - -0.32422979548437053 + - -0.9459783505557425 + - - -0.32422979548437053 + - -0.9459783505557425 +- - - -0.33610324454221563 + - -0.9418251477892251 + - - -0.33610324454221563 + - -0.9418251477892251 +- - - -0.34792351269428334 + - -0.9375229220208277 + - - -0.34792351269428334 + - -0.9375229220208277 +- - - -0.3596887296445355 + - -0.9330723539826374 + - - -0.3596887296445355 + - -0.9330723539826374 +- - - -0.3713970338075679 + - -0.9284741478786258 + - - -0.3713970338075679 + - -0.9284741478786258 +- - - -0.3830465726031674 + - -0.9237290312732227 + - - -0.3830465726031674 + - -0.9237290312732227 +- - - -0.3946355027494405 + - -0.9188377549761962 + - - -0.3946355027494405 + - -0.9188377549761962 +- - - -0.406161990554472 + - -0.9138010929238535 + - - -0.406161990554472 + - -0.9138010929238535 +- - - -0.41762421220646645 + - -0.9086198420565822 + - - -0.41762421220646645 + - -0.9086198420565822 +- - - -0.4290203540623263 + - -0.9032948221927524 + - - -0.4290203540623263 + - -0.9032948221927524 +- - - -0.44034861293461913 + - -0.8978268758989992 + - - -0.44034861293461913 + - -0.8978268758989992 +- - - -0.4516071963768948 + - -0.892216868356904 + - - -0.4516071963768948 + - -0.892216868356904 +- - - -0.46279432296729867 + - -0.8864656872260989 + - - -0.46279432296729867 + - -0.8864656872260989 +- - - -0.47390822259044274 + - -0.8805742425038149 + - - -0.47390822259044274 + - -0.8805742425038149 +- - - -0.4849471367174873 + - -0.8745434663808944 + - - -0.4849471367174873 + - -0.8745434663808944 +- - - -0.495909318684389 + - -0.8683743130942929 + - - -0.495909318684389 + - -0.8683743130942929 +- - - -0.5067930339682724 + - -0.8620677587760915 + - - -0.5067930339682724 + - -0.8620677587760915 +- - - -0.5175965604618782 + - -0.8556248012990468 + - - -0.5175965604618782 + - -0.8556248012990468 +- - - -0.5283181887460511 + - -0.849046460118698 + - - -0.5283181887460511 + - -0.849046460118698 +- - - -0.538956222360216 + - -0.842333776112062 + - - -0.538956222360216 + - -0.842333776112062 +- - - -0.5495089780708056 + - -0.8354878114129367 + - - -0.5495089780708056 + - -0.8354878114129367 +- - - -0.5599747861375949 + - -0.8285096492438424 + - - -0.5599747861375949 + - -0.8285096492438424 +- - - -0.5703519905779012 + - -0.8214003937446254 + - - -0.5703519905779012 + - -0.8214003937446254 +- - - -0.5806389494286053 + - -0.814161169797753 + - - -0.5806389494286053 + - -0.814161169797753 +- - - -0.5908340350059578 + - -0.8067931228503245 + - - -0.5908340350059578 + - -0.8067931228503245 +- - - -0.6009356341631226 + - -0.7992974187328304 + - - -0.6009356341631226 + - -0.7992974187328304 +- - - -0.6109421485454225 + - -0.7916752434746857 + - - -0.6109421485454225 + - -0.7916752434746857 +- - - -0.6208519948432432 + - -0.7839278031165661 + - - -0.6208519948432432 + - -0.7839278031165661 +- - - -0.630663605042557 + - -0.7760563235195791 + - - -0.630663605042557 + - -0.7760563235195791 +- - - -0.6403754266730258 + - -0.7680620501712998 + - - -0.6403754266730258 + - -0.7680620501712998 +- - - -0.6499859230536464 + - -0.7599462479886977 + - - -0.6499859230536464 + - -0.7599462479886977 +- - - -0.6594935735358957 + - -0.7517102011179935 + - - -0.6594935735358957 + - -0.7517102011179935 +- - - -0.6688968737443391 + - -0.7433552127314704 + - - -0.6688968737443391 + - -0.7433552127314704 +- - - -0.6781943358146659 + - -0.7348826048212762 + - - -0.6781943358146659 + - -0.7348826048212762 +- - - -0.6873844886291098 + - -0.7262937179902474 + - - -0.6873844886291098 + - -0.7262937179902474 +- - - -0.6964658780492216 + - -0.717589911239788 + - - -0.6964658780492216 + - -0.717589911239788 +- - - -0.7054370671459529 + - -0.7087725617548385 + - - -0.7054370671459529 + - -0.7087725617548385 +- - - -0.7142966364270207 + - -0.6998430646859656 + - - -0.7142966364270207 + - -0.6998430646859656 +- - - -0.723043184061509 + - -0.6908028329286112 + - - -0.723043184061509 + - -0.6908028329286112 +- - - -0.731675326101678 + - -0.6816532968995332 + - - -0.731675326101678 + - -0.6816532968995332 +- - - -0.7401916967019432 + - -0.6723959043104729 + - - -0.7401916967019432 + - -0.6723959043104729 +- - - -0.7485909483349904 + - -0.6630321199390868 + - - -0.7485909483349904 + - -0.6630321199390868 +- - - -0.7568717520049916 + - -0.6535634253971795 + - - -0.7568717520049916 + - -0.6535634253971795 +- - - -0.7650327974578898 + - -0.6439913188962686 + - - -0.7650327974578898 + - -0.6439913188962686 +- - - -0.7730727933887175 + - -0.634317315010528 + - - -0.7730727933887175 + - -0.634317315010528 +- - - -0.7809904676459172 + - -0.6245429444371393 + - - -0.7809904676459172 + - -0.6245429444371393 +- - - -0.788784567432631 + - -0.6146697537540928 + - - -0.788784567432631 + - -0.6146697537540928 +- - - -0.7964538595049286 + - -0.6046993051754759 + - - -0.7964538595049286 + - -0.6046993051754759 +- - - -0.8039971303669401 + - -0.5946331763042871 + - - -0.8039971303669401 + - -0.5946331763042871 +- - - -0.8114131864628653 + - -0.5844729598828156 + - - -0.8114131864628653 + - -0.5844729598828156 +- - - -0.8187008543658276 + - -0.5742202635406243 + - - -0.8187008543658276 + - -0.5742202635406243 +- - - -0.825858980963543 + - -0.5638767095401779 + - - -0.825858980963543 + - -0.5638767095401779 +- - - -0.8328864336407734 + - -0.5534439345201586 + - - -0.8328864336407734 + - -0.5534439345201586 +- - - -0.8397821004585396 + - -0.5429235892364995 + - - -0.8397821004585396 + - -0.5429235892364995 +- - - -0.8465448903300604 + - -0.5323173383011922 + - - -0.8465448903300604 + - -0.5323173383011922 +- - - -0.8531737331933926 + - -0.521626859918898 + - - -0.8531737331933926 + - -0.521626859918898 +- - - -0.8596675801807451 + - -0.5108538456214089 + - - -0.8596675801807451 + - -0.5108538456214089 +- - - -0.8660254037844384 + - -0.5000000000000004 + - - -0.8660254037844384 + - -0.5000000000000004 +- - - -0.872246198019486 + - -0.4890670404357173 + - - -0.872246198019486 + - -0.4890670404357173 +- - - -0.8783289785827684 + - -0.4780566968276366 + - - -0.8783289785827684 + - -0.4780566968276366 +- - - -0.8842727830087774 + - -0.46697071131914863 + - - -0.8842727830087774 + - -0.46697071131914863 +- - - -0.8900766708219056 + - -0.4558108380223019 + - - -0.8900766708219056 + - -0.4558108380223019 +- - - -0.895739723685255 + - -0.4445788427402534 + - - -0.895739723685255 + - -0.4445788427402534 +- - - -0.9012610455459443 + - -0.4332765026878693 + - - -0.9012610455459443 + - -0.4332765026878693 +- - - -0.9066397627768893 + - -0.4219056062105194 + - - -0.9066397627768893 + - -0.4219056062105194 +- - - -0.9118750243150336 + - -0.410467952501114 + - - -0.9118750243150336 + - -0.410467952501114 +- - - -0.9169660017960133 + - -0.39896535131541655 + - - -0.9169660017960133 + - -0.39896535131541655 +- - - -0.921911889685225 + - -0.38739962268569333 + - - -0.921911889685225 + - -0.38739962268569333 +- - - -0.9267119054052849 + - -0.37577259663273255 + - - -0.9267119054052849 + - -0.37577259663273255 +- - - -0.931365289459854 + - -0.3640861128762842 + - - -0.931365289459854 + - -0.3640861128762842 +- - - -0.9358713055538119 + - -0.3523420205439648 + - - -0.9358713055538119 + - -0.3523420205439648 +- - - -0.9402292407097588 + - -0.3405421778786742 + - - -0.9402292407097588 + - -0.3405421778786742 +- - - -0.9444384053808287 + - -0.32868845194456947 + - - -0.9444384053808287 + - -0.32868845194456947 +- - - -0.948498133559795 + - -0.3167827183316434 + - - -0.948498133559795 + - -0.3167827183316434 +- - - -0.9524077828844512 + - -0.30482686085895394 + - - -0.9524077828844512 + - -0.30482686085895394 +- - - -0.9561667347392507 + - -0.2928227712765512 + - - -0.9561667347392507 + - -0.2928227712765512 +- - - -0.959774394353189 + - -0.28077234896614933 + - - -0.959774394353189 + - -0.28077234896614933 +- - - -0.9632301908939126 + - -0.26867750064059465 + - - -0.9632301908939126 + - -0.26867750064059465 +- - - -0.9665335775580413 + - -0.25654014004216524 + - - -0.9665335775580413 + - -0.25654014004216524 +- - - -0.9696840316576876 + - -0.2443621876397672 + - - -0.9696840316576876 + - -0.2443621876397672 +- - - -0.97268105470316 + - -0.2321455703250619 + - - -0.97268105470316 + - -0.2321455703250619 +- - - -0.9755241724818386 + - -0.21989222110757806 + - - -0.9755241724818386 + - -0.21989222110757806 +- - - -0.9782129351332083 + - -0.2076040788088557 + - - -0.9782129351332083 + - -0.2076040788088557 +- - - -0.9807469172200395 + - -0.19528308775567055 + - - -0.9807469172200395 + - -0.19528308775567055 +- - - -0.9831257177957041 + - -0.18293119747238726 + - - -0.9831257177957041 + - -0.18293119747238726 +- - - -0.9853489604676163 + - -0.17055036237249038 + - - -0.9853489604676163 + - -0.17055036237249038 +- - - -0.9874162934567888 + - -0.15814254144934156 + - - -0.9874162934567888 + - -0.15814254144934156 +- - - -0.9893273896534934 + - -0.14570969796621222 + - - -0.9893273896534934 + - -0.14570969796621222 +- - - -0.9910819466690195 + - -0.1332537991456406 + - - -0.9910819466690195 + - -0.1332537991456406 +- - - -0.9926796868835203 + - -0.1207768158581612 + - - -0.9926796868835203 + - -0.1207768158581612 +- - - -0.9941203574899392 + - -0.10828072231046196 + - - -0.9941203574899392 + - -0.10828072231046196 +- - - -0.9954037305340125 + - -0.09576749573300417 + - - -0.9954037305340125 + - -0.09576749573300417 +- - - -0.9965296029503367 + - -0.08323911606717305 + - - -0.9965296029503367 + - -0.08323911606717305 +- - - -0.9974977965944997 + - -0.070697565651995 + - - -0.9974977965944997 + - -0.070697565651995 +- - - -0.9983081582712682 + - -0.05814482891047624 + - - -0.9983081582712682 + - -0.05814482891047624 +- - - -0.9989605597588274 + - -0.04558289203561173 + - - -0.9989605597588274 + - -0.04558289203561173 +- - - -0.9994548978290693 + - -0.0330137426761141 + - - -0.9994548978290693 + - -0.0330137426761141 +- - - -0.9997910942639261 + - -0.020439369621912166 + - - -0.9997910942639261 + - -0.020439369621912166 +- - - -0.9999690958677468 + - -0.007861762489468911 + - - -0.9999690958677468 + - -0.007861762489468911 +- - - -0.999988874475714 + - 0.004717088593031313 + - - -0.999988874475714 + - 0.004717088593031313 +- - - -0.9998504269583004 + - 0.01729519330057657 + - - -0.9998504269583004 + - 0.01729519330057657 +- - - -0.9995537752217639 + - 0.029870561426252256 + - - -0.9995537752217639 + - 0.029870561426252256 +- - - -0.9990989662046815 + - 0.04244120319614822 + - - -0.9990989662046815 + - 0.04244120319614822 +- - - -0.9984860718705224 + - 0.055005129584192916 + - - -0.9984860718705224 + - 0.055005129584192916 +- - - -0.9977151891962615 + - 0.06756035262687816 + - - -0.9977151891962615 + - 0.06756035262687816 +- - - -0.9967864401570343 + - 0.08010488573780679 + - - -0.9967864401570343 + - 0.08010488573780679 +- - - -0.9956999717068378 + - 0.09263674402202696 + - - -0.9956999717068378 + - 0.09263674402202696 +- - - -0.9944559557552776 + - 0.10515394459009784 + - - -0.9944559557552776 + - 0.10515394459009784 +- - - -0.9930545891403677 + - 0.11765450687183807 + - - -0.9930545891403677 + - 0.11765450687183807 +- - - -0.9914960935973849 + - 0.1301364529297071 + - - -0.9914960935973849 + - 0.1301364529297071 +- - - -0.9897807157237836 + - 0.1425978077717702 + - - -0.9897807157237836 + - 0.1425978077717702 +- - - -0.9879087269401782 + - 0.1550365996641971 + - - -0.9879087269401782 + - 0.1550365996641971 +- - - -0.9858804234473959 + - 0.16745086044324545 + - - -0.9858804234473959 + - 0.16745086044324545 +- - - -0.9836961261796103 + - 0.17983862582667898 + - - -0.9836961261796103 + - 0.17983862582667898 +- - - -0.9813561807535597 + - 0.19219793572457194 + - - -0.9813561807535597 + - 0.19219793572457194 +- - - -0.9788609574138615 + - 0.20452683454945075 + - - -0.9788609574138615 + - 0.20452683454945075 +- - - -0.9762108509744296 + - 0.21682337152571898 + - - -0.9762108509744296 + - 0.21682337152571898 +- - - -0.9734062807560028 + - 0.22908560099832972 + - - -0.9734062807560028 + - 0.22908560099832972 +- - - -0.9704476905197971 + - 0.24131158274063894 + - - -0.9704476905197971 + - 0.24131158274063894 +- - - -0.9673355483972903 + - 0.25349938226140434 + - - -0.9673355483972903 + - 0.25349938226140434 +- - - -0.9640703468161508 + - 0.2656470711108758 + - - -0.9640703468161508 + - 0.2656470711108758 +- - - -0.9606526024223212 + - 0.27775272718593 + - - -0.9606526024223212 + - 0.27775272718593 +- - - -0.957082855998271 + - 0.28981443503420057 + - - -0.957082855998271 + - 0.28981443503420057 +- - - -0.9533616723774295 + - 0.30183028615715607 + - - -0.9533616723774295 + - 0.30183028615715607 +- - - -0.9494896403548136 + - 0.31379837931207794 + - - -0.9494896403548136 + - 0.31379837931207794 +- - - -0.9454673725938637 + - 0.3257168208128897 + - - -0.9454673725938637 + - 0.3257168208128897 +- - - -0.9412955055295036 + - 0.33758372482979143 + - - -0.9412955055295036 + - 0.33758372482979143 +- - - -0.9369746992674384 + - 0.34939721368765 + - - -0.9369746992674384 + - 0.34939721368765 +- - - -0.9325056374797075 + - 0.361155418163101 + - - -0.9325056374797075 + - 0.361155418163101 +- - - -0.9278890272965095 + - 0.3728564777803084 + - - -0.9278890272965095 + - 0.3728564777803084 +- - - -0.9231255991943125 + - 0.3844985411053488 + - - -0.9231255991943125 + - 0.3844985411053488 +- - - -0.9182161068802741 + - 0.3960797660391565 + - - -0.9182161068802741 + - 0.3960797660391565 +- - - -0.9131613271729835 + - 0.4075983201089958 + - - -0.9131613271729835 + - 0.4075983201089958 +- - - -0.9079620598795464 + - 0.41905238075840945 + - - -0.9079620598795464 + - 0.41905238075840945 +- - - -0.9026191276690343 + - 0.4304401356355976 + - - -0.9026191276690343 + - 0.4304401356355976 +- - - -0.8971333759423143 + - 0.4417597828801825 + - - -0.8971333759423143 + - 0.4417597828801825 +- - - -0.8915056726982842 + - 0.4530095314083134 + - - -0.8915056726982842 + - 0.4530095314083134 +- - - -0.8857369083965297 + - 0.4641876011960654 + - - -0.8857369083965297 + - 0.4641876011960654 +- - - -0.8798279958164298 + - 0.4752922235610892 + - - -0.8798279958164298 + - 0.4752922235610892 +- - - -0.873779869912729 + - 0.486321641442466 + - - -0.873779869912729 + - 0.486321641442466 +- - - -0.8675934876676018 + - 0.49727410967872326 + - - -0.8675934876676018 + - 0.49727410967872326 +- - - -0.8612698279392309 + - 0.5081478952839691 + - - -0.8612698279392309 + - 0.5081478952839691 +- - - -0.8548098913069261 + - 0.5189412777220956 + - - -0.8548098913069261 + - 0.5189412777220956 +- - - -0.8482146999128025 + - 0.5296525491790203 + - - -0.8482146999128025 + - 0.5296525491790203 +- - - -0.8414852973000504 + - 0.5402800148329067 + - - -0.8414852973000504 + - 0.5402800148329067 +- - - -0.8346227482478176 + - 0.5508219931223336 + - - -0.8346227482478176 + - 0.5508219931223336 +- - - -0.8276281386027314 + - 0.5612768160123647 + - - -0.8276281386027314 + - 0.5612768160123647 +- - - -0.8205025751070878 + - 0.5716428292584782 + - - -0.8205025751070878 + - 0.5716428292584782 +- - - -0.8132471852237334 + - 0.5819183926683146 + - - -0.8132471852237334 + - 0.5819183926683146 +- - - -0.8058631169576695 + - 0.5921018803612005 + - - -0.8058631169576695 + - 0.5921018803612005 +- - - -0.7983515386744064 + - 0.6021916810254089 + - - -0.7983515386744064 + - 0.6021916810254089 +- - - -0.7907136389150943 + - 0.6121861981731129 + - - -0.7907136389150943 + - 0.6121861981731129 +- - - -0.7829506262084637 + - 0.6220838503929953 + - - -0.7829506262084637 + - 0.6220838503929953 +- - - -0.7750637288796017 + - 0.6318830716004721 + - - -0.7750637288796017 + - 0.6318830716004721 +- - - -0.7670541948555989 + - 0.6415823112854881 + - - -0.7670541948555989 + - 0.6415823112854881 +- - - -0.7589232914680891 + - 0.6511800347578556 + - - -0.7589232914680891 + - 0.6511800347578556 +- - - -0.7506723052527245 + - 0.6606747233900812 + - - -0.7506723052527245 + - 0.6606747233900812 +- - - -0.7423025417456096 + - 0.670064874857657 + - - -0.7423025417456096 + - 0.670064874857657 +- - - -0.7338153252767281 + - 0.6793490033767694 + - - -0.7338153252767281 + - 0.6793490033767694 +- - - -0.7252119987603977 + - 0.6885256399393918 + - - -0.7252119987603977 + - 0.6885256399393918 +- - - -0.7164939234827836 + - 0.6975933325457224 + - - -0.7164939234827836 + - 0.6975933325457224 +- - - -0.7076624788865049 + - 0.706550646433932 + - - -0.7076624788865049 + - 0.706550646433932 +- - - -0.698719062352368 + - 0.7153961643071813 + - - -0.698719062352368 + - 0.7153961643071813 +- - - -0.6896650889782625 + - 0.7241284865578796 + - - -0.6896650889782625 + - 0.7241284865578796 +- - - -0.6805019913552531 + - 0.7327462314891391 + - - -0.6805019913552531 + - 0.7327462314891391 +- - - -0.6712312193409035 + - 0.7412480355333995 + - - -0.6712312193409035 + - 0.7412480355333995 +- - - -0.6618542398298681 + - 0.7496325534681825 + - - -0.6618542398298681 + - 0.7496325534681825 +- - - -0.6523725365217912 + - 0.7578984586289408 + - - -0.6523725365217912 + - 0.7578984586289408 +- - - -0.6427876096865396 + - 0.7660444431189778 + - - -0.6427876096865396 + - 0.7660444431189778 +- - - -0.6331009759268216 + - 0.7740692180163904 + - - -0.6331009759268216 + - 0.7740692180163904 +- - - -0.623314167938217 + - 0.7819715135780128 + - - -0.623314167938217 + - 0.7819715135780128 +- - - -0.6134287342666622 + - 0.7897500794403256 + - - -0.6134287342666622 + - 0.7897500794403256 +- - - -0.6034462390634266 + - 0.7974036848172986 + - - -0.6034462390634266 + - 0.7974036848172986 +- - - -0.5933682618376209 + - 0.8049311186951345 + - - -0.5933682618376209 + - 0.8049311186951345 +- - - -0.5831963972062739 + - 0.8123311900238854 + - - -0.5831963972062739 + - 0.8123311900238854 +- - - -0.5729322546420206 + - 0.819602727905911 + - - -0.5729322546420206 + - 0.819602727905911 +- - - -0.5625774582184379 + - 0.826744581781146 + - - -0.5625774582184379 + - 0.826744581781146 +- - - -0.552133646353071 + - 0.8337556216091511 + - - -0.552133646353071 + - 0.8337556216091511 +- - - -0.541602471548191 + - 0.8406347380479176 + - - -0.541602471548191 + - 0.8406347380479176 +- - - -0.5309856001293205 + - 0.8473808426293961 + - - -0.5309856001293205 + - 0.8473808426293961 +- - - -0.5202847119815792 + - 0.8539928679317206 + - - -0.5202847119815792 + - 0.8539928679317206 +- - - -0.5095015002838734 + - 0.8604697677481075 + - - -0.5095015002838734 + - 0.8604697677481075 +- - - -0.4986376712409919 + - 0.8668105172523927 + - - -0.4986376712409919 + - 0.8668105172523927 +- - - -0.487694943813635 + - 0.8730141131611879 + - - -0.487694943813635 + - 0.8730141131611879 +- - - -0.47667504944642797 + - 0.8790795738926286 + - - -0.47667504944642797 + - 0.8790795738926286 +- - - -0.4655797317939577 + - 0.8850059397216871 + - - -0.4655797317939577 + - 0.8850059397216871 +- - - -0.45441074644487806 + - 0.890792272932028 + - - -0.45441074644487806 + - 0.890792272932028 +- - - -0.4431698606441268 + - 0.8964376579643814 + - - -0.4431698606441268 + - 0.8964376579643814 +- - - -0.4318588530132981 + - 0.9019412015614092 + - - -0.4318588530132981 + - 0.9019412015614092 +- - - -0.4204795132692152 + - 0.907302032909044 + - - -0.4204795132692152 + - 0.907302032909044 +- - - -0.4090336419407468 + - 0.9125193037742757 + - - -0.4090336419407468 + - 0.9125193037742757 +- - - -0.3975230500839139 + - 0.9175921886393661 + - - -0.3975230500839139 + - 0.9175921886393661 +- - - -0.38594955899532896 + - 0.9225198848324686 + - - -0.38594955899532896 + - 0.9225198848324686 +- - - -0.3743149999240192 + - 0.9273016126546322 + - - -0.3743149999240192 + - 0.9273016126546322 +- - - -0.3626212137816673 + - 0.9319366155031737 + - - -0.3626212137816673 + - 0.9319366155031737 +- - - -0.35087005085133094 + - 0.9364241599913922 + - - -0.35087005085133094 + - 0.9364241599913922 +- - - -0.3390633704946757 + - 0.9407635360646108 + - - -0.3390633704946757 + - 0.9407635360646108 +- - - -0.3272030408577722 + - 0.9449540571125281 + - - -0.3272030408577722 + - 0.9449540571125281 +- - - -0.3152909385755031 + - 0.9489950600778585 + - - -0.3152909385755031 + - 0.9489950600778585 +- - - -0.3033289484746273 + - 0.9528859055612465 + - - -0.3033289484746273 + - 0.9528859055612465 +- - - -0.29131896327554796 + - 0.9566259779224375 + - - -0.29131896327554796 + - 0.9566259779224375 +- - - -0.2792628832928309 + - 0.9602146853776892 + - - -0.2792628832928309 + - 0.9602146853776892 +- - - -0.26716261613452225 + - 0.9636514600934084 + - - -0.26716261613452225 + - 0.9636514600934084 +- - - -0.25502007640031144 + - 0.9669357582759981 + - - -0.25502007640031144 + - 0.9669357582759981 +- - - -0.24283718537858734 + - 0.9700670602579007 + - - -0.24283718537858734 + - 0.9700670602579007 +- - - -0.23061587074244044 + - 0.9730448705798238 + - - -0.23061587074244044 + - 0.9730448705798238 +- - - -0.21835806624464577 + - 0.975868718069136 + - - -0.21835806624464577 + - 0.975868718069136 +- - - -0.20606571141169297 + - 0.9785381559144195 + - - -0.20606571141169297 + - 0.9785381559144195 +- - - -0.19374075123689813 + - 0.981052761736168 + - - -0.19374075123689813 + - 0.981052761736168 +- - - -0.18138513587265162 + - 0.9834121376536186 + - - -0.18138513587265162 + - 0.9834121376536186 +- - - -0.16900082032184968 + - 0.9856159103477083 + - - -0.16900082032184968 + - 0.9856159103477083 +- - - -0.15658976412855838 + - 0.9876637311201432 + - - -0.15658976412855838 + - 0.9876637311201432 +- - - -0.14415393106795907 + - 0.9895552759485718 + - - -0.14415393106795907 + - 0.9895552759485718 +- - - -0.13169528883562445 + - 0.9912902455378553 + - - -0.13169528883562445 + - 0.9912902455378553 +- - - -0.11921580873617425 + - 0.9928683653674237 + - - -0.11921580873617425 + - 0.9928683653674237 +- - - -0.10671746537135988 + - 0.9942893857347128 + - - -0.10671746537135988 + - 0.9942893857347128 +- - - -0.0942022363276273 + - 0.9955530817946745 + - - -0.0942022363276273 + - 0.9955530817946745 +- - - -0.08167210186320688 + - 0.9966592535953529 + - - -0.08167210186320688 + - 0.9966592535953529 +- - - -0.06912904459478485 + - 0.9976077261095226 + - - -0.06912904459478485 + - 0.9976077261095226 +- - - -0.056575049183792726 + - 0.998398349262383 + - - -0.056575049183792726 + - 0.998398349262383 +- - - -0.04401210202238211 + - 0.9990309979553044 + - - -0.04401210202238211 + - 0.9990309979553044 +- - - -0.031442190919121114 + - 0.9995055720856215 + - - -0.031442190919121114 + - 0.9995055720856215 +- - - -0.018867304784467676 + - 0.9998219965624732 + - - -0.018867304784467676 + - 0.9998219965624732 +- - - -0.006289433316068405 + - 0.9999802213186832 + - - -0.006289433316068405 + - 0.9999802213186832 +- - - 0.006289433316067026 + - 0.9999802213186832 + - - 0.006289433316067026 + - 0.9999802213186832 +- - - 0.0188673047844663 + - 0.9998219965624732 + - - 0.0188673047844663 + - 0.9998219965624732 +- - - 0.03144219091911974 + - 0.9995055720856215 + - - 0.03144219091911974 + - 0.9995055720856215 +- - - 0.04401210202238073 + - 0.9990309979553045 + - - 0.04401210202238073 + - 0.9990309979553045 +- - - 0.056575049183791346 + - 0.9983983492623831 + - - 0.056575049183791346 + - 0.9983983492623831 +- - - 0.06912904459478347 + - 0.9976077261095226 + - - 0.06912904459478347 + - 0.9976077261095226 +- - - 0.08167210186320639 + - 0.9966592535953529 + - - 0.08167210186320639 + - 0.9966592535953529 +- - - 0.09420223632762592 + - 0.9955530817946746 + - - 0.09420223632762592 + - 0.9955530817946746 +- - - 0.10671746537135851 + - 0.994289385734713 + - - 0.10671746537135851 + - 0.994289385734713 +- - - 0.11921580873617288 + - 0.9928683653674238 + - - 0.11921580873617288 + - 0.9928683653674238 +- - - 0.13169528883562306 + - 0.9912902455378555 + - - 0.13169528883562306 + - 0.9912902455378555 +- - - 0.14415393106795768 + - 0.9895552759485721 + - - 0.14415393106795768 + - 0.9895552759485721 +- - - 0.15658976412855702 + - 0.9876637311201434 + - - 0.15658976412855702 + - 0.9876637311201434 +- - - 0.16900082032184832 + - 0.9856159103477086 + - - 0.16900082032184832 + - 0.9856159103477086 +- - - 0.18138513587265026 + - 0.9834121376536189 + - - 0.18138513587265026 + - 0.9834121376536189 +- - - 0.19374075123689677 + - 0.9810527617361683 + - - 0.19374075123689677 + - 0.9810527617361683 +- - - 0.2060657114116916 + - 0.9785381559144198 + - - 0.2060657114116916 + - 0.9785381559144198 +- - - 0.21835806624464443 + - 0.9758687180691363 + - - 0.21835806624464443 + - 0.9758687180691363 +- - - 0.2306158707424391 + - 0.9730448705798241 + - - 0.2306158707424391 + - 0.9730448705798241 +- - - 0.24283718537858687 + - 0.9700670602579009 + - - 0.24283718537858687 + - 0.9700670602579009 +- - - 0.2550200764003101 + - 0.9669357582759984 + - - 0.2550200764003101 + - 0.9669357582759984 +- - - 0.2671626161345209 + - 0.9636514600934087 + - - 0.2671626161345209 + - 0.9636514600934087 +- - - 0.2792628832928296 + - 0.9602146853776896 + - - 0.2792628832928296 + - 0.9602146853776896 +- - - 0.2913189632755466 + - 0.956625977922438 + - - 0.2913189632755466 + - 0.956625977922438 +- - - 0.30332894847462605 + - 0.952885905561247 + - - 0.30332894847462605 + - 0.952885905561247 +- - - 0.3152909385755018 + - 0.9489950600778589 + - - 0.3152909385755018 + - 0.9489950600778589 +- - - 0.3272030408577709 + - 0.9449540571125286 + - - 0.3272030408577709 + - 0.9449540571125286 +- - - 0.33906337049467444 + - 0.9407635360646113 + - - 0.33906337049467444 + - 0.9407635360646113 +- - - 0.3508700508513296 + - 0.9364241599913926 + - - 0.3508700508513296 + - 0.9364241599913926 +- - - 0.36262121378166595 + - 0.9319366155031743 + - - 0.36262121378166595 + - 0.9319366155031743 +- - - 0.3743149999240179 + - 0.9273016126546327 + - - 0.3743149999240179 + - 0.9273016126546327 +- - - 0.3859495589953277 + - 0.9225198848324692 + - - 0.3859495589953277 + - 0.9225198848324692 +- - - 0.39752305008391264 + - 0.9175921886393666 + - - 0.39752305008391264 + - 0.9175921886393666 +- - - 0.40903364194074554 + - 0.9125193037742763 + - - 0.40903364194074554 + - 0.9125193037742763 +- - - 0.4204795132692139 + - 0.9073020329090445 + - - 0.4204795132692139 + - 0.9073020329090445 +- - - 0.4318588530132969 + - 0.9019412015614098 + - - 0.4318588530132969 + - 0.9019412015614098 +- - - 0.44316986064412556 + - 0.896437657964382 + - - 0.44316986064412556 + - 0.896437657964382 +- - - 0.45441074644487683 + - 0.8907922729320287 + - - 0.45441074644487683 + - 0.8907922729320287 +- - - 0.46557973179395645 + - 0.8850059397216877 + - - 0.46557973179395645 + - 0.8850059397216877 +- - - 0.47667504944642675 + - 0.8790795738926293 + - - 0.47667504944642675 + - 0.8790795738926293 +- - - 0.48769494381363376 + - 0.8730141131611886 + - - 0.48769494381363376 + - 0.8730141131611886 +- - - 0.4986376712409907 + - 0.8668105172523933 + - - 0.4986376712409907 + - 0.8668105172523933 +- - - 0.5095015002838723 + - 0.8604697677481082 + - - 0.5095015002838723 + - 0.8604697677481082 +- - - 0.520284711981578 + - 0.8539928679317214 + - - 0.520284711981578 + - 0.8539928679317214 +- - - 0.5309856001293194 + - 0.8473808426293968 + - - 0.5309856001293194 + - 0.8473808426293968 +- - - 0.5416024715481897 + - 0.8406347380479183 + - - 0.5416024715481897 + - 0.8406347380479183 +- - - 0.5521336463530699 + - 0.8337556216091518 + - - 0.5521336463530699 + - 0.8337556216091518 +- - - 0.5625774582184366 + - 0.8267445817811466 + - - 0.5625774582184366 + - 0.8267445817811466 +- - - 0.5729322546420195 + - 0.8196027279059118 + - - 0.5729322546420195 + - 0.8196027279059118 +- - - 0.5831963972062728 + - 0.8123311900238863 + - - 0.5831963972062728 + - 0.8123311900238863 +- - - 0.5933682618376198 + - 0.8049311186951352 + - - 0.5933682618376198 + - 0.8049311186951352 +- - - 0.6034462390634255 + - 0.7974036848172994 + - - 0.6034462390634255 + - 0.7974036848172994 +- - - 0.6134287342666611 + - 0.7897500794403265 + - - 0.6134287342666611 + - 0.7897500794403265 +- - - 0.6233141679382159 + - 0.7819715135780135 + - - 0.6233141679382159 + - 0.7819715135780135 +- - - 0.6331009759268206 + - 0.7740692180163913 + - - 0.6331009759268206 + - 0.7740692180163913 +- - - 0.6427876096865385 + - 0.7660444431189787 + - - 0.6427876096865385 + - 0.7660444431189787 +- - - 0.6523725365217901 + - 0.7578984586289417 + - - 0.6523725365217901 + - 0.7578984586289417 +- - - 0.6618542398298678 + - 0.7496325534681827 + - - 0.6618542398298678 + - 0.7496325534681827 +- - - 0.6712312193409025 + - 0.7412480355334005 + - - 0.6712312193409025 + - 0.7412480355334005 +- - - 0.6805019913552521 + - 0.7327462314891401 + - - 0.6805019913552521 + - 0.7327462314891401 +- - - 0.6896650889782615 + - 0.7241284865578805 + - - 0.6896650889782615 + - 0.7241284865578805 +- - - 0.698719062352367 + - 0.7153961643071823 + - - 0.698719062352367 + - 0.7153961643071823 +- - - 0.7076624788865039 + - 0.7065506464339328 + - - 0.7076624788865039 + - 0.7065506464339328 +- - - 0.7164939234827827 + - 0.6975933325457234 + - - 0.7164939234827827 + - 0.6975933325457234 +- - - 0.7252119987603968 + - 0.6885256399393928 + - - 0.7252119987603968 + - 0.6885256399393928 +- - - 0.7338153252767271 + - 0.6793490033767704 + - - 0.7338153252767271 + - 0.6793490033767704 +- - - 0.7423025417456087 + - 0.670064874857658 + - - 0.7423025417456087 + - 0.670064874857658 +- - - 0.7506723052527237 + - 0.6606747233900823 + - - 0.7506723052527237 + - 0.6606747233900823 +- - - 0.7589232914680881 + - 0.6511800347578566 + - - 0.7589232914680881 + - 0.6511800347578566 +- - - 0.767054194855598 + - 0.6415823112854891 + - - 0.767054194855598 + - 0.6415823112854891 +- - - 0.7750637288796014 + - 0.6318830716004724 + - - 0.7750637288796014 + - 0.6318830716004724 +- - - 0.7829506262084629 + - 0.6220838503929964 + - - 0.7829506262084629 + - 0.6220838503929964 +- - - 0.7907136389150935 + - 0.612186198173114 + - - 0.7907136389150935 + - 0.612186198173114 +- - - 0.7983515386744056 + - 0.60219168102541 + - - 0.7983515386744056 + - 0.60219168102541 +- - - 0.8058631169576688 + - 0.5921018803612016 + - - 0.8058631169576688 + - 0.5921018803612016 +- - - 0.8132471852237325 + - 0.5819183926683157 + - - 0.8132471852237325 + - 0.5819183926683157 +- - - 0.820502575107087 + - 0.5716428292584793 + - - 0.820502575107087 + - 0.5716428292584793 +- - - 0.8276281386027308 + - 0.5612768160123658 + - - 0.8276281386027308 + - 0.5612768160123658 +- - - 0.8346227482478168 + - 0.5508219931223347 + - - 0.8346227482478168 + - 0.5508219931223347 +- - - 0.8414852973000496 + - 0.5402800148329078 + - - 0.8414852973000496 + - 0.5402800148329078 +- - - 0.8482146999128017 + - 0.5296525491790214 + - - 0.8482146999128017 + - 0.5296525491790214 +- - - 0.8548098913069254 + - 0.5189412777220967 + - - 0.8548098913069254 + - 0.5189412777220967 +- - - 0.8612698279392301 + - 0.5081478952839703 + - - 0.8612698279392301 + - 0.5081478952839703 +- - - 0.8675934876676011 + - 0.49727410967872443 + - - 0.8675934876676011 + - 0.49727410967872443 +- - - 0.8737798699127283 + - 0.48632164144246715 + - - 0.8737798699127283 + - 0.48632164144246715 +- - - 0.8798279958164291 + - 0.4752922235610904 + - - 0.8798279958164291 + - 0.4752922235610904 +- - - 0.8857369083965291 + - 0.4641876011960666 + - - 0.8857369083965291 + - 0.4641876011960666 +- - - 0.8915056726982836 + - 0.4530095314083147 + - - 0.8915056726982836 + - 0.4530095314083147 +- - - 0.8971333759423138 + - 0.4417597828801838 + - - 0.8971333759423138 + - 0.4417597828801838 +- - - 0.9026191276690336 + - 0.43044013563559885 + - - 0.9026191276690336 + - 0.43044013563559885 +- - - 0.9079620598795458 + - 0.4190523807584107 + - - 0.9079620598795458 + - 0.4190523807584107 +- - - 0.9131613271729829 + - 0.4075983201089971 + - - 0.9131613271729829 + - 0.4075983201089971 +- - - 0.9182161068802737 + - 0.39607976603915773 + - - 0.9182161068802737 + - 0.39607976603915773 +- - - 0.9231255991943119 + - 0.3844985411053501 + - - 0.9231255991943119 + - 0.3844985411053501 +- - - 0.9278890272965089 + - 0.37285647778030967 + - - 0.9278890272965089 + - 0.37285647778030967 +- - - 0.932505637479707 + - 0.36115541816310226 + - - 0.932505637479707 + - 0.36115541816310226 +- - - 0.9369746992674379 + - 0.3493972136876513 + - - 0.9369746992674379 + - 0.3493972136876513 +- - - 0.9412955055295031 + - 0.3375837248297927 + - - 0.9412955055295031 + - 0.3375837248297927 +- - - 0.9454673725938633 + - 0.32571682081289105 + - - 0.9454673725938633 + - 0.32571682081289105 +- - - 0.9494896403548132 + - 0.3137983793120792 + - - 0.9494896403548132 + - 0.3137983793120792 +- - - 0.9533616723774291 + - 0.3018302861571574 + - - 0.9533616723774291 + - 0.3018302861571574 +- - - 0.9570828559982706 + - 0.2898144350342019 + - - 0.9570828559982706 + - 0.2898144350342019 +- - - 0.9606526024223209 + - 0.27775272718593136 + - - 0.9606526024223209 + - 0.27775272718593136 +- - - 0.9640703468161504 + - 0.26564707111087715 + - - 0.9640703468161504 + - 0.26564707111087715 +- - - 0.96733554839729 + - 0.25349938226140567 + - - 0.96733554839729 + - 0.25349938226140567 +- - - 0.9704476905197967 + - 0.24131158274064027 + - - 0.9704476905197967 + - 0.24131158274064027 +- - - 0.9734062807560024 + - 0.22908560099833106 + - - 0.9734062807560024 + - 0.22908560099833106 +- - - 0.9762108509744293 + - 0.21682337152572034 + - - 0.9762108509744293 + - 0.21682337152572034 +- - - 0.9788609574138614 + - 0.20452683454945125 + - - 0.9788609574138614 + - 0.20452683454945125 +- - - 0.9813561807535595 + - 0.1921979357245733 + - - 0.9813561807535595 + - 0.1921979357245733 +- - - 0.98369612617961 + - 0.17983862582668034 + - - 0.98369612617961 + - 0.17983862582668034 +- - - 0.9858804234473957 + - 0.1674508604432468 + - - 0.9858804234473957 + - 0.1674508604432468 +- - - 0.987908726940178 + - 0.15503659966419847 + - - 0.987908726940178 + - 0.15503659966419847 +- - - 0.9897807157237833 + - 0.14259780777177156 + - - 0.9897807157237833 + - 0.14259780777177156 +- - - 0.9914960935973847 + - 0.13013645292970846 + - - 0.9914960935973847 + - 0.13013645292970846 +- - - 0.9930545891403676 + - 0.11765450687183943 + - - 0.9930545891403676 + - 0.11765450687183943 +- - - 0.9944559557552775 + - 0.1051539445900992 + - - 0.9944559557552775 + - 0.1051539445900992 +- - - 0.9956999717068375 + - 0.09263674402202833 + - - 0.9956999717068375 + - 0.09263674402202833 +- - - 0.9967864401570342 + - 0.08010488573780816 + - - 0.9967864401570342 + - 0.08010488573780816 +- - - 0.9977151891962615 + - 0.06756035262687954 + - - 0.9977151891962615 + - 0.06756035262687954 +- - - 0.9984860718705224 + - 0.05500512958419429 + - - 0.9984860718705224 + - 0.05500512958419429 +- - - 0.9990989662046814 + - 0.042441203196148705 + - - 0.9990989662046814 + - 0.042441203196148705 +- - - 0.9995537752217638 + - 0.029870561426253633 + - - 0.9995537752217638 + - 0.029870561426253633 +- - - 0.9998504269583004 + - 0.01729519330057795 + - - 0.9998504269583004 + - 0.01729519330057795 +- - - 0.999988874475714 + - 0.004717088593032691 + - - 0.999988874475714 + - 0.004717088593032691 +- - - 0.999969095867747 + - -0.007861762489467534 + - - 0.999969095867747 + - -0.007861762489467534 +- - - 0.9997910942639262 + - -0.020439369621910786 + - - 0.9997910942639262 + - -0.020439369621910786 +- - - 0.9994548978290694 + - -0.03301374267611272 + - - 0.9994548978290694 + - -0.03301374267611272 +- - - 0.9989605597588275 + - -0.045582892035610355 + - - 0.9989605597588275 + - -0.045582892035610355 +- - - 0.9983081582712683 + - -0.058144828910474865 + - - 0.9983081582712683 + - -0.058144828910474865 +- - - 0.9974977965944998 + - -0.07069756565199363 + - - 0.9974977965944998 + - -0.07069756565199363 +- - - 0.9965296029503368 + - -0.08323911606717167 + - - 0.9965296029503368 + - -0.08323911606717167 +- - - 0.9954037305340127 + - -0.09576749573300279 + - - 0.9954037305340127 + - -0.09576749573300279 +- - - 0.9941203574899394 + - -0.1082807223104606 + - - 0.9941203574899394 + - -0.1082807223104606 +- - - 0.9926796868835203 + - -0.12077681585816072 + - - 0.9926796868835203 + - -0.12077681585816072 +- - - 0.9910819466690197 + - -0.1332537991456392 + - - 0.9910819466690197 + - -0.1332537991456392 +- - - 0.9893273896534936 + - -0.14570969796621086 + - - 0.9893273896534936 + - -0.14570969796621086 +- - - 0.9874162934567892 + - -0.1581425414493393 + - - 0.9874162934567892 + - -0.1581425414493393 +- - - 0.9853489604676167 + - -0.17055036237248902 + - - 0.9853489604676167 + - -0.17055036237248902 +- - - 0.9831257177957046 + - -0.18293119747238504 + - - 0.9831257177957046 + - -0.18293119747238504 +- - - 0.9807469172200398 + - -0.1952830877556692 + - - 0.9807469172200398 + - -0.1952830877556692 +- - - 0.9782129351332084 + - -0.2076040788088552 + - - 0.9782129351332084 + - -0.2076040788088552 +- - - 0.9755241724818389 + - -0.2198922211075767 + - - 0.9755241724818389 + - -0.2198922211075767 +- - - 0.9726810547031601 + - -0.23214557032506142 + - - 0.9726810547031601 + - -0.23214557032506142 +- - - 0.9696840316576879 + - -0.24436218763976586 + - - 0.9696840316576879 + - -0.24436218763976586 +- - - 0.9665335775580415 + - -0.25654014004216474 + - - 0.9665335775580415 + - -0.25654014004216474 +- - - 0.9632301908939129 + - -0.2686775006405933 + - - 0.9632301908939129 + - -0.2686775006405933 +- - - 0.9597743943531892 + - -0.2807723489661489 + - - 0.9597743943531892 + - -0.2807723489661489 +- - - 0.9561667347392514 + - -0.29282277127654904 + - - 0.9561667347392514 + - -0.29282277127654904 +- - - 0.9524077828844516 + - -0.3048268608589526 + - - 0.9524077828844516 + - -0.3048268608589526 +- - - 0.9484981335597957 + - -0.3167827183316413 + - - 0.9484981335597957 + - -0.3167827183316413 +- - - 0.9444384053808291 + - -0.32868845194456814 + - - 0.9444384053808291 + - -0.32868845194456814 +- - - 0.9402292407097596 + - -0.340542177878672 + - - 0.9402292407097596 + - -0.340542177878672 +- - - 0.9358713055538124 + - -0.3523420205439635 + - - 0.9358713055538124 + - -0.3523420205439635 +- - - 0.9313652894598542 + - -0.36408611287628373 + - - 0.9313652894598542 + - -0.36408611287628373 +- - - 0.9267119054052854 + - -0.37577259663273127 + - - 0.9267119054052854 + - -0.37577259663273127 +- - - 0.9219118896852252 + - -0.38739962268569283 + - - 0.9219118896852252 + - -0.38739962268569283 +- - - 0.9169660017960138 + - -0.3989653513154153 + - - 0.9169660017960138 + - -0.3989653513154153 +- - - 0.9118750243150339 + - -0.4104679525011135 + - - 0.9118750243150339 + - -0.4104679525011135 +- - - 0.9066397627768898 + - -0.4219056062105182 + - - 0.9066397627768898 + - -0.4219056062105182 +- - - 0.901261045545945 + - -0.4332765026878681 + - - 0.901261045545945 + - -0.4332765026878681 +- - - 0.895739723685256 + - -0.44457884274025133 + - - 0.895739723685256 + - -0.44457884274025133 +- - - 0.8900766708219062 + - -0.45581083802230066 + - - 0.8900766708219062 + - -0.45581083802230066 +- - - 0.8842727830087785 + - -0.46697071131914664 + - - 0.8842727830087785 + - -0.46697071131914664 +- - - 0.878328978582769 + - -0.47805669682763535 + - - 0.878328978582769 + - -0.47805669682763535 +- - - 0.8722461980194871 + - -0.48906704043571536 + - - 0.8722461980194871 + - -0.48906704043571536 +- - - 0.8660254037844392 + - -0.4999999999999992 + - - 0.8660254037844392 + - -0.4999999999999992 +- - - 0.8596675801807453 + - -0.5108538456214086 + - - 0.8596675801807453 + - -0.5108538456214086 +- - - 0.8531737331933934 + - -0.5216268599188969 + - - 0.8531737331933934 + - -0.5216268599188969 +- - - 0.8465448903300608 + - -0.5323173383011919 + - - 0.8465448903300608 + - -0.5323173383011919 +- - - 0.8397821004585404 + - -0.5429235892364983 + - - 0.8397821004585404 + - -0.5429235892364983 +- - - 0.8328864336407736 + - -0.5534439345201582 + - - 0.8328864336407736 + - -0.5534439345201582 +- - - 0.8258589809635439 + - -0.5638767095401768 + - - 0.8258589809635439 + - -0.5638767095401768 +- - - 0.8187008543658284 + - -0.5742202635406232 + - - 0.8187008543658284 + - -0.5742202635406232 +- - - 0.8114131864628666 + - -0.5844729598828138 + - - 0.8114131864628666 + - -0.5844729598828138 +- - - 0.803997130366941 + - -0.5946331763042861 + - - 0.803997130366941 + - -0.5946331763042861 +- - - 0.7964538595049301 + - -0.6046993051754741 + - - 0.7964538595049301 + - -0.6046993051754741 +- - - 0.7887845674326319 + - -0.6146697537540917 + - - 0.7887845674326319 + - -0.6146697537540917 +- - - 0.7809904676459185 + - -0.6245429444371375 + - - 0.7809904676459185 + - -0.6245429444371375 +- - - 0.7730727933887184 + - -0.6343173150105269 + - - 0.7730727933887184 + - -0.6343173150105269 +- - - 0.76503279745789 + - -0.6439913188962683 + - - 0.76503279745789 + - -0.6439913188962683 +- - - 0.7568717520049925 + - -0.6535634253971785 + - - 0.7568717520049925 + - -0.6535634253971785 +- - - 0.7485909483349908 + - -0.6630321199390865 + - - 0.7485909483349908 + - -0.6630321199390865 +- - - 0.7401916967019444 + - -0.6723959043104716 + - - 0.7401916967019444 + - -0.6723959043104716 +- - - 0.7316753261016786 + - -0.6816532968995326 + - - 0.7316753261016786 + - -0.6816532968995326 +- - - 0.7230431840615102 + - -0.69080283292861 + - - 0.7230431840615102 + - -0.69080283292861 +- - - 0.7142966364270213 + - -0.6998430646859649 + - - 0.7142966364270213 + - -0.6998430646859649 +- - - 0.7054370671459542 + - -0.7087725617548373 + - - 0.7054370671459542 + - -0.7087725617548373 +- - - 0.6964658780492222 + - -0.7175899112397874 + - - 0.6964658780492222 + - -0.7175899112397874 +- - - 0.6873844886291115 + - -0.7262937179902459 + - - 0.6873844886291115 + - -0.7262937179902459 +- - - 0.678194335814667 + - -0.7348826048212753 + - - 0.678194335814667 + - -0.7348826048212753 +- - - 0.6688968737443408 + - -0.7433552127314689 + - - 0.6688968737443408 + - -0.7433552127314689 +- - - 0.6594935735358967 + - -0.7517102011179926 + - - 0.6594935735358967 + - -0.7517102011179926 +- - - 0.6499859230536468 + - -0.7599462479886974 + - - 0.6499859230536468 + - -0.7599462479886974 +- - - 0.6403754266730268 + - -0.7680620501712988 + - - 0.6403754266730268 + - -0.7680620501712988 +- - - 0.6306636050425575 + - -0.7760563235195788 + - - 0.6306636050425575 + - -0.7760563235195788 +- - - 0.6208519948432446 + - -0.7839278031165648 + - - 0.6208519948432446 + - -0.7839278031165648 +- - - 0.6109421485454233 + - -0.7916752434746851 + - - 0.6109421485454233 + - -0.7916752434746851 +- - - 0.600935634163124 + - -0.7992974187328293 + - - 0.600935634163124 + - -0.7992974187328293 +- - - 0.5908340350059585 + - -0.8067931228503239 + - - 0.5908340350059585 + - -0.8067931228503239 +- - - 0.5806389494286068 + - -0.8141611697977519 + - - 0.5806389494286068 + - -0.8141611697977519 +- - - 0.570351990577902 + - -0.8214003937446248 + - - 0.570351990577902 + - -0.8214003937446248 +- - - 0.5599747861375968 + - -0.8285096492438412 + - - 0.5599747861375968 + - -0.8285096492438412 +- - - 0.5495089780708068 + - -0.8354878114129359 + - - 0.5495089780708068 + - -0.8354878114129359 +- - - 0.5389562223602165 + - -0.8423337761120617 + - - 0.5389562223602165 + - -0.8423337761120617 +- - - 0.5283181887460523 + - -0.8490464601186973 + - - 0.5283181887460523 + - -0.8490464601186973 +- - - 0.5175965604618786 + - -0.8556248012990465 + - - 0.5175965604618786 + - -0.8556248012990465 +- - - 0.5067930339682736 + - -0.8620677587760909 + - - 0.5067930339682736 + - -0.8620677587760909 +- - - 0.49590931868438975 + - -0.8683743130942925 + - - 0.49590931868438975 + - -0.8683743130942925 +- - - 0.4849471367174889 + - -0.8745434663808935 + - - 0.4849471367174889 + - -0.8745434663808935 +- - - 0.4739082225904436 + - -0.8805742425038144 + - - 0.4739082225904436 + - -0.8805742425038144 +- - - 0.4627943229673003 + - -0.886465687226098 + - - 0.4627943229673003 + - -0.886465687226098 +- - - 0.4516071963768956 + - -0.8922168683569035 + - - 0.4516071963768956 + - -0.8922168683569035 +- - - 0.44034861293462074 + - -0.8978268758989985 + - - 0.44034861293462074 + - -0.8978268758989985 +- - - 0.42902035406232714 + - -0.903294822192752 + - - 0.42902035406232714 + - -0.903294822192752 +- - - 0.4176242122064685 + - -0.9086198420565812 + - - 0.4176242122064685 + - -0.9086198420565812 +- - - 0.4061619905544733 + - -0.9138010929238529 + - - 0.4061619905544733 + - -0.9138010929238529 +- - - 0.3946355027494409 + - -0.918837754976196 + - - 0.3946355027494409 + - -0.918837754976196 +- - - 0.38304657260316866 + - -0.9237290312732221 + - - 0.38304657260316866 + - -0.9237290312732221 +- - - 0.37139703380756833 + - -0.9284741478786256 + - - 0.37139703380756833 + - -0.9284741478786256 +- - - 0.3596887296445368 + - -0.9330723539826369 + - - 0.3596887296445368 + - -0.9330723539826369 +- - - 0.34792351269428423 + - -0.9375229220208273 + - - 0.34792351269428423 + - -0.9375229220208273 +- - - 0.3361032445422173 + - -0.9418251477892244 + - - 0.3361032445422173 + - -0.9418251477892244 +- - - 0.3242297954843714 + - -0.9459783505557422 + - - 0.3242297954843714 + - -0.9459783505557422 +- - - 0.31230504423149086 + - -0.9499818731678866 + - - 0.31230504423149086 + - -0.9499818731678866 +- - - 0.3003308776117511 + - -0.9538350821567402 + - - 0.3003308776117511 + - -0.9538350821567402 +- - - 0.28830919027222335 + - -0.9575373678371905 + - - 0.28830919027222335 + - -0.9575373678371905 +- - - 0.27624188437907515 + - -0.9610881444044025 + - - 0.27624188437907515 + - -0.9610881444044025 +- - - 0.264130869316608 + - -0.9644868500265066 + - - 0.264130869316608 + - -0.9644868500265066 +- - - 0.2519780613851261 + - -0.9677329469334987 + - - 0.2519780613851261 + - -0.9677329469334987 +- - - 0.2397853834977361 + - -0.9708259215023276 + - - 0.2397853834977361 + - -0.9708259215023276 +- - - 0.22755476487608342 + - -0.9737652843381666 + - - 0.22755476487608342 + - -0.9737652843381666 +- - - 0.2152881407450906 + - -0.9765505703518492 + - - 0.2152881407450906 + - -0.9765505703518492 +- - - 0.20298745202676252 + - -0.9791813388334577 + - - 0.20298745202676252 + - -0.9791813388334577 +- - - 0.19065464503306495 + - -0.9816571735220581 + - - 0.19065464503306495 + - -0.9816571735220581 +- - - 0.17829167115797728 + - -0.9839776826715613 + - - 0.17829167115797728 + - -0.9839776826715613 +- - - 0.1659004865687139 + - -0.9861424991127113 + - - 0.1659004865687139 + - -0.9861424991127113 +- - - 0.15348305189621775 + - -0.9881512803111794 + - - 0.15348305189621775 + - -0.9881512803111794 +- - - 0.14104133192492 + - -0.9900037084217637 + - - 0.14104133192492 + - -0.9900037084217637 +- - - 0.12857729528187029 + - -0.9916994903386805 + - - 0.12857729528187029 + - -0.9916994903386805 +- - - 0.11609291412523105 + - -0.9932383577419429 + - - 0.11609291412523105 + - -0.9932383577419429 +- - - 0.10359016383224108 + - -0.9946200671398147 + - - 0.10359016383224108 + - -0.9946200671398147 +- - - 0.09107102268664179 + - -0.9958443999073395 + - - 0.09107102268664179 + - -0.9958443999073395 +- - - 0.07853747156566976 + - -0.996911162320932 + - - 0.07853747156566976 + - -0.996911162320932 +- - - 0.0659914936266216 + - -0.9978201855890306 + - - 0.0659914936266216 + - -0.9978201855890306 +- - - 0.05343507399305771 + - -0.9985713258788059 + - - 0.05343507399305771 + - -0.9985713258788059 +- - - 0.04087019944071283 + - -0.9991644643389177 + - - 0.04087019944071283 + - -0.9991644643389177 +- - - 0.028298858083118522 + - -0.9995995071183216 + - - 0.028298858083118522 + - -0.9995995071183216 +- - - 0.01572303905704239 + - -0.9998763853811183 + - - 0.01572303905704239 + - -0.9998763853811183 +- - - 0.003144732207736932 + - -0.9999950553174458 + - - 0.003144732207736932 + - -0.9999950553174458 +- - - -0.009434072225895224 + - -0.999955498150411 + - - -0.009434072225895224 + - -0.999955498150411 +- - - -0.02201138392622685 + - -0.9997577201390606 + - - -0.02201138392622685 + - -0.9997577201390606 +- - - -0.03458521281181564 + - -0.9994017525773914 + - - -0.03458521281181564 + - -0.9994017525773914 +- - - -0.04715356935230482 + - -0.9988876517893979 + - - -0.04715356935230482 + - -0.9988876517893979 +- - - -0.05971446488320808 + - -0.9982154991201609 + - - -0.05971446488320808 + - -0.9982154991201609 +- - - -0.07226591192058601 + - -0.9973854009229762 + - - -0.07226591192058601 + - -0.9973854009229762 +- - - -0.08480592447550901 + - -0.9963974885425265 + - - -0.08480592447550901 + - -0.9963974885425265 +- - - -0.0973325183683015 + - -0.9952519182940992 + - - -0.0973325183683015 + - -0.9952519182940992 +- - - -0.1098437115424997 + - -0.9939488714388522 + - - -0.1098437115424997 + - -0.9939488714388522 +- - - -0.12233752437845594 + - -0.9924885541551351 + - - -0.12233752437845594 + - -0.9924885541551351 +- - - -0.13481198000658376 + - -0.9908711975058637 + - - -0.13481198000658376 + - -0.9908711975058637 +- - - -0.14726510462013975 + - -0.9890970574019616 + - - -0.14726510462013975 + - -0.9890970574019616 +- - - -0.15969492778754882 + - -0.9871664145618658 + - - -0.15969492778754882 + - -0.9871664145618658 +- - - -0.17209948276416748 + - -0.9850795744671118 + - - -0.17209948276416748 + - -0.9850795744671118 +- - - -0.18447680680349163 + - -0.9828368673139949 + - - -0.18447680680349163 + - -0.9828368673139949 +- - - -0.19682494146770374 + - -0.9804386479613271 + - - -0.19682494146770374 + - -0.9804386479613271 +- - - -0.2091419329375665 + - -0.9778852958742853 + - - -0.2091419329375665 + - -0.9778852958742853 +- - - -0.22142583232155733 + - -0.9751772150643726 + - - -0.22142583232155733 + - -0.9751772150643726 +- - - -0.23367469596425144 + - -0.9723148340254892 + - - -0.23367469596425144 + - -0.9723148340254892 +- - - -0.24588658575385006 + - -0.9692986056661356 + - - -0.24588658575385006 + - -0.9692986056661356 +- - - -0.2580595694288491 + - -0.9661290072377483 + - - -0.2580595694288491 + - -0.9661290072377483 +- - - -0.2701917208837818 + - -0.9628065402591844 + - - -0.2701917208837818 + - -0.9628065402591844 +- - - -0.2822811204739704 + - -0.9593317304373705 + - - -0.2822811204739704 + - -0.9593317304373705 +- - - -0.29432585531928135 + - -0.9557051275841171 + - - -0.29432585531928135 + - -0.9557051275841171 +- - - -0.30632401960678207 + - -0.951927305529127 + - - -0.30632401960678207 + - -0.951927305529127 +- - - -0.31827371489230794 + - -0.9479988620291956 + - - -0.31827371489230794 + - -0.9479988620291956 +- - - -0.3301730504008353 + - -0.9439204186736335 + - - -0.3301730504008353 + - -0.9439204186736335 +- - - -0.342020143325668 + - -0.9396926207859086 + - - -0.342020143325668 + - -0.9396926207859086 +- - - -0.35381311912633706 + - -0.9353161373215435 + - - -0.35381311912633706 + - -0.9353161373215435 +- - - -0.3655501118252182 + - -0.9307916607622624 + - - -0.3655501118252182 + - -0.9307916607622624 +- - - -0.37722926430276815 + - -0.9261199070064267 + - - -0.37722926430276815 + - -0.9261199070064267 +- - - -0.3888487285913865 + - -0.9213016152557545 + - - -0.3888487285913865 + - -0.9213016152557545 +- - - -0.4004066661678036 + - -0.9163375478983632 + - - -0.4004066661678036 + - -0.9163375478983632 +- - - -0.4119012482439916 + - -0.9112284903881362 + - - -0.4119012482439916 + - -0.9112284903881362 +- - - -0.4233306560565341 + - -0.9059752511204401 + - - -0.4233306560565341 + - -0.9059752511204401 +- - - -0.4346930811543944 + - -0.9005786613042189 + - - -0.4346930811543944 + - -0.9005786613042189 +- - - -0.4459867256850755 + - -0.8950395748304681 + - - -0.4459867256850755 + - -0.8950395748304681 +- - - -0.4572098026790778 + - -0.8893588681371309 + - - -0.4572098026790778 + - -0.8893588681371309 +- - - -0.46836053633265995 + - -0.8835374400704156 + - - -0.46836053633265995 + - -0.8835374400704156 +- - - -0.47943716228880834 + - -0.8775762117425784 + - - -0.47943716228880834 + - -0.8775762117425784 +- - - -0.4904379279164198 + - -0.8714761263861728 + - - -0.4904379279164198 + - -0.8714761263861728 +- - - -0.5013610925876044 + - -0.8652381492048091 + - - -0.5013610925876044 + - -0.8652381492048091 +- - - -0.5122049279531135 + - -0.8588632672204265 + - - -0.5122049279531135 + - -0.8588632672204265 +- - - -0.5229677182158008 + - -0.852352489117125 + - - -0.5229677182158008 + - -0.852352489117125 +- - - -0.5336477604021214 + - -0.8457068450815567 + - - -0.5336477604021214 + - -0.8457068450815567 +- - - -0.5442433646315787 + - -0.8389273866399275 + - - -0.5442433646315787 + - -0.8389273866399275 +- - - -0.5547528543841161 + - -0.8320151864916143 + - - -0.5547528543841161 + - -0.8320151864916143 +- - - -0.5651745667653925 + - -0.8249713383394304 + - - -0.5651745667653925 + - -0.8249713383394304 +- - - -0.5755068527698889 + - -0.8177969567165786 + - - -0.5755068527698889 + - -0.8177969567165786 +- - - -0.5857480775418389 + - -0.8104931768102923 + - - -0.5857480775418389 + - -0.8104931768102923 +- - - -0.5958966206338965 + - -0.8030611542822266 + - - -0.5958966206338965 + - -0.8030611542822266 +- - - -0.6059508762635476 + - -0.7955020650855904 + - - -0.6059508762635476 + - -0.7955020650855904 +- - - -0.6159092535671783 + - -0.7878171052790878 + - - -0.6159092535671783 + - -0.7878171052790878 +- - - -0.6257701768518052 + - -0.7800074908376589 + - - -0.6257701768518052 + - -0.7800074908376589 +- - - -0.6355320858443827 + - -0.7720744574600873 + - - -0.6355320858443827 + - -0.7720744574600873 +- - - -0.6451934359386927 + - -0.76401926037347 + - - -0.6451934359386927 + - -0.76401926037347 +- - - -0.6547526984397336 + - -0.7558431741346133 + - - -0.6547526984397336 + - -0.7558431741346133 +- - - -0.6642083608056132 + - -0.7475474924283543 + - - -0.6642083608056132 + - -0.7475474924283543 +- - - -0.6735589268868657 + - -0.7391335278628713 + - - -0.6735589268868657 + - -0.7391335278628713 +- - - -0.6828029171631881 + - -0.7306026117619896 + - - -0.6828029171631881 + - -0.7306026117619896 +- - - -0.6919388689775459 + - -0.7219560939545248 + - - -0.6919388689775459 + - -0.7219560939545248 +- - - -0.7009653367675964 + - -0.7131953425607112 + - - -0.7009653367675964 + - -0.7131953425607112 +- - - -0.7098808922944282 + - -0.7043217437757168 + - - -0.7098808922944282 + - -0.7043217437757168 +- - - -0.7186841248685372 + - -0.695336701650319 + - - -0.7186841248685372 + - -0.695336701650319 +- - - -0.7273736415730482 + - -0.6862416378687342 + - - -0.7273736415730482 + - -0.6862416378687342 +- - - -0.7359480674841022 + - -0.6770379915236775 + - - -0.7359480674841022 + - -0.6770379915236775 +- - - -0.7444060458884184 + - -0.6677272188886492 + - - -0.7444060458884184 + - -0.6677272188886492 +- - - -0.7527462384979536 + - -0.6583107931875202 + - - -0.7527462384979536 + - -0.6583107931875202 +- - - -0.7609673256616669 + - -0.648790204361418 + - - -0.7609673256616669 + - -0.648790204361418 +- - - -0.7690680065743155 + - -0.6391669588329865 + - - -0.7690680065743155 + - -0.6391669588329865 +- - - -0.7770469994822877 + - -0.6294425792680167 + - - -0.7770469994822877 + - -0.6294425792680167 +- - - -0.7849030418864043 + - -0.619618604334529 + - - -0.7849030418864043 + - -0.619618604334529 +- - - -0.7926348907416839 + - -0.609696588459308 + - - -0.7926348907416839 + - -0.609696588459308 +- - - -0.8002413226540318 + - -0.5996781015819452 + - - -0.8002413226540318 + - -0.5996781015819452 +- - - -0.807721134073806 + - -0.5895647289064406 + - - -0.807721134073806 + - -0.5895647289064406 +- - - -0.8150731414862619 + - -0.5793580706503675 + - - -0.8150731414862619 + - -0.5793580706503675 +- - - -0.8222961815988086 + - -0.5690597417916851 + - - -0.8222961815988086 + - -0.5690597417916851 +- - - -0.8293891115250823 + - -0.5586713718131927 + - - -0.8293891115250823 + - -0.5586713718131927 +- - - -0.8363508089657752 + - -0.5481946044447112 + - - -0.8363508089657752 + - -0.5481946044447112 +- - - -0.8431801723862219 + - -0.537631097402988 + - - -0.8431801723862219 + - -0.537631097402988 +- - - -0.8498761211906855 + - -0.5269825221294112 + - - -0.8498761211906855 + - -0.5269825221294112 +- - - -0.8564375958933453 + - -0.5162505635255297 + - - -0.8564375958933453 + - -0.5162505635255297 +- - - -0.8628635582859301 + - -0.5054369196864662 + - - -0.8628635582859301 + - -0.5054369196864662 +- - - -0.8691529916019983 + - -0.49454330163221977 + - - -0.8691529916019983 + - -0.49454330163221977 +- - - -0.8753049006778127 + - -0.4835714330369447 + - - -0.8753049006778127 + - -0.4835714330369447 +- - - -0.8813183121098064 + - -0.4725230499562131 + - - -0.8813183121098064 + - -0.4725230499562131 +- - - -0.8871922744086038 + - -0.46139990055231767 + - - -0.8871922744086038 + - -0.46139990055231767 +- - - -0.8929258581495678 + - -0.4502037448176746 + - - -0.8929258581495678 + - -0.4502037448176746 +- - - -0.898518156119867 + - -0.43893635429633115 + - - -0.898518156119867 + - -0.43893635429633115 +- - - -0.9039682834620154 + - -0.42759951180367056 + - - -0.9039682834620154 + - -0.42759951180367056 +- - - -0.9092753778138881 + - -0.4161950111443084 + - - -0.9092753778138881 + - -0.4161950111443084 +- - - -0.914438599445165 + - -0.40472465682827513 + - - -0.914438599445165 + - -0.40472465682827513 +- - - -0.919457131390205 + - -0.39319026378547983 + - - -0.919457131390205 + - -0.39319026378547983 +- - - -0.9243301795773077 + - -0.38159365707855025 + - - -0.9243301795773077 + - -0.38159365707855025 +- - - -0.9290569729543624 + - -0.36993667161404425 + - - -0.9290569729543624 + - -0.36993667161404425 +- - - -0.9336367636108461 + - -0.3582211518521277 + - - -0.9336367636108461 + - -0.3582211518521277 +- - - -0.9380688268961654 + - -0.34644895151472466 + - - -0.9380688268961654 + - -0.34644895151472466 +- - - -0.9423524615343185 + - -0.3346219332922018 + - - -0.9423524615343185 + - -0.3346219332922018 +- - - -0.946486989734852 + - -0.32274196854865056 + - - -0.946486989734852 + - -0.32274196854865056 +- - - -0.9504717573001114 + - -0.31081093702577167 + - - -0.9504717573001114 + - -0.31081093702577167 +- - - -0.9543061337287484 + - -0.2988307265454612 + - - -0.9543061337287484 + - -0.2988307265454612 +- - - -0.9579895123154887 + - -0.2868032327110909 + - - -0.9579895123154887 + - -0.2868032327110909 +- - - -0.9615213102471251 + - -0.27473035860758444 + - - -0.9615213102471251 + - -0.27473035860758444 +- - - -0.9649009686947388 + - -0.2626140145002827 + - - -0.9649009686947388 + - -0.2626140145002827 +- - - -0.9681279529021183 + - -0.25045611753270025 + - - -0.9681279529021183 + - -0.25045611753270025 +- - - -0.9712017522703761 + - -0.23825859142316594 + - - -0.9712017522703761 + - -0.23825859142316594 +- - - -0.9741218804387358 + - -0.22602336616045093 + - - -0.9741218804387358 + - -0.22602336616045093 +- - - -0.9768878753614922 + - -0.21375237769837674 + - - -0.9768878753614922 + - -0.21375237769837674 +- - - -0.9794992993811164 + - -0.2014475676495055 + - - -0.9794992993811164 + - -0.2014475676495055 +- - - -0.9819557392975065 + - -0.18911088297791753 + - - -0.9819557392975065 + - -0.18911088297791753 +- - - -0.9842568064333685 + - -0.17674427569114207 + - - -0.9842568064333685 + - -0.17674427569114207 +- - - -0.9864021366957143 + - -0.1643497025313075 + - - -0.9864021366957143 + - -0.1643497025313075 +- - - -0.9883913906334727 + - -0.1519291246655162 + - - -0.9883913906334727 + - -0.1519291246655162 +- - - -0.9902242534911982 + - -0.1394845073755471 + - - -0.9902242534911982 + - -0.1394845073755471 +- - - -0.9919004352588768 + - -0.12701781974687945 + - - -0.9919004352588768 + - -0.12701781974687945 +- - - -0.9934196707178105 + - -0.11453103435714257 + - - -0.9934196707178105 + - -0.11453103435714257 +- - - -0.9947817194825852 + - -0.10202612696398496 + - - -0.9947817194825852 + - -0.10202612696398496 +- - - -0.9959863660391042 + - -0.08950507619246842 + - - -0.9959863660391042 + - -0.08950507619246842 +- - - -0.9970334197786901 + - -0.07696986322198038 + - - -0.9970334197786901 + - -0.07696986322198038 +- - - -0.9979227150282431 + - -0.0644224714727701 + - - -0.9979227150282431 + - -0.0644224714727701 +- - - -0.9986541110764564 + - -0.051864886292102175 + - - -0.9986541110764564 + - -0.051864886292102175 +- - - -0.9992274921960794 + - -0.03929909464013164 + - - -0.9992274921960794 + - -0.03929909464013164 +- - - -0.9996427676622299 + - -0.026727084775506123 + - - -0.9996427676622299 + - -0.026727084775506123 +- - - -0.9998998717667489 + - -0.014150845940762564 + - - -0.9998998717667489 + - -0.014150845940762564 +- - - -0.9999987638285974 + - -0.001572368047586014 + - - -0.9999987638285974 + - -0.001572368047586014 +- - - -0.9999394282002937 + - 0.0110063586380641 + - - -0.9999394282002937 + - 0.0110063586380641 +- - - -0.9997218742703887 + - 0.02358334381085534 + - - -0.9997218742703887 + - 0.02358334381085534 +- - - -0.9993461364619809 + - 0.036156597441018276 + - - -0.9993461364619809 + - 0.036156597441018276 +- - - -0.9988122742272693 + - 0.04872413008921046 + - - -0.9988122742272693 + - 0.04872413008921046 +- - - -0.9981203720381463 + - 0.06128395322131545 + - - -0.9981203720381463 + - 0.06128395322131545 +- - - -0.9972705393728328 + - 0.0738340795230701 + - - -0.9972705393728328 + - 0.0738340795230701 +- - - -0.9962629106985544 + - 0.08637252321452737 + - - -0.9962629106985544 + - 0.08637252321452737 +- - - -0.9950976454502662 + - 0.09889730036424782 + - - -0.9950976454502662 + - 0.09889730036424782 +- - - -0.9937749280054243 + - 0.11140642920322712 + - - -0.9937749280054243 + - 0.11140642920322712 +- - - -0.9922949676548137 + - 0.12389793043845473 + - - -0.9922949676548137 + - 0.12389793043845473 +- - - -0.9906579985694319 + - 0.1363698275660986 + - - -0.9906579985694319 + - 0.1363698275660986 +- - - -0.9888642797634358 + - 0.14882014718424852 + - - -0.9888642797634358 + - 0.14882014718424852 +- - - -0.9869140950531602 + - 0.16124691930515087 + - - -0.9869140950531602 + - 0.16124691930515087 +- - - -0.9848077530122081 + - 0.17364817766692972 + - - -0.9848077530122081 + - 0.17364817766692972 +- - - -0.9825455869226281 + - 0.18602196004469043 + - - -0.9825455869226281 + - 0.18602196004469043 +- - - -0.9801279547221767 + - 0.19836630856101212 + - - -0.9801279547221767 + - 0.19836630856101212 +- - - -0.9775552389476866 + - 0.21067926999572462 + - - -0.9775552389476866 + - 0.21067926999572462 +- - - -0.9748278466745344 + - 0.2229588960949763 + - - -0.9748278466745344 + - 0.2229588960949763 +- - - -0.9719462094522341 + - 0.23520324387948816 + - - -0.9719462094522341 + - 0.23520324387948816 +- - - -0.9689107832361499 + - 0.24741037595200138 + - - -0.9689107832361499 + - 0.24741037595200138 +- - - -0.9657220483153551 + - 0.25957836080381363 + - - -0.9657220483153551 + - 0.25957836080381363 +- - - -0.9623805092366339 + - 0.27170527312041143 + - - -0.9623805092366339 + - 0.27170527312041143 +- - - -0.9588866947246498 + - 0.2837891940860965 + - - -0.9588866947246498 + - 0.2837891940860965 +- - - -0.9552411575982872 + - 0.29582821168760115 + - - -0.9552411575982872 + - 0.29582821168760115 +- - - -0.9514444746831768 + - 0.30782042101662727 + - - -0.9514444746831768 + - 0.30782042101662727 +- - - -0.9474972467204302 + - 0.31976392457124386 + - - -0.9474972467204302 + - 0.31976392457124386 +- - - -0.9434000982715814 + - 0.3316568325561384 + - - -0.9434000982715814 + - 0.3316568325561384 +- - - -0.9391536776197683 + - 0.3434972631816217 + - - -0.9391536776197683 + - 0.3434972631816217 +- - - -0.9347586566671513 + - 0.35528334296139286 + - - -0.9347586566671513 + - 0.35528334296139286 +- - - -0.9302157308286049 + - 0.3670132070089637 + - - -0.9302157308286049 + - 0.3670132070089637 +- - - -0.9255256189216783 + - 0.3786849993327492 + - - -0.9255256189216783 + - 0.3786849993327492 +- - - -0.9206890630528639 + - 0.3902968731297237 + - - -0.9206890630528639 + - 0.3902968731297237 +- - - -0.9157068285001696 + - 0.40184699107765015 + - - -0.9157068285001696 + - 0.40184699107765015 +- - - -0.9105797035920364 + - 0.41333352562578207 + - - -0.9105797035920364 + - 0.41333352562578207 +- - - -0.9053084995825972 + - 0.4247546592840467 + - - -0.9053084995825972 + - 0.4247546592840467 +- - - -0.8998940505233184 + - 0.4361085849106107 + - - -0.8998940505233184 + - 0.4361085849106107 +- - - -0.8943372131310279 + - 0.4473935059978257 + - - -0.8943372131310279 + - 0.4473935059978257 +- - - -0.8886388666523561 + - 0.45860763695649037 + - - -0.8886388666523561 + - 0.45860763695649037 +- - - -0.8827999127246203 + - 0.4697492033983695 + - - -0.8827999127246203 + - 0.4697492033983695 +- - - -0.8768212752331539 + - 0.48081644241696414 + - - -0.8768212752331539 + - 0.48081644241696414 +- - - -0.8707039001651283 + - 0.49180760286644026 + - - -0.8707039001651283 + - 0.49180760286644026 +- - - -0.8644487554598653 + - 0.502720945638721 + - - -0.8644487554598653 + - 0.502720945638721 +- - - -0.8580568308556884 + - 0.5135547439386501 + - - -0.8580568308556884 + - 0.5135547439386501 +- - - -0.8515291377333118 + - 0.5243072835572309 + - - -0.8515291377333118 + - 0.5243072835572309 +- - - -0.8448667089558188 + - 0.53497686314285 + - - -0.8448667089558188 + - 0.53497686314285 +- - - -0.838070598705227 + - 0.5455617944704909 + - - -0.838070598705227 + - 0.5455617944704909 +- - - -0.8311418823156947 + - 0.5560604027088458 + - - -0.8311418823156947 + - 0.5560604027088458 +- - - -0.8240816561033651 + - 0.5664710266853329 + - - -0.8240816561033651 + - 0.5664710266853329 +- - - -0.8168910371929057 + - 0.5767920191489293 + - - -0.8168910371929057 + - 0.5767920191489293 +- - - -0.8095711633407447 + - 0.5870217470308176 + - - -0.8095711633407447 + - 0.5870217470308176 +- - - -0.8021231927550442 + - 0.5971585917027857 + - - -0.8021231927550442 + - 0.5971585917027857 +- - - -0.7945483039124446 + - 0.6072009492333305 + - - -0.7945483039124446 + - 0.6072009492333305 +- - - -0.7868476953715905 + - 0.6171472306414546 + - - -0.7868476953715905 + - 0.6171472306414546 +- - - -0.7790225855834922 + - 0.6269958621480771 + - - -0.7790225855834922 + - 0.6269958621480771 +- - - -0.7710742126987252 + - 0.6367452854250599 + - - -0.7710742126987252 + - 0.6367452854250599 +- - - -0.7630038343715285 + - 0.6463939578417678 + - - -0.7630038343715285 + - 0.6463939578417678 +- - - -0.7548127275607995 + - 0.6559403527091668 + - - -0.7548127275607995 + - 0.6559403527091668 +- - - -0.7465021883280534 + - 0.6653829595213779 + - - -0.7465021883280534 + - 0.6653829595213779 +- - - -0.7380735316323398 + - 0.6747202841946918 + - - -0.7380735316323398 + - 0.6747202841946918 +- - - -0.7295280911221899 + - 0.6839508493039641 + - - -0.7295280911221899 + - 0.6839508493039641 +- - - -0.7208672189245859 + - 0.6930731943163961 + - - -0.7208672189245859 + - 0.6930731943163961 +- - - -0.7120922854310258 + - 0.7020858758226223 + - - -0.7120922854310258 + - 0.7020858758226223 +- - - -0.703204679080685 + - 0.7109874677651012 + - - -0.703204679080685 + - 0.7109874677651012 +- - - -0.694205806140723 + - 0.719776561663763 + - - -0.694205806140723 + - 0.719776561663763 +- - - -0.685097090483782 + - 0.7284517668388598 + - - -0.685097090483782 + - 0.7284517668388598 +- - - -0.6758799733626797 + - 0.7370117106310208 + - - -0.6758799733626797 + - 0.7370117106310208 +- - - -0.6665559131823733 + - 0.745455038618435 + - - -0.6665559131823733 + - 0.745455038618435 +- - - -0.6571263852691893 + - 0.7537804148311689 + - - -0.6571263852691893 + - 0.7537804148311689 +- - - -0.6475928816373955 + - 0.7619865219625438 + - - -0.6475928816373955 + - 0.7619865219625438 +- - - -0.6379569107531127 + - 0.7700720615775806 + - - -0.6379569107531127 + - 0.7700720615775806 +- - - -0.6282199972956439 + - 0.7780357543184383 + - - -0.6282199972956439 + - 0.7780357543184383 +- - - -0.6183836819162163 + - 0.7858763401068541 + - - -0.6183836819162163 + - 0.7858763401068541 +- - - -0.6084495209942188 + - 0.7935925783435136 + - - -0.6084495209942188 + - 0.7935925783435136 +- - - -0.5984190863909279 + - 0.8011832481043567 + - - -0.5984190863909279 + - 0.8011832481043567 +- - - -0.5882939652008056 + - 0.8086471483337546 + - - -0.5882939652008056 + - 0.8086471483337546 +- - - -0.5780757595003719 + - 0.8159830980345537 + - - -0.5780757595003719 + - 0.8159830980345537 +- - - -0.5677660860947084 + - 0.8231899364549449 + - - -0.5677660860947084 + - 0.8231899364549449 +- - - -0.5573665762616435 + - 0.8302665232721198 + - - -0.5573665762616435 + - 0.8302665232721198 +- - - -0.546878875493628 + - 0.8372117387727103 + - - -0.546878875493628 + - 0.8372117387727103 +- - - -0.5363046432373839 + - 0.8440244840299495 + - - -0.5363046432373839 + - 0.8440244840299495 +- - - -0.5256455526313215 + - 0.850703681077561 + - - -0.5256455526313215 + - 0.850703681077561 +- - - -0.5149032902408143 + - 0.8572482730803158 + - - -0.5149032902408143 + - 0.8572482730803158 +- - - -0.5040795557913256 + - 0.86365722450126 + - - -0.5040795557913256 + - 0.86365722450126 +- - - -0.49317606189947616 + - 0.8699295212655587 + - - -0.49317606189947616 + - 0.8699295212655587 +- - - -0.4821945338020488 + - 0.8760641709209576 + - - -0.4821945338020488 + - 0.8760641709209576 +- - - -0.4711367090830182 + - 0.8820602027948112 + - - -0.4711367090830182 + - 0.8820602027948112 +- - - -0.46000433739861224 + - 0.8879166681476723 + - - -0.46000433739861224 + - 0.8879166681476723 +- - - -0.44879918020046267 + - 0.893632640323412 + - - -0.44879918020046267 + - 0.893632640323412 +- - - -0.43752301045690567 + - 0.8992072148958361 + - - -0.43752301045690567 + - 0.8992072148958361 +- - - -0.4261776123724359 + - 0.9046395098117977 + - - -0.4261776123724359 + - 0.9046395098117977 +- - - -0.4147647811054085 + - 0.909928665530756 + - - -0.4147647811054085 + - 0.909928665530756 +- - - -0.403286322483982 + - 0.9150738451607857 + - - -0.403286322483982 + - 0.9150738451607857 +- - - -0.39174405272039897 + - 0.9200742345909907 + - - -0.39174405272039897 + - 0.9200742345909907 +- - - -0.3801397981235976 + - 0.9249290426203247 + - - -0.3801397981235976 + - 0.9249290426203247 +- - - -0.3684753948102517 + - 0.9296375010827764 + - - -0.3684753948102517 + - 0.9296375010827764 +- - - -0.3567526884142328 + - 0.9341988649689195 + - - -0.3567526884142328 + - 0.9341988649689195 +- - - -0.34497353379459245 + - 0.9386124125437886 + - - -0.34497353379459245 + - 0.9386124125437886 +- - - -0.33313979474205874 + - 0.9428774454610838 + - - -0.33313979474205874 + - 0.9428774454610838 +- - - -0.3212533436841441 + - 0.9469932888736632 + - - -0.3212533436841441 + - 0.9469932888736632 +- - - -0.30931606138887024 + - 0.9509592915403249 + - - -0.30931606138887024 + - 0.9509592915403249 +- - - -0.2973298366671729 + - 0.9547748259288534 + - - -0.2973298366671729 + - 0.9547748259288534 +- - - -0.28529656607405124 + - 0.9584392883153082 + - - -0.28529656607405124 + - 0.9584392883153082 +- - - -0.2732181536084666 + - 0.9619520988795546 + - - -0.2732181536084666 + - 0.9619520988795546 +- - - -0.26109651041208987 + - 0.9653127017970029 + - - -0.26109651041208987 + - 0.9653127017970029 +- - - -0.24893355446689247 + - 0.9685205653265596 + - - -0.24893355446689247 + - 0.9685205653265596 +- - - -0.2367312102916815 + - 0.9715751818947599 + - - -0.2367312102916815 + - 0.9715751818947599 +- - - -0.22449140863757358 + - 0.974476068176083 + - - -0.22449140863757358 + - 0.974476068176083 +- - - -0.2122160861825098 + - 0.9772227651694252 + - - -0.2122160861825098 + - 0.9772227651694252 +- - - -0.19990718522480572 + - 0.9798148382707292 + - - -0.19990718522480572 + - 0.9798148382707292 +- - - -0.1875666533758392 + - 0.9822518773417477 + - - -0.1875666533758392 + - 0.9822518773417477 +- - - -0.17519644325187023 + - 0.9845334967749417 + - - -0.17519644325187023 + - 0.9845334967749417 +- - - -0.16279851216509478 + - 0.9866593355544919 + - - -0.16279851216509478 + - 0.9866593355544919 +- - - -0.1503748218139381 + - 0.9886290573134224 + - - -0.1503748218139381 + - 0.9886290573134224 +- - - -0.1379273379726542 + - 0.9904423503868245 + - - -0.1379273379726542 + - 0.9904423503868245 +- - - -0.12545803018029758 + - 0.9920989278611683 + - - -0.12545803018029758 + - 0.9920989278611683 +- - - -0.11296887142907358 + - 0.9935985276197029 + - - -0.11296887142907358 + - 0.9935985276197029 +- - - -0.10046183785216964 + - 0.9949409123839287 + - - -0.10046183785216964 + - 0.9949409123839287 +- - - -0.08793890841106214 + - 0.9961258697511428 + - - -0.08793890841106214 + - 0.9961258697511428 +- - - -0.07540206458240344 + - 0.9971532122280462 + - - -0.07540206458240344 + - 0.9971532122280462 +- - - -0.06285329004448297 + - 0.9980227772604111 + - - -0.06285329004448297 + - 0.9980227772604111 +- - - -0.05029457036336817 + - 0.9987344272588005 + - - -0.05029457036336817 + - 0.9987344272588005 +- - - -0.037727892678718344 + - 0.99928804962034 + - - -0.037727892678718344 + - 0.99928804962034 +- - - -0.025155245389377974 + - 0.9996835567465338 + - - -0.025155245389377974 + - 0.9996835567465338 +- - - -0.012578617838742366 + - 0.9999208860571255 + - - -0.012578617838742366 + - 0.9999208860571255 +- - - -4.898587196589413e-16 + - 1.0 + - - -4.898587196589413e-16 + - 1.0 +init_spikes: +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +- - 0.0 + - 0.0 +n_neurons: 2 diff --git a/docs/examples/plot_ND_basis_function.py b/docs/examples/plot_ND_basis_function.py index d356e40a..eff939aa 100644 --- a/docs/examples/plot_ND_basis_function.py +++ b/docs/examples/plot_ND_basis_function.py @@ -63,8 +63,9 @@ # $$ # Here, we simply add two basis objects, `a_basis` and `b_basis`, together to define the additive basis. -import numpy as np import matplotlib.pyplot as plt +import numpy as np + import neurostatslib as nsl # Define 1D basis objects diff --git a/docs/examples/plot_1D_basis_function.py b/docs/examples/plot_a1D_basis_function.py similarity index 99% rename from docs/examples/plot_1D_basis_function.py rename to docs/examples/plot_a1D_basis_function.py index 96545f18..2344d0ea 100644 --- a/docs/examples/plot_1D_basis_function.py +++ b/docs/examples/plot_a1D_basis_function.py @@ -12,8 +12,9 @@ - The order of the spline, which should be an integer greater than 1. """ -import numpy as np import matplotlib.pylab as plt +import numpy as np + import neurostatslib as nsl # Initialize hyperparameters diff --git a/docs/examples/plot_example_convolution.py b/docs/examples/plot_example_convolution.py index ddf9f5e8..02337ed3 100644 --- a/docs/examples/plot_example_convolution.py +++ b/docs/examples/plot_example_convolution.py @@ -1,14 +1,15 @@ """ -One-dimensional convolutions +# One-dimensional convolutions """ # %% # ## Generate synthetic data # Generate some simulated spike counts. -import numpy as np -import matplotlib.pylab as plt import matplotlib.patches as patches +import matplotlib.pylab as plt +import numpy as np + import neurostatslib as nsl np.random.seed(10) diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py new file mode 100644 index 00000000..181364a3 --- /dev/null +++ b/docs/examples/plot_glm_demo.py @@ -0,0 +1,264 @@ +""" +# GLM Demo: Toy Model Examples + +## Introduction + +In this demo we will work through two toy example of a Poisson-GLM on synthetic data: a purely feed-forward input model +and a recurrently coupled model. + +In particular, we will learn how to: + +- Define & configurate a GLM object. +- Fit the model +- Cross-validate the model with `sklearn` +- Simulate spike trains. + +Before digging into the GLM module, let's first import the packages + we are going to use for this tutorial, and generate some synthetic + data. + +""" +import jax +import matplotlib.pyplot as plt +import numpy as np +import sklearn.model_selection as slkearn_model_selection +import yaml + +import neurostatslib as nsl + +# Enable float64 precision (optional) +jax.config.update("jax_enable_x64", True) + +np.random.seed(111) +# Random design tensor. Shape (n_time_points, n_neurons, n_features). +X = 0.5*np.random.normal(size=(100, 1, 5)) + +# log-rates & weights, shape (n_neurons, ) and (n_neurons, n_features) respectively. +b_true = np.zeros((1, )) +w_true = np.random.normal(size=(1, 5)) + +# sparsify rates +w_true[0, 1:4] = 0. + +# generate counts +rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) +spikes = np.random.poisson(rate) + +# %% +# ## The Feed-Forward GLM +# +# ### Model Definition +# The class implementing the feed-forward GLM is `neurostatslib.glm.GLM`. +# In order to define the class, one **must** provide: +# +# - **Noise Model**: The noise model for the GLM, e.g. an object of the class of type +# `neurostatslib.noise_model.NoiseModel`. So far, only the `PoissonNoiseModel` noise +# model has been implemented. +# - **Solver**: The desired solver, e.g. an object of the `neurostatslib.solver.Solver` class. +# Currently, we implemented the un-regulrized, Ridge, Lasso, and Group-Lasso solver. +# +# The default for the GLM class is the `PoissonNoiseModel` with log-link function with a Ridge solver. +# Here is how to define the model. + +# default Poisson GLM with Ridge solver and Poisson noise model. +model = nsl.glm.GLM() + +print("Solver type: ", type(model.solver)) +print("Noise model type:",type(model.noise_model)) + +# %% +# ### Model Configuration +# One could visualize the model hyperparameters by calling `get_params` method. + +# Get the glm model parameters only +print("\nGLM model parameters only:") +for key, value in model.get_params(deep=False).items(): + print(f"\t- {key}: {value}") + +# Get the glm model parameters, including the all the +# attributes +print("\nAll parameters:") +for key, value in model.get_params(deep=True).items(): + print(f"\t- {key}: {value}") + +# %% +# These parameters can be configured at initialization and/or +# set after the model is initialized with the following syntax: + +# Poisson noise model with soft-plus NL +noise_model = nsl.noise_model.PoissonNoiseModel(jax.nn.softplus) + +# Observation noise +solver = nsl.solver.RidgeSolver( + solver_name="LBFGS", + alpha=0.1, + solver_kwargs={"tol":10**-10} +) + +# define the GLM +model = nsl.glm.GLM( + noise_model=noise_model, + solver=solver, + data_type=jax.numpy.float64 +) + +print("Solver type: ", type(model.solver)) +print("Noise model type:",type(model.noise_model)) + +# %% +# Hyperparameters can be set at any moment via the `set_params` method. + +model.set_params( + solver=nsl.solver.LassoSolver(), + noise_model__inverse_link_function=jax.numpy.exp +) + +print("Updated solver: ", model.solver) +print("Updated NL: ", model.noise_model.inverse_link_function) + +# %% +# !!! warning +# Each `Solver` has an associated attribute `Solver.allowed_optimizers` +# which lists the optimizers that are suited for each optimization problem. +# For example, a RidgeSolver is differentiable and can be fit with `GradientDescent` +# , `BFGS`, etc., while a LassoSolver should use the `ProximalGradient` method instead. +# If the provided `solver_name` is not listed in the `allowed_optimizers` this will raise an +# exception. + +# %% +# ### Model Fit +# Fitting the model is as straight forward as calling the `model.fit` +# providing the design tensor and the population counts. +# Additionally one may provide an initial parameter guess. +# The same exact syntax works for any configuration. + +# Fit a ridge regression Poisson GLM +model = nsl.glm.GLM() +model.set_params(solver__alpha=0.1) +model.fit(X, spikes) + +print("Ridge results") +print("True weights: ", w_true) +print("Recovered weights: ", model.basis_coeff_) + +# %% +# ## K-fold Cross Validation with `sklearn` +# Our implementation follows the `scikit-learn` api, this enables us +# to take advantage of the `scikit-learn` tool-box seamlessly, while at the same time +# we take advantage of the `jax` GPU accelerat# **Lasso**ion and auto-differentiation in the +# back-end. +# +# Here is an example of how we can perform 5-fold cross-validation via `scikit-learn`. +# **Ridge** + +parameter_grid = {"solver__alpha": np.logspace(-1.5, 1.5, 6)} +cls = slkearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls.fit(X, spikes) + +print("Ridge results ") +print("Best hyperparameter: ", cls.best_params_) +print("True weights: ", w_true) +print("Recovered weights: ", cls.best_estimator_.basis_coeff_) + +# %% +# We can compare the Ridge cross-validated results with other solvers. +# **Lasso** + +model.set_params(solver=nsl.solver.LassoSolver()) +cls = slkearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls.fit(X, spikes) + +print("Lasso results ") +print("Best hyperparameter: ", cls.best_params_) +print("True weights: ", w_true) +print("Recovered weights: ", cls.best_estimator_.basis_coeff_) + +# %% +# **Group Lasso** + +# define groups by masking. Mask size (n_groups, n_features) +mask = np.zeros((2, 5)) +mask[0, [0, -1]] = 1 +mask[1, 1:-1] = 1 + +solver = nsl.solver.GroupLassoSolver("ProximalGradient", mask=mask) +model.set_params(solver=solver) +cls = slkearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls.fit(X, spikes) + +print("\nGroup Lasso results") +print("Group mask: :") +print(mask) +print("Best hyperparameter: ", cls.best_params_) +print("True weights: ", w_true) +print("Recovered weights: ", cls.best_estimator_.basis_coeff_) + +# %% +# ## Simulate spikes +# Spikes in response to a feedforward-stimuli can be generated +# through the `model.simulate` method. + +Xnew = np.random.normal(size=(20, ) + X.shape[1:]) +# generate a ranodm key given a seed +random_key = jax.random.PRNGKey(123) +spikes = model.simulate(random_key, Xnew) + +plt.figure() +plt.eventplot(np.where(spikes)[0]) + +# %% +# ## Recurrently Coupled GLM. +# Defining a recurrent model follows the same syntax. Here +# we will import some configuration parameters to simplify the data generation process. + +# load parameters +with open("coupled_neurons_params.yml", "r") as fh: + config_dict = yaml.safe_load(fh) + +# basis weights & intercept for the GLM (both coupling and feedforward) +basis_coeff = np.asarray(config_dict["basis_coeff_"]) +baseline_log_fr = np.asarray(config_dict["baseline_link_fr_"]) + +# basis function, inputs and initial spikes +coupling_basis = jax.numpy.asarray(config_dict["coupling_basis"]) +feedforward_input = jax.numpy.asarray(config_dict["feedforward_input"]) +init_spikes = jax.numpy.asarray(config_dict["init_spikes"]) + +# plot coupling functions +n_basis_coupling = coupling_basis.shape[1] +fig, axs = plt.subplots(2,2) +plt.suptitle("Coupling filters") +for neu_i in range(2): + for neu_j in range(2): + axs[neu_i,neu_j].set_title(f"neu {neu_j} -> neu {neu_i}") + coeff = basis_coeff[neu_i, neu_j*n_basis_coupling: (neu_j+1)*n_basis_coupling] + axs[neu_i, neu_j].plot(np.dot(coupling_basis, coeff)) +plt.tight_layout() + +fig, axs = plt.subplots(1,1) +plt.title("Feedforward inputs") +plt.plot(feedforward_input[:, 0]) + +# %% +# We can now generate spikes from the model by defining a Recurrent GLM, +# setting the parameters and calling the simulate method + +# define the model and set the parameters +model = nsl.glm.GLMRecurrent() +model.basis_coeff_ = jax.numpy.asarray(basis_coeff) +model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr) + +# call simulate, with both the reccurrent coupling +# and the input +sim_spikes, sim_rates = model.simulate( + random_key, + feedforward_input=feedforward_input, + coupling_basis_matrix=coupling_basis, + init_y=init_spikes +) + +plt.figure() +spike_ind, spike_neu = np.where(sim_spikes) +plt.eventplot([spike_ind[spike_neu == 0], spike_ind[spike_neu == 1]]) +plt.yticks([0, 1], ["neu 0", "neu 1"]) + diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 3db45302..e37e1642 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -17,7 +17,7 @@ def __dir__() -> list[str]: class Solver(_Base, abc.ABC): - allowed_solvers = [] + allowed_optimizers = [] def __init__( self, @@ -37,11 +37,11 @@ def __init__( self._check_solver_kwargs(self.solver_name, self.solver_kwargs) def _check_solver(self, solver_name: str): - if solver_name not in self.allowed_solvers: + if solver_name not in self.allowed_optimizers: raise ValueError( f"Solver `{solver_name}` not allowed for " f"{self.__class__} regularization. " - f"Allowed solvers are {self.allowed_solvers}." + f"Allowed solvers are {self.allowed_optimizers}." ) @staticmethod @@ -87,7 +87,7 @@ def solver_run( class UnRegularizedSolver(Solver): - allowed_solvers = [ + allowed_optimizers = [ "GradientDescent", "BFGS", "LBFGS", @@ -116,7 +116,7 @@ def instantiate_solver( class RidgeSolver(Solver): - allowed_solvers = [ + allowed_optimizers = [ "GradientDescent", "BFGS", "LBFGS", @@ -156,7 +156,7 @@ def penalized_loss(params, X, y): class ProxGradientSolver(Solver, abc.ABC): - allowed_solvers = ["ProximalGradient"] + allowed_optimizers = ["ProximalGradient"] def __init__( self, diff --git a/tests/test_glm.py b/tests/test_glm.py index 4837c56d..d3b9e288 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -8,93 +8,121 @@ import neurostatslib as nsl +def _test_class_initialization(cls, kwargs, error, match_str): + if error: + with pytest.raises(error, match=match_str): + cls(**kwargs) + else: + cls(**kwargs) + + +def _test_class_method(cls, method_name, args, kwargs, error, match_str): + if error: + with pytest.raises(error, match=match_str): + getattr(cls, method_name)(*args, **kwargs) + else: + getattr(cls, method_name)(*args, **kwargs) + + class TestGLM: """ Unit tests for the PoissonGLM class. """ + cls = nsl.glm.GLM + ####################### # Test model.__init__ ####################### - @pytest.mark.parametrize("solver", [nsl.solver.RidgeSolver("BFGS"), nsl.solver.Solver, 1]) - def test_init_solver_type(self, solver: nsl.solver.Solver, poisson_noise_model): + @pytest.mark.parametrize( + "solver, error, match_str", + [ + (nsl.solver.RidgeSolver("BFGS"), None, None), + (nsl.solver.Solver, TypeError, "The provided `solver` should be one of the implemented"), + (1, TypeError, "The provided `solver` should be one of the implemented") + ] + ) + def test_init_solver_type(self, solver, error, match_str, poisson_noise_model): """ Test initialization with different solver names. Check if an appropriate exception is raised when the solver name is not present in jaxopt. """ - raise_exception = solver.__class__.__name__ not in nsl.solver.__all__ - if raise_exception: - with pytest.raises(TypeError, match="The provided `solver` should be one of the implemented"): - nsl.glm.GLM(solver=solver, noise_model=poisson_noise_model) - else: - nsl.glm.GLM(solver=solver, noise_model=poisson_noise_model) + _test_class_initialization(self.cls, {'solver': solver, 'noise_model': poisson_noise_model}, error, match_str) - @pytest.mark.parametrize("noise", [nsl.noise_model.PoissonNoiseModel(), nsl.solver.Solver, 1]) - def test_init_noise_type(self, noise: nsl.noise_model.NoiseModel, ridge_solver): + @pytest.mark.parametrize( + "noise, error, match_str", + [ + (nsl.noise_model.PoissonNoiseModel(), None, None), + (nsl.solver.Solver, TypeError, "The provided `noise_model` should be one of the implemented"), + (1, TypeError, "The provided `noise_model` should be one of the implemented") + ] + ) + def test_init_noise_type(self, noise, error, match_str, ridge_solver): """ Test initialization with different solver names. Check if an appropriate exception is raised when the solver name is not present in jaxopt. """ - raise_exception = noise.__class__.__name__ not in nsl.noise_model.__all__ - if raise_exception: - with pytest.raises(TypeError, match="The provided `noise_model` should be one of the implemented"): - nsl.glm.GLM(solver=ridge_solver, noise_model=noise) - else: - nsl.glm.GLM(solver=ridge_solver, noise_model=noise) + _test_class_initialization(self.cls, {'solver': ridge_solver, 'noise_model': noise}, error, match_str) ####################### # Test model.fit ####################### - @pytest.mark.parametrize("n_params", [0, 1, 2, 3]) - def test_fit_param_length(self, n_params, poissonGLM_model_instantiation): + @pytest.mark.parametrize("n_params, error, match_str", [ + (0, ValueError, "Params needs to be array-like of length two."), + (1, ValueError, "Params needs to be array-like of length two."), + (2, None, None), + (3, ValueError, "Params needs to be array-like of length two."), + ]) + def test_fit_param_length(self, n_params, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with different numbers of initial parameters. Check for correct number of parameters. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.log(y.mean(axis=0)) if n_params == 0: init_params = tuple() elif n_params == 1: - init_params = (init_w,) - elif n_params == 2: - init_params = (init_w, init_b) + init_params = (true_params[0],) else: - init_params = (init_w, init_b) + (init_w,) * (n_params - 2) - - raise_exception = n_params != 2 - if raise_exception: - with pytest.raises(ValueError, match="Params needs to be array-like of length two."): - model.fit(X, y, init_params=init_params) - else: - model.fit(X, y, init_params=init_params) - - @pytest.mark.parametrize("add_entry", [0, np.nan, np.inf]) - @pytest.mark.parametrize("add_to", ["X", "y"]) - def test_fit_param_values(self, add_entry, add_to, poissonGLM_model_instantiation): + init_params = true_params + (true_params[0],) * (n_params - 2) + _test_class_method(model, "fit", + [X, y], + {"init_params": init_params}, + error, match_str) + + @pytest.mark.parametrize("add_entry, add_to, error, match_str", [ + (0, "X", None, None), + (np.nan, "X", ValueError, "Input X contains a NaNs or Infs"), + (np.inf, "X", ValueError, "Input X contains a NaNs or Infs"), + (0, "y", None, None), + (np.nan, "y", ValueError, "Input y contains a NaNs or Infs"), + (np.inf, "y", ValueError, "Input y contains a NaNs or Infs"), + ]) + def test_fit_param_values(self, add_entry, add_to, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with altered X or y values. Ensure the method raises exceptions for NaN or Inf values. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if add_to == "X": + # get an index to be edited idx = np.unravel_index(np.random.choice(X.size), X.shape) X[idx] = add_entry elif add_to == "y": idx = np.unravel_index(np.random.choice(y.size), y.shape) y = np.asarray(y, dtype=np.float32) y[idx] = add_entry - - raise_exception = jnp.isnan(add_entry) or jnp.isinf(add_entry) - if raise_exception: - with pytest.raises(ValueError, match="Input (X|y) contains a NaNs or Infs"): - model.fit(X, y, init_params=true_params) - else: - model.fit(X, y, init_params=true_params) - - @pytest.mark.parametrize("dim_weights", [0, 1, 2, 3]) - def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instantiation): + _test_class_method(model, "fit", + [X, y], + {"init_params": true_params}, + error, match_str) + + @pytest.mark.parametrize("dim_weights, error, match_str", [ + (0, ValueError, "params\[0\] must be of shape \(n_neurons, n_features\)"), + (1, ValueError, "params\[0\] must be of shape \(n_neurons, n_features\)"), + (2, None, None), + (3, ValueError, "params\[0\] must be of shape \(n_neurons, n_features\)") + ]) + def test_fit_weights_dimensionality(self, dim_weights, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with weight matrices of different dimensionalities. Check for correct dimensionality. @@ -109,16 +137,15 @@ def test_fit_weights_dimensionality(self, dim_weights, poissonGLM_model_instanti init_w = jnp.zeros((n_neurons, n_features)) else: init_w = jnp.zeros((n_neurons, n_features) + (1,) * (dim_weights - 2)) - init_b = jnp.log(y.mean(axis=0)) - raise_exception = dim_weights != 2 - if raise_exception: - with pytest.raises(ValueError, match="params\[0\] must be of shape \(n_neurons, n_features\)"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": (init_w, true_params[1])}, error, match_str) - @pytest.mark.parametrize("dim_intercepts", [0, 1, 2, 3]) - def test_fit_intercepts_dimensionality(self, dim_intercepts, poissonGLM_model_instantiation): + @pytest.mark.parametrize("dim_intercepts, error, match_str", [ + (0, ValueError, "params\[1\] must be of shape"), + (1, None, None), + (2, ValueError, "params\[1\] must be of shape"), + (3, ValueError, "params\[1\] must be of shape") + ]) + def test_fit_intercepts_dimensionality(self, dim_intercepts, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with intercepts of different dimensionalities. Check for correct dimensionality. """ @@ -127,44 +154,36 @@ def test_fit_intercepts_dimensionality(self, dim_intercepts, poissonGLM_model_in init_b = jnp.zeros((n_neurons,) * dim_intercepts) init_w = jnp.zeros((n_neurons, n_features)) - raise_exception = dim_intercepts != 1 - if raise_exception: - with pytest.raises(ValueError, match="params\[1\] must be of shape"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) - - @pytest.mark.parametrize("init_params", - [dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), - [jnp.zeros((1, 5)), jnp.zeros((1,))], - dict(p1=jnp.zeros((1, 5)), p2=np.zeros((1,), dtype='U10')), - 0, - {0, 1}, - iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), - [jnp.zeros((1, 5)), ""], - ["", jnp.zeros((1,))]]) - def test_fit_init_params_type(self, init_params, poissonGLM_model_instantiation): + _test_class_method(model, "fit", [X, y], {"init_params": (init_w, init_b)}, error, match_str) + + @pytest.mark.parametrize( + "init_params, error, match_str", + [ + ([jnp.zeros((1, 5)), jnp.zeros((1,))], None, None), + (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), TypeError, "Initial parameters must be array-like"), + (dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), TypeError, "Initial parameters must be array-like"), + (0, TypeError, "Initial parameters must be array-like"), + ({0, 1}, TypeError, "Initial parameters must be array-like"), + ([jnp.zeros((1, 5)), ""], TypeError, "Initial parameters must be array-like"), + (["", jnp.zeros((1,))], TypeError, "Initial parameters must be array-like") + ] + ) + def test_fit_init_params_type(self, init_params, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with various types of initial parameters. Ensure that the provided initial parameters are array-like. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - # check if parameter can be converted - try: - tuple(jnp.asarray(par, dtype=jnp.float32) for par in init_params) - # ensure that it's an array-like (for example excluding sets and iterators) - raise_exception = not hasattr(init_params, "__getitem__") - except(TypeError, ValueError): - raise_exception = True + _test_class_method(model, "fit", [X, y], {"init_params": init_params}, error, match_str) - if raise_exception: - with pytest.raises(TypeError, match="Initial parameters must be array-like"): - model.fit(X, y, init_params=init_params) - else: - model.fit(X, y, init_params=init_params) - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_fit_n_neuron_match_weights(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "Model parameters have inconsistent shapes"), + (0, None, None), + (1, ValueError, "Model parameters have inconsistent shapes") + ]) + def test_fit_n_neuron_match_weights(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method ensuring the number of neurons in the weights matches the expected number. """ @@ -172,38 +191,25 @@ def test_fit_n_neuron_match_weights(self, delta_n_neuron, poissonGLM_model_insta X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape init_w = jnp.zeros((n_neurons + delta_n_neuron, n_features)) - init_b = jnp.zeros((n_neurons, )) - # model.basis_coeff_ = init_w - # model.baseline_link_fr_ = init_b - if raise_exception: - with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): - model.fit(X, y, init_params=(init_w, init_b)) - # with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): - # model.predict(X) - # with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): - # model.score(X, y) - else: - model.fit(X, y, init_params=(init_w, init_b)) - # model.predict(X) - # model.score(X, y) + _test_class_method(model, "fit", [X, y], {"init_params": (init_w, true_params[1])}, error, match_str) - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "Model parameters have inconsistent shapes"), + (0, None, None), + (1, ValueError, "Model parameters have inconsistent shapes") + ]) + def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method ensuring the number of neurons in the baseline rate matches the expected number. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) init_b = jnp.zeros((n_neurons + delta_n_neuron,)) + _test_class_method(model, "fit", [X, y], {"init_params": (true_params[0], init_b)}, error, match_str) - if raise_exception: - with pytest.raises(ValueError, match="Model parameters have inconsistent shapes"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) def test_fit_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): From 3fc04b7b192e5d4cbd93dfde89ba82e44fc749ff Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 13 Sep 2023 13:47:26 -0400 Subject: [PATCH 068/250] fixed linking of noise model --- docs/examples/plot_glm_demo.py | 151 +++++++++++++++++++++++-------- src/neurostatslib/glm.py | 13 ++- src/neurostatslib/noise_model.py | 2 +- 3 files changed, 120 insertions(+), 46 deletions(-) diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 181364a3..3c48a6a0 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -20,8 +20,9 @@ """ import jax import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle import numpy as np -import sklearn.model_selection as slkearn_model_selection +import sklearn.model_selection as sklearn_model_selection import yaml import neurostatslib as nsl @@ -37,7 +38,7 @@ b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) -# sparsify rates +# sparsify weights w_true[0, 1:4] = 0. # generate counts @@ -71,14 +72,16 @@ # One could visualize the model hyperparameters by calling `get_params` method. # Get the glm model parameters only -print("\nGLM model parameters only:") +print("\nGLM model parameters:") for key, value in model.get_params(deep=False).items(): print(f"\t- {key}: {value}") # Get the glm model parameters, including the all the # attributes -print("\nAll parameters:") +print("\nNested parameters:") for key, value in model.get_params(deep=True).items(): + if key in model.get_params(deep=False): + continue print(f"\t- {key}: {value}") # %% @@ -145,14 +148,14 @@ # ## K-fold Cross Validation with `sklearn` # Our implementation follows the `scikit-learn` api, this enables us # to take advantage of the `scikit-learn` tool-box seamlessly, while at the same time -# we take advantage of the `jax` GPU accelerat# **Lasso**ion and auto-differentiation in the +# we take advantage of the `jax` GPU acceleration and auto-differentiation in the # back-end. # # Here is an example of how we can perform 5-fold cross-validation via `scikit-learn`. # **Ridge** parameter_grid = {"solver__alpha": np.logspace(-1.5, 1.5, 6)} -cls = slkearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) print("Ridge results ") @@ -162,10 +165,11 @@ # %% # We can compare the Ridge cross-validated results with other solvers. +# # **Lasso** model.set_params(solver=nsl.solver.LassoSolver()) -cls = slkearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) print("Lasso results ") @@ -183,7 +187,7 @@ solver = nsl.solver.GroupLassoSolver("ProximalGradient", mask=mask) model.set_params(solver=solver) -cls = slkearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) print("\nGroup Lasso results") @@ -194,18 +198,21 @@ print("Recovered weights: ", cls.best_estimator_.basis_coeff_) # %% -# ## Simulate spikes -# Spikes in response to a feedforward-stimuli can be generated +# ## Simulate Spikes +# We can generate spikes in response to a feedforward-stimuli # through the `model.simulate` method. +# here we are creating a new data input, of 20 timepoints (arbitrary) +# with the same number of neurons and features (mandatory) Xnew = np.random.normal(size=(20, ) + X.shape[1:]) -# generate a ranodm key given a seed +# generate a random key given a seed random_key = jax.random.PRNGKey(123) -spikes = model.simulate(random_key, Xnew) +spikes, rates = model.simulate(random_key, Xnew) plt.figure() plt.eventplot(np.where(spikes)[0]) + # %% # ## Recurrently Coupled GLM. # Defining a recurrent model follows the same syntax. Here @@ -216,7 +223,10 @@ config_dict = yaml.safe_load(fh) # basis weights & intercept for the GLM (both coupling and feedforward) -basis_coeff = np.asarray(config_dict["basis_coeff_"]) +basis_coeff = np.asarray(config_dict["basis_coeff_"])[:, :-1] + +# Only neuron 1 gets +basis_coeff[:, 40:] = np.abs(basis_coeff[:, 40:]) * np.array([[1.], [0.]]) baseline_log_fr = np.asarray(config_dict["baseline_link_fr_"]) # basis function, inputs and initial spikes @@ -224,41 +234,106 @@ feedforward_input = jax.numpy.asarray(config_dict["feedforward_input"]) init_spikes = jax.numpy.asarray(config_dict["init_spikes"]) -# plot coupling functions -n_basis_coupling = coupling_basis.shape[1] -fig, axs = plt.subplots(2,2) -plt.suptitle("Coupling filters") -for neu_i in range(2): - for neu_j in range(2): - axs[neu_i,neu_j].set_title(f"neu {neu_j} -> neu {neu_i}") - coeff = basis_coeff[neu_i, neu_j*n_basis_coupling: (neu_j+1)*n_basis_coupling] - axs[neu_i, neu_j].plot(np.dot(coupling_basis, coeff)) -plt.tight_layout() - -fig, axs = plt.subplots(1,1) -plt.title("Feedforward inputs") -plt.plot(feedforward_input[:, 0]) -# %% -# We can now generate spikes from the model by defining a Recurrent GLM, -# setting the parameters and calling the simulate method -# define the model and set the parameters +####################### +# test stim +####################### + model = nsl.glm.GLMRecurrent() model.basis_coeff_ = jax.numpy.asarray(basis_coeff) -model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr) +model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr -1) + -# call simulate, with both the reccurrent coupling +stim_step = np.zeros((1000, 2, 1)) +stim_step[200:500] = 2.5 + +# call simulate, with both the recurrent coupling # and the input -sim_spikes, sim_rates = model.simulate( +spikes, rates = model.simulate( random_key, - feedforward_input=feedforward_input, + feedforward_input=stim_step, coupling_basis_matrix=coupling_basis, init_y=init_spikes ) + plt.figure() -spike_ind, spike_neu = np.where(sim_spikes) -plt.eventplot([spike_ind[spike_neu == 0], spike_ind[spike_neu == 1]]) -plt.yticks([0, 1], ["neu 0", "neu 1"]) +ax = plt.subplot(111) +ax.spines['top'].set_visible(False) +ax.spines['right'].set_visible(False) + +patch = Rectangle((200, -0.011), 300, 0.15, alpha=0.2, color="grey") + +p0, = plt.plot(rates[:, 0]) +p1, = plt.plot(rates[:, 1]) + +plt.vlines(np.where(spikes[:, 0])[0], 0.00, 0.01, color=p0.get_color(), label="neu 0") +plt.vlines(np.where(spikes[:, 1])[0], -0.01, 0.00, color=p1.get_color(), label="neu 1") +plt.plot(np.exp(basis_coeff[0,-1] * stim_step[:, 0, 0] + baseline_log_fr[0]-1), color='k', lw=0.8, label="stimulus") +ax.add_patch(patch) +plt.ylim(-0.011, .13) +plt.legend() + +model.set_params(noise_model__inverse_link_function=jax.nn.softplus) +spikes_sp, rates_sp = model.simulate( + random_key, + feedforward_input=stim_step, + coupling_basis_matrix=coupling_basis, + init_y=init_spikes +) + +linkr = basis_coeff[0,-1] * stim_step[:, 0, 0] + baseline_log_fr[0]-1 + +plt.figure() +plt.plot(rates[:, 0]) +plt.plot(rates_sp[:, 0]) + + + +# # plot coupling functions +# n_basis_coupling = coupling_basis.shape[1] +# fig, axs = plt.subplots(2,2) +# plt.suptitle("Coupling filters") +# for neu_i in range(2): +# for neu_j in range(2): +# axs[neu_i,neu_j].set_title(f"neu {neu_j} -> neu {neu_i}") +# coeff = basis_coeff[neu_i, neu_j*n_basis_coupling: (neu_j+1)*n_basis_coupling] +# axs[neu_i, neu_j].plot(np.dot(coupling_basis, coeff)) +# plt.tight_layout() +# +# fig, axs = plt.subplots(1,1) +# plt.title("Feedforward inputs") +# plt.plot(feedforward_input[:, 0]) +# +# # %% +# # We can now generate spikes from the model by defining a Recurrent GLM, +# # setting the parameters and calling the simulate method +# +# # define the model and set the parameters +# model = nsl.glm.GLMRecurrent() +# model.basis_coeff_ = jax.numpy.asarray(basis_coeff) +# model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr) +# +# # call simulate, with both the recurrent coupling +# # and the input +# spikes, rates = model.simulate( +# random_key, +# feedforward_input=feedforward_input, +# coupling_basis_matrix=coupling_basis, +# init_y=init_spikes +# ) +# +# plt.figure() +# plt.subplot(121) +# plt.title("Rate") +# plt.plot(rates[:, 0], label="neu 0") +# plt.plot(rates[:, 1], label="neu 1") +# +# plt.subplot(122) +# plt.title("Spikes") +# spike_ind, spike_neu = np.where(spikes) +# plt.eventplot([spike_ind[spike_neu == 0], spike_ind[spike_neu == 1]]) +# plt.yticks([0, 1], ["neu 0", "neu 1"]) +# diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 1a7f8d37..8bea1bf2 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -36,7 +36,6 @@ def __init__( self.noise_model = noise_model self.solver = solver - self.inverse_link_function = noise_model.inverse_link_function if not jax.config.values["jax_enable_x64"] and (data_type == jnp.float64): raise TypeError( @@ -88,7 +87,7 @@ def _predict( The predicted rates. Shape (n_time_bins, n_neurons). """ Ws, bs = params - return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) + return self.noise_model.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: """Predict rates based on fit parameters. @@ -342,7 +341,7 @@ def simulate( device: Literal["cpu", "gpu", "tpu"] = "cpu", # feed-forward input and/coupling basis **kwargs, - ): + ) -> Tuple[jnp.ndarray, jnp.ndarray]: """Simulate neural activity in response to a feed-forward input. Parameters @@ -362,10 +361,10 @@ def simulate( (feedforward_input,) = self._preprocess_simulate( feedforward_input, params_f=(Ws, bs) ) - + predicted_rate = self._predict((Ws, bs), feedforward_input) return self.noise_model.emission_probability( - key=random_key, predicted_rate=self._predict((Ws, bs), feedforward_input) - ) + key=random_key, predicted_rate=predicted_rate + ), predicted_rate class GLMRecurrent(GLM): @@ -541,7 +540,7 @@ def scan_fn( # Predict the firing rate using the model coefficients # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input - firing_rate = self.inverse_link_function( + firing_rate = self.noise_model.inverse_link_function( jnp.einsum("ik,tik->ti", Wr, conv_act) + input_slice + bs[None, :] ) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 3a69019c..c65454bd 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -32,7 +32,7 @@ def negative_log_likelihood(self, firing_rate, y): @staticmethod @abc.abstractmethod def emission_probability( - key: KeyArray, firing_rate: jnp.ndarray, **kwargs + key: KeyArray, predicted_rate: jnp.ndarray, **kwargs ) -> jnp.ndarray: pass From db8c9c08360bd17ea8ec27badf2aa98ee7882f06 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 13 Sep 2023 16:01:14 -0400 Subject: [PATCH 069/250] imporoved text --- docs/examples/coupled_neurons_params.yml | 6002 ++++++++-------------- docs/examples/plot_glm_demo.py | 109 +- 2 files changed, 2037 insertions(+), 4074 deletions(-) diff --git a/docs/examples/coupled_neurons_params.yml b/docs/examples/coupled_neurons_params.yml index 1be6c9cc..d61c47f6 100644 --- a/docs/examples/coupled_neurons_params.yml +++ b/docs/examples/coupled_neurons_params.yml @@ -1,6 +1,6 @@ baseline_link_fr_: -- -3.0 -- -3.0 +- -4.0 +- -4.0 basis_coeff_: - - -0.004372 - -0.02786 @@ -2089,4005 +2089,2005 @@ coupling_basis: - 0.0 feedforward_input: - - - 0.0 - - 1.0 - - - 0.0 - - 1.0 -- - - 0.012578617838741058 - - 0.9999208860571255 - - - 0.012578617838741058 - - 0.9999208860571255 -- - - 0.025155245389375847 - - 0.9996835567465339 - - - 0.025155245389375847 - - 0.9996835567465339 -- - - 0.03772789267871718 - - 0.99928804962034 - - - 0.03772789267871718 - - 0.99928804962034 -- - - 0.05029457036336618 - - 0.9987344272588006 - - - 0.05029457036336618 - - 0.9987344272588006 -- - - 0.06285329004448194 - - 0.9980227772604111 - - - 0.06285329004448194 - - 0.9980227772604111 -- - - 0.07540206458240159 - - 0.9971532122280464 - - - 0.07540206458240159 - - 0.9971532122280464 -- - - 0.08793890841106125 - - 0.9961258697511429 - - - 0.08793890841106125 - - 0.9961258697511429 -- - - 0.10046183785216795 - - 0.9949409123839288 - - - 0.10046183785216795 - - 0.9949409123839288 -- - - 0.11296887142907283 - - 0.9935985276197029 - - - 0.11296887142907283 - - 0.9935985276197029 -- - - 0.12545803018029603 - - 0.9920989278611685 - - - 0.12545803018029603 - - 0.9920989278611685 -- - - 0.13792733797265358 - - 0.9904423503868246 - - - 0.13792733797265358 - - 0.9904423503868246 -- - - 0.1503748218139367 - - 0.9886290573134227 - - - 0.1503748218139367 - - 0.9886290573134227 -- - - 0.1627985121650943 - - 0.986659335554492 - - - 0.1627985121650943 - - 0.986659335554492 -- - - 0.17519644325186898 - - 0.984533496774942 - - - 0.17519644325186898 - - 0.984533496774942 -- - - 0.18756665337583714 - - 0.9822518773417481 - - - 0.18756665337583714 - - 0.9822518773417481 -- - - 0.19990718522480458 - - 0.9798148382707295 - - - 0.19990718522480458 - - 0.9798148382707295 -- - - 0.21221608618250787 - - 0.9772227651694256 - - - 0.21221608618250787 - - 0.9772227651694256 -- - - 0.22449140863757258 - - 0.9744760681760832 - - - 0.22449140863757258 - - 0.9744760681760832 -- - - 0.23673121029167973 - - 0.9715751818947602 - - - 0.23673121029167973 - - 0.9715751818947602 -- - - 0.2489335544668916 - - 0.9685205653265598 - - - 0.2489335544668916 - - 0.9685205653265598 -- - - 0.2610965104120882 - - 0.9653127017970033 - - - 0.2610965104120882 - - 0.9653127017970033 -- - - 0.27321815360846585 - - 0.9619520988795548 - - - 0.27321815360846585 - - 0.9619520988795548 -- - - 0.28529656607404974 - - 0.9584392883153087 - - - 0.28529656607404974 - - 0.9584392883153087 -- - - 0.2973298366671723 - - 0.9547748259288535 - - - 0.2973298366671723 - - 0.9547748259288535 -- - - 0.30931606138886886 - - 0.9509592915403253 - - - 0.30931606138886886 - - 0.9509592915403253 -- - - 0.32125334368414366 - - 0.9469932888736633 - - - 0.32125334368414366 - - 0.9469932888736633 -- - - 0.33313979474205757 - - 0.9428774454610842 - - - 0.33313979474205757 - - 0.9428774454610842 -- - - 0.34497353379459045 - - 0.9386124125437894 - - - 0.34497353379459045 - - 0.9386124125437894 -- - - 0.3567526884142317 - - 0.9341988649689198 - - - 0.3567526884142317 - - 0.9341988649689198 -- - - 0.3684753948102499 - - 0.9296375010827771 - - - 0.3684753948102499 - - 0.9296375010827771 -- - - 0.38013979812359666 - - 0.924929042620325 - - - 0.38013979812359666 - - 0.924929042620325 -- - - 0.3917440527203973 - - 0.9200742345909914 - - - 0.3917440527203973 - - 0.9200742345909914 -- - - 0.4032863224839812 - - 0.915073845160786 - - - 0.4032863224839812 - - 0.915073845160786 -- - - 0.41476478110540693 - - 0.9099286655307568 - - - 0.41476478110540693 - - 0.9099286655307568 -- - - 0.4261776123724353 - - 0.9046395098117981 - - - 0.4261776123724353 - - 0.9046395098117981 -- - - 0.4375230104569043 - - 0.8992072148958368 - - - 0.4375230104569043 - - 0.8992072148958368 -- - - 0.4487991802004621 - - 0.8936326403234123 - - - 0.4487991802004621 - - 0.8936326403234123 -- - - 0.46000433739861096 - - 0.887916668147673 - - - 0.46000433739861096 - - 0.887916668147673 -- - - 0.47113670908301786 - - 0.8820602027948115 - - - 0.47113670908301786 - - 0.8820602027948115 -- - - 0.4821945338020477 - - 0.8760641709209582 - - - 0.4821945338020477 - - 0.8760641709209582 -- - - 0.4931760618994744 - - 0.8699295212655597 - - - 0.4931760618994744 - - 0.8699295212655597 -- - - 0.5040795557913246 - - 0.8636572245012607 - - - 0.5040795557913246 - - 0.8636572245012607 -- - - 0.5149032902408126 - - 0.8572482730803168 - - - 0.5149032902408126 - - 0.8572482730803168 -- - - 0.5256455526313207 - - 0.8507036810775614 - - - 0.5256455526313207 - - 0.8507036810775614 -- - - 0.5363046432373825 - - 0.8440244840299503 - - - 0.5363046432373825 - - 0.8440244840299503 -- - - 0.5468788754936273 - - 0.8372117387727107 - - - 0.5468788754936273 - - 0.8372117387727107 -- - - 0.5573665762616421 - - 0.8302665232721208 - - - 0.5573665762616421 - - 0.8302665232721208 -- - - 0.5677660860947078 - - 0.8231899364549453 - - - 0.5677660860947078 - - 0.8231899364549453 -- - - 0.5780757595003707 - - 0.8159830980345546 - - - 0.5780757595003707 - - 0.8159830980345546 -- - - 0.588293965200805 - - 0.8086471483337551 - - - 0.588293965200805 - - 0.8086471483337551 -- - - 0.5984190863909268 - - 0.8011832481043575 - - - 0.5984190863909268 - - 0.8011832481043575 -- - - 0.608449520994217 - - 0.7935925783435149 - - - 0.608449520994217 - - 0.7935925783435149 -- - - 0.6183836819162153 - - 0.7858763401068549 - - - 0.6183836819162153 - - 0.7858763401068549 -- - - 0.6282199972956423 - - 0.7780357543184395 - - - 0.6282199972956423 - - 0.7780357543184395 -- - - 0.6379569107531118 - - 0.7700720615775812 - - - 0.6379569107531118 - - 0.7700720615775812 -- - - 0.647592881637394 - - 0.7619865219625451 - - - 0.647592881637394 - - 0.7619865219625451 -- - - 0.6571263852691885 - - 0.7537804148311695 - - - 0.6571263852691885 - - 0.7537804148311695 -- - - 0.666555913182372 - - 0.7454550386184362 - - - 0.666555913182372 - - 0.7454550386184362 -- - - 0.675879973362679 - - 0.7370117106310213 - - - 0.675879973362679 - - 0.7370117106310213 -- - - 0.6850970904837809 - - 0.7284517668388609 - - - 0.6850970904837809 - - 0.7284517668388609 -- - - 0.6942058061407225 - - 0.7197765616637636 - - - 0.6942058061407225 - - 0.7197765616637636 -- - - 0.7032046790806838 - - 0.7109874677651024 - - - 0.7032046790806838 - - 0.7109874677651024 -- - - 0.7120922854310254 - - 0.7020858758226226 - - - 0.7120922854310254 - - 0.7020858758226226 -- - - 0.720867218924585 - - 0.6930731943163971 - - - 0.720867218924585 - - 0.6930731943163971 -- - - 0.7295280911221884 - - 0.6839508493039657 - - - 0.7295280911221884 - - 0.6839508493039657 -- - - 0.7380735316323389 - - 0.6747202841946927 - - - 0.7380735316323389 - - 0.6747202841946927 -- - - 0.746502188328052 - - 0.6653829595213794 - - - 0.746502188328052 - - 0.6653829595213794 -- - - 0.7548127275607989 - - 0.6559403527091677 - - - 0.7548127275607989 - - 0.6559403527091677 -- - - 0.7630038343715272 - - 0.6463939578417693 - - - 0.7630038343715272 - - 0.6463939578417693 -- - - 0.7710742126987247 - - 0.6367452854250606 - - - 0.7710742126987247 - - 0.6367452854250606 -- - - 0.7790225855834911 - - 0.6269958621480786 - - - 0.7790225855834911 - - 0.6269958621480786 -- - - 0.7868476953715899 - - 0.6171472306414553 - - - 0.7868476953715899 - - 0.6171472306414553 -- - - 0.7945483039124437 - - 0.6072009492333317 - - - 0.7945483039124437 - - 0.6072009492333317 -- - - 0.8021231927550437 - - 0.5971585917027863 - - - 0.8021231927550437 - - 0.5971585917027863 -- - - 0.809571163340744 - - 0.5870217470308187 - - - 0.809571163340744 - - 0.5870217470308187 -- - - 0.8168910371929053 - - 0.5767920191489297 - - - 0.8168910371929053 - - 0.5767920191489297 -- - - 0.8240816561033644 - - 0.566471026685334 - - - 0.8240816561033644 - - 0.566471026685334 -- - - 0.8311418823156935 - - 0.5560604027088476 - - - 0.8311418823156935 - - 0.5560604027088476 -- - - 0.8380705987052264 - - 0.545561794470492 - - - 0.8380705987052264 - - 0.545561794470492 -- - - 0.8448667089558177 - - 0.5349768631428518 - - - 0.8448667089558177 - - 0.5349768631428518 -- - - 0.8515291377333112 - - 0.5243072835572319 - - - 0.8515291377333112 - - 0.5243072835572319 -- - - 0.8580568308556875 - - 0.5135547439386516 - - - 0.8580568308556875 - - 0.5135547439386516 -- - - 0.8644487554598649 - - 0.5027209456387218 - - - 0.8644487554598649 - - 0.5027209456387218 -- - - 0.8707039001651274 - - 0.4918076028664418 - - - 0.8707039001651274 - - 0.4918076028664418 -- - - 0.8768212752331536 - - 0.4808164424169648 - - - 0.8768212752331536 - - 0.4808164424169648 -- - - 0.8827999127246196 - - 0.4697492033983709 - - - 0.8827999127246196 - - 0.4697492033983709 -- - - 0.8886388666523558 - - 0.45860763695649104 - - - 0.8886388666523558 - - 0.45860763695649104 -- - - 0.8943372131310272 - - 0.4473935059978269 - - - 0.8943372131310272 - - 0.4473935059978269 -- - - 0.8998940505233182 - - 0.4361085849106111 - - - 0.8998940505233182 - - 0.4361085849106111 -- - - 0.9053084995825966 - - 0.42475465928404793 - - - 0.9053084995825966 - - 0.42475465928404793 -- - - 0.9105797035920355 - - 0.4133335256257842 - - - 0.9105797035920355 - - 0.4133335256257842 -- - - 0.9157068285001692 - - 0.4018469910776512 - - - 0.9157068285001692 - - 0.4018469910776512 -- - - 0.920689063052863 - - 0.3902968731297256 - - - 0.920689063052863 - - 0.3902968731297256 -- - - 0.9255256189216778 - - 0.3786849993327503 - - - 0.9255256189216778 - - 0.3786849993327503 -- - - 0.9302157308286042 - - 0.3670132070089654 - - - 0.9302157308286042 - - 0.3670132070089654 -- - - 0.934758656667151 - - 0.35528334296139374 - - - 0.934758656667151 - - 0.35528334296139374 -- - - 0.9391536776197676 - - 0.34349726318162344 - - - 0.9391536776197676 - - 0.34349726318162344 -- - - 0.9434000982715812 - - 0.3316568325561391 - - - 0.9434000982715812 - - 0.3316568325561391 -- - - 0.9474972467204298 - - 0.31976392457124536 - - - 0.9474972467204298 - - 0.31976392457124536 -- - - 0.9514444746831766 - - 0.30782042101662793 - - - 0.9514444746831766 - - 0.30782042101662793 -- - - 0.9552411575982869 - - 0.2958282116876025 - - - 0.9552411575982869 - - 0.2958282116876025 -- - - 0.9588866947246497 - - 0.28378919408609693 - - - 0.9588866947246497 - - 0.28378919408609693 -- - - 0.9623805092366334 - - 0.27170527312041276 - - - 0.9623805092366334 - - 0.27170527312041276 -- - - 0.9657220483153546 - - 0.25957836080381586 - - - 0.9657220483153546 - - 0.25957836080381586 -- - - 0.9689107832361495 - - 0.24741037595200252 - - - 0.9689107832361495 - - 0.24741037595200252 -- - - 0.9719462094522335 - - 0.23520324387949015 - - - 0.9719462094522335 - - 0.23520324387949015 -- - - 0.9748278466745341 - - 0.2229588960949774 - - - 0.9748278466745341 - - 0.2229588960949774 -- - - 0.9775552389476861 - - 0.21067926999572642 - - - 0.9775552389476861 - - 0.21067926999572642 -- - - 0.9801279547221765 - - 0.19836630856101303 - - - 0.9801279547221765 - - 0.19836630856101303 -- - - 0.9825455869226277 - - 0.18602196004469224 - - - 0.9825455869226277 - - 0.18602196004469224 -- - - 0.984807753012208 - - 0.17364817766693041 - - - 0.984807753012208 - - 0.17364817766693041 -- - - 0.98691409505316 - - 0.16124691930515242 - - - 0.98691409505316 - - 0.16124691930515242 -- - - 0.9888642797634357 - - 0.14882014718424924 - - - 0.9888642797634357 - - 0.14882014718424924 -- - - 0.9906579985694317 - - 0.1363698275661 - - - 0.9906579985694317 - - 0.1363698275661 -- - - 0.9922949676548136 - - 0.12389793043845522 - - - 0.9922949676548136 - - 0.12389793043845522 -- - - 0.9937749280054242 - - 0.11140642920322849 - - - 0.9937749280054242 - - 0.11140642920322849 -- - - 0.995097645450266 - - 0.09889730036424986 - - - 0.995097645450266 - - 0.09889730036424986 -- - - 0.9962629106985543 - - 0.08637252321452853 - - - 0.9962629106985543 - - 0.08637252321452853 -- - - 0.9972705393728327 - - 0.07383407952307214 - - - 0.9972705393728327 - - 0.07383407952307214 -- - - 0.9981203720381463 - - 0.06128395322131638 - - - 0.9981203720381463 - - 0.06128395322131638 -- - - 0.9988122742272691 - - 0.04872413008921228 - - - 0.9988122742272691 - - 0.04872413008921228 -- - - 0.9993461364619809 - - 0.036156597441019206 - - - 0.9993461364619809 - - 0.036156597441019206 -- - - 0.9997218742703887 - - 0.023583343810857166 - - - 0.9997218742703887 - - 0.023583343810857166 -- - - 0.9999394282002937 - - 0.011006358638064812 - - - 0.9999394282002937 - - 0.011006358638064812 -- - - 0.9999987638285974 - - -0.001572368047584414 - - - 0.9999987638285974 - - -0.001572368047584414 -- - - 0.9998998717667489 - - -0.014150845940761853 - - - 0.9998998717667489 - - -0.014150845940761853 -- - - 0.9996427676622299 - - -0.026727084775504745 - - - 0.9996427676622299 - - -0.026727084775504745 -- - - 0.9992274921960794 - - -0.03929909464013115 - - - 0.9992274921960794 - - -0.03929909464013115 -- - - 0.9986541110764565 - - -0.0518648862921008 - - - 0.9986541110764565 - - -0.0518648862921008 -- - - 0.9979227150282433 - - -0.06442247147276806 - - - 0.9979227150282433 - - -0.06442247147276806 -- - - 0.9970334197786902 - - -0.07696986322197923 - - - 0.9970334197786902 - - -0.07696986322197923 -- - - 0.9959863660391044 - - -0.08950507619246638 - - - 0.9959863660391044 - - -0.08950507619246638 -- - - 0.9947817194825853 - - -0.10202612696398403 - - - 0.9947817194825853 - - -0.10202612696398403 -- - - 0.9934196707178107 - - -0.11453103435714077 - - - 0.9934196707178107 - - -0.11453103435714077 -- - - 0.991900435258877 - - -0.12701781974687854 - - - 0.991900435258877 - - -0.12701781974687854 -- - - 0.9902242534911986 - - -0.1394845073755453 - - - 0.9902242534911986 - - -0.1394845073755453 -- - - 0.9883913906334728 - - -0.15192912466551547 - - - 0.9883913906334728 - - -0.15192912466551547 -- - - 0.9864021366957146 - - -0.16434970253130593 - - - 0.9864021366957146 - - -0.16434970253130593 -- - - 0.9842568064333687 - - -0.17674427569114137 - - - 0.9842568064333687 - - -0.17674427569114137 -- - - 0.9819557392975067 - - -0.18911088297791617 - - - 0.9819557392975067 - - -0.18911088297791617 -- - - 0.9794992993811165 - - -0.20144756764950503 - - - 0.9794992993811165 - - -0.20144756764950503 -- - - 0.9768878753614926 - - -0.21375237769837538 - - - 0.9768878753614926 - - -0.21375237769837538 -- - - 0.9741218804387363 - - -0.22602336616044894 - - - 0.9741218804387363 - - -0.22602336616044894 -- - - 0.9712017522703763 - - -0.23825859142316483 - - - 0.9712017522703763 - - -0.23825859142316483 -- - - 0.9681279529021188 - - -0.25045611753269825 - - - 0.9681279529021188 - - -0.25045611753269825 -- - - 0.9649009686947391 - - -0.2626140145002818 - - - 0.9649009686947391 - - -0.2626140145002818 -- - - 0.9615213102471255 - - -0.27473035860758266 - - - 0.9615213102471255 - - -0.27473035860758266 -- - - 0.9579895123154889 - - -0.28680323271109 - - - 0.9579895123154889 - - -0.28680323271109 -- - - 0.9543061337287488 - - -0.29883072654545967 - - - 0.9543061337287488 - - -0.29883072654545967 -- - - 0.9504717573001116 - - -0.310810937025771 - - - 0.9504717573001116 - - -0.310810937025771 -- - - 0.9464869897348526 - - -0.32274196854864906 - - - 0.9464869897348526 - - -0.32274196854864906 -- - - 0.9423524615343186 - - -0.33462193329220136 - - - 0.9423524615343186 - - -0.33462193329220136 -- - - 0.9380688268961659 - - -0.3464489515147234 - - - 0.9380688268961659 - - -0.3464489515147234 -- - - 0.9336367636108462 - - -0.3582211518521272 - - - 0.9336367636108462 - - -0.3582211518521272 -- - - 0.9290569729543628 - - -0.369936671614043 - - - 0.9290569729543628 - - -0.369936671614043 -- - - 0.9243301795773085 - - -0.38159365707854837 - - - 0.9243301795773085 - - -0.38159365707854837 -- - - 0.9194571313902055 - - -0.3931902637854787 - - - 0.9194571313902055 - - -0.3931902637854787 -- - - 0.9144385994451658 - - -0.40472465682827324 - - - 0.9144385994451658 - - -0.40472465682827324 -- - - 0.9092753778138886 - - -0.4161950111443075 - - - 0.9092753778138886 - - -0.4161950111443075 -- - - 0.9039682834620162 - - -0.42759951180366895 - - - 0.9039682834620162 - - -0.42759951180366895 -- - - 0.8985181561198674 - - -0.4389363542963303 - - - 0.8985181561198674 - - -0.4389363542963303 -- - - 0.8929258581495686 - - -0.450203744817673 - - - 0.8929258581495686 - - -0.450203744817673 -- - - 0.8871922744086043 - - -0.46139990055231683 - - - 0.8871922744086043 - - -0.46139990055231683 -- - - 0.881318312109807 - - -0.47252304995621186 - - - 0.881318312109807 - - -0.47252304995621186 -- - - 0.8753049006778131 - - -0.4835714330369443 - - - 0.8753049006778131 - - -0.4835714330369443 -- - - 0.869152991601999 - - -0.4945433016322186 - - - 0.869152991601999 - - -0.4945433016322186 -- - - 0.8628635582859312 - - -0.5054369196864643 - - - 0.8628635582859312 - - -0.5054369196864643 -- - - 0.856437595893346 - - -0.5162505635255284 - - - 0.856437595893346 - - -0.5162505635255284 -- - - 0.8498761211906867 - - -0.5269825221294092 - - - 0.8498761211906867 - - -0.5269825221294092 -- - - 0.8431801723862224 - - -0.5376310974029872 - - - 0.8431801723862224 - - -0.5376310974029872 -- - - 0.8363508089657762 - - -0.5481946044447097 - - - 0.8363508089657762 - - -0.5481946044447097 -- - - 0.8293891115250829 - - -0.5586713718131919 - - - 0.8293891115250829 - - -0.5586713718131919 -- - - 0.8222961815988096 - - -0.5690597417916836 - - - 0.8222961815988096 - - -0.5690597417916836 -- - - 0.8150731414862624 - - -0.5793580706503667 - - - 0.8150731414862624 - - -0.5793580706503667 -- - - 0.8077211340738071 - - -0.5895647289064391 - - - 0.8077211340738071 - - -0.5895647289064391 -- - - 0.800241322654032 - - -0.5996781015819448 - - - 0.800241322654032 - - -0.5996781015819448 -- - - 0.7926348907416848 - - -0.6096965884593069 - - - 0.7926348907416848 - - -0.6096965884593069 -- - - 0.7849030418864046 - - -0.6196186043345285 - - - 0.7849030418864046 - - -0.6196186043345285 -- - - 0.7770469994822886 - - -0.6294425792680156 - - - 0.7770469994822886 - - -0.6294425792680156 -- - - 0.769068006574317 - - -0.6391669588329847 - - - 0.769068006574317 - - -0.6391669588329847 -- - - 0.7609673256616678 - - -0.648790204361417 - - - 0.7609673256616678 - - -0.648790204361417 -- - - 0.7527462384979551 - - -0.6583107931875185 - - - 0.7527462384979551 - - -0.6583107931875185 -- - - 0.744406045888419 - - -0.6677272188886485 - - - 0.744406045888419 - - -0.6677272188886485 -- - - 0.7359480674841035 - - -0.6770379915236763 - - - 0.7359480674841035 - - -0.6770379915236763 -- - - 0.7273736415730488 - - -0.6862416378687335 - - - 0.7273736415730488 - - -0.6862416378687335 -- - - 0.7186841248685385 - - -0.6953367016503177 - - - 0.7186841248685385 - - -0.6953367016503177 -- - - 0.7098808922944289 - - -0.7043217437757161 - - - 0.7098808922944289 - - -0.7043217437757161 -- - - 0.7009653367675978 - - -0.7131953425607098 - - - 0.7009653367675978 - - -0.7131953425607098 -- - - 0.6919388689775463 - - -0.7219560939545244 - - - 0.6919388689775463 - - -0.7219560939545244 -- - - 0.6828029171631891 - - -0.7306026117619886 - - - 0.6828029171631891 - - -0.7306026117619886 -- - - 0.673558926886866 - - -0.739133527862871 - - - 0.673558926886866 - - -0.739133527862871 -- - - 0.6642083608056142 - - -0.7475474924283534 - - - 0.6642083608056142 - - -0.7475474924283534 -- - - 0.6547526984397353 - - -0.7558431741346118 - - - 0.6547526984397353 - - -0.7558431741346118 -- - - 0.6451934359386937 - - -0.764019260373469 - - - 0.6451934359386937 - - -0.764019260373469 -- - - 0.6355320858443845 - - -0.7720744574600859 - - - 0.6355320858443845 - - -0.7720744574600859 -- - - 0.6257701768518059 - - -0.7800074908376582 - - - 0.6257701768518059 - - -0.7800074908376582 -- - - 0.6159092535671797 - - -0.7878171052790867 - - - 0.6159092535671797 - - -0.7878171052790867 -- - - 0.6059508762635484 - - -0.7955020650855897 - - - 0.6059508762635484 - - -0.7955020650855897 -- - - 0.5958966206338979 - - -0.8030611542822255 - - - 0.5958966206338979 - - -0.8030611542822255 -- - - 0.5857480775418397 - - -0.8104931768102919 - - - 0.5857480775418397 - - -0.8104931768102919 -- - - 0.5755068527698903 - - -0.8177969567165775 - - - 0.5755068527698903 - - -0.8177969567165775 -- - - 0.5651745667653929 - - -0.8249713383394301 - - - 0.5651745667653929 - - -0.8249713383394301 -- - - 0.5547528543841173 - - -0.8320151864916135 - - - 0.5547528543841173 - - -0.8320151864916135 -- - - 0.5442433646315792 - - -0.8389273866399272 - - - 0.5442433646315792 - - -0.8389273866399272 -- - - 0.5336477604021226 - - -0.8457068450815559 - - - 0.5336477604021226 - - -0.8457068450815559 -- - - 0.5229677182158028 - - -0.8523524891171238 - - - 0.5229677182158028 - - -0.8523524891171238 -- - - 0.5122049279531147 - - -0.8588632672204258 - - - 0.5122049279531147 - - -0.8588632672204258 -- - - 0.5013610925876063 - - -0.865238149204808 - - - 0.5013610925876063 - - -0.865238149204808 -- - - 0.49043792791642066 - - -0.8714761263861723 - - - 0.49043792791642066 - - -0.8714761263861723 -- - - 0.47943716228880995 - - -0.8775762117425775 - - - 0.47943716228880995 - - -0.8775762117425775 -- - - 0.4683605363326608 - - -0.8835374400704151 - - - 0.4683605363326608 - - -0.8835374400704151 -- - - 0.4572098026790794 - - -0.8893588681371302 - - - 0.4572098026790794 - - -0.8893588681371302 -- - - 0.44598672568507636 - - -0.8950395748304677 - - - 0.44598672568507636 - - -0.8950395748304677 -- - - 0.4346930811543961 - - -0.9005786613042182 - - - 0.4346930811543961 - - -0.9005786613042182 -- - - 0.4233306560565345 - - -0.9059752511204399 - - - 0.4233306560565345 - - -0.9059752511204399 -- - - 0.4119012482439928 - - -0.9112284903881356 - - - 0.4119012482439928 - - -0.9112284903881356 -- - - 0.40040666616780407 - - -0.916337547898363 - - - 0.40040666616780407 - - -0.916337547898363 -- - - 0.3888487285913878 - - -0.9213016152557539 - - - 0.3888487285913878 - - -0.9213016152557539 -- - - 0.37722926430277026 - - -0.9261199070064258 - - - 0.37722926430277026 - - -0.9261199070064258 -- - - 0.36555011182521946 - - -0.9307916607622618 - - - 0.36555011182521946 - - -0.9307916607622618 -- - - 0.3538131191263388 - - -0.9353161373215428 - - - 0.3538131191263388 - - -0.9353161373215428 -- - - 0.3420201433256689 - - -0.9396926207859083 - - - 0.3420201433256689 - - -0.9396926207859083 -- - - 0.330173050400837 - - -0.9439204186736329 - - - 0.330173050400837 - - -0.9439204186736329 -- - - 0.3182737148923088 - - -0.9479988620291954 - - - 0.3182737148923088 - - -0.9479988620291954 -- - - 0.3063240196067838 - - -0.9519273055291264 - - - 0.3063240196067838 - - -0.9519273055291264 -- - - 0.29432585531928224 - - -0.9557051275841167 - - - 0.29432585531928224 - - -0.9557051275841167 -- - - 0.2822811204739722 - - -0.9593317304373701 - - - 0.2822811204739722 - - -0.9593317304373701 -- - - 0.27019172088378224 - - -0.9628065402591843 - - - 0.27019172088378224 - - -0.9628065402591843 -- - - 0.25805956942885044 - - -0.9661290072377479 - - - 0.25805956942885044 - - -0.9661290072377479 -- - - 0.24588658575385056 - - -0.9692986056661355 - - - 0.24588658575385056 - - -0.9692986056661355 -- - - 0.23367469596425278 - - -0.9723148340254889 - - - 0.23367469596425278 - - -0.9723148340254889 -- - - 0.22142583232155955 - - -0.975177215064372 - - - 0.22142583232155955 - - -0.975177215064372 -- - - 0.20914193293756786 - - -0.977885295874285 - - - 0.20914193293756786 - - -0.977885295874285 -- - - 0.19682494146770554 - - -0.9804386479613267 - - - 0.19682494146770554 - - -0.9804386479613267 -- - - 0.18447680680349254 - - -0.9828368673139948 - - - 0.18447680680349254 - - -0.9828368673139948 -- - - 0.17209948276416928 - - -0.9850795744671115 - - - 0.17209948276416928 - - -0.9850795744671115 -- - - 0.15969492778754976 - - -0.9871664145618657 - - - 0.15969492778754976 - - -0.9871664145618657 -- - - 0.14726510462014156 - - -0.9890970574019613 - - - 0.14726510462014156 - - -0.9890970574019613 -- - - 0.1348119800065847 - - -0.9908711975058636 - - - 0.1348119800065847 - - -0.9908711975058636 -- - - 0.12233752437845731 - - -0.992488554155135 - - - 0.12233752437845731 - - -0.992488554155135 -- - - 0.1098437115425002 - - -0.9939488714388522 - - - 0.1098437115425002 - - -0.9939488714388522 -- - - 0.09733251836830287 - - -0.9952519182940991 - - - 0.09733251836830287 - - -0.9952519182940991 -- - - 0.0848059244755095 - - -0.9963974885425265 - - - 0.0848059244755095 - - -0.9963974885425265 -- - - 0.07226591192058739 - - -0.9973854009229761 - - - 0.07226591192058739 - - -0.9973854009229761 -- - - 0.05971446488321034 - - -0.9982154991201608 - - - 0.05971446488321034 - - -0.9982154991201608 -- - - 0.04715356935230619 - - -0.9988876517893978 - - - 0.04715356935230619 - - -0.9988876517893978 -- - - 0.034585212811817465 - - -0.9994017525773913 - - - 0.034585212811817465 - - -0.9994017525773913 -- - - 0.022011383926227784 - - -0.9997577201390606 - - - 0.022011383926227784 - - -0.9997577201390606 -- - - 0.009434072225897046 - - -0.999955498150411 - - - 0.009434072225897046 - - -0.999955498150411 -- - - -0.0031447322077359985 - - -0.9999950553174459 - - - -0.0031447322077359985 - - -0.9999950553174459 -- - - -0.015723039057040564 - - -0.9998763853811183 - - - -0.015723039057040564 - - -0.9998763853811183 -- - - -0.02829885808311759 - - -0.9995995071183217 - - - -0.02829885808311759 - - -0.9995995071183217 -- - - -0.04087019944071145 - - -0.9991644643389178 - - - -0.04087019944071145 - - -0.9991644643389178 -- - - -0.053435073993057226 - - -0.9985713258788059 - - - -0.053435073993057226 - - -0.9985713258788059 -- - - -0.06599149362662023 - - -0.9978201855890307 - - - -0.06599149362662023 - - -0.9978201855890307 -- - - -0.07853747156566927 - - -0.996911162320932 - - - -0.07853747156566927 - - -0.996911162320932 -- - - -0.09107102268664041 - - -0.9958443999073396 - - - -0.09107102268664041 - - -0.9958443999073396 -- - - -0.10359016383223883 - - -0.9946200671398149 - - - -0.10359016383223883 - - -0.9946200671398149 -- - - -0.11609291412522968 - - -0.993238357741943 - - - -0.11609291412522968 - - -0.993238357741943 -- - - -0.12857729528186848 - - -0.9916994903386808 - - - -0.12857729528186848 - - -0.9916994903386808 -- - - -0.14104133192491908 - - -0.9900037084217639 - - - -0.14104133192491908 - - -0.9900037084217639 -- - - -0.15348305189621594 - - -0.9881512803111796 - - - -0.15348305189621594 - - -0.9881512803111796 -- - - -0.16590048656871298 - - -0.9861424991127116 - - - -0.16590048656871298 - - -0.9861424991127116 -- - - -0.1782916711579755 - - -0.9839776826715616 - - - -0.1782916711579755 - - -0.9839776826715616 -- - - -0.19065464503306404 - - -0.9816571735220583 - - - -0.19065464503306404 - - -0.9816571735220583 -- - - -0.20298745202676116 - - -0.979181338833458 - - - -0.20298745202676116 - - -0.979181338833458 -- - - -0.2152881407450901 - - -0.9765505703518493 - - - -0.2152881407450901 - - -0.9765505703518493 -- - - -0.2275547648760821 - - -0.9737652843381669 - - - -0.2275547648760821 - - -0.9737652843381669 -- - - -0.23978538349773562 - - -0.9708259215023277 - - - -0.23978538349773562 - - -0.9708259215023277 -- - - -0.25197806138512474 - - -0.967732946933499 - - - -0.25197806138512474 - - -0.967732946933499 -- - - -0.2641308693166058 - - -0.9644868500265071 - - - -0.2641308693166058 - - -0.9644868500265071 -- - - -0.2762418843790738 - - -0.9610881444044029 - - - -0.2762418843790738 - - -0.9610881444044029 -- - - -0.2883091902722216 - - -0.9575373678371909 - - - -0.2883091902722216 - - -0.9575373678371909 -- - - -0.3003308776117502 - - -0.9538350821567405 - - - -0.3003308776117502 - - -0.9538350821567405 -- - - -0.31230504423148914 - - -0.9499818731678872 - - - -0.31230504423148914 - - -0.9499818731678872 -- - - -0.32422979548437053 - - -0.9459783505557425 - - - -0.32422979548437053 - - -0.9459783505557425 -- - - -0.33610324454221563 - - -0.9418251477892251 - - - -0.33610324454221563 - - -0.9418251477892251 -- - - -0.34792351269428334 - - -0.9375229220208277 - - - -0.34792351269428334 - - -0.9375229220208277 -- - - -0.3596887296445355 - - -0.9330723539826374 - - - -0.3596887296445355 - - -0.9330723539826374 -- - - -0.3713970338075679 - - -0.9284741478786258 - - - -0.3713970338075679 - - -0.9284741478786258 -- - - -0.3830465726031674 - - -0.9237290312732227 - - - -0.3830465726031674 - - -0.9237290312732227 -- - - -0.3946355027494405 - - -0.9188377549761962 - - - -0.3946355027494405 - - -0.9188377549761962 -- - - -0.406161990554472 - - -0.9138010929238535 - - - -0.406161990554472 - - -0.9138010929238535 -- - - -0.41762421220646645 - - -0.9086198420565822 - - - -0.41762421220646645 - - -0.9086198420565822 -- - - -0.4290203540623263 - - -0.9032948221927524 - - - -0.4290203540623263 - - -0.9032948221927524 -- - - -0.44034861293461913 - - -0.8978268758989992 - - - -0.44034861293461913 - - -0.8978268758989992 -- - - -0.4516071963768948 - - -0.892216868356904 - - - -0.4516071963768948 - - -0.892216868356904 -- - - -0.46279432296729867 - - -0.8864656872260989 - - - -0.46279432296729867 - - -0.8864656872260989 -- - - -0.47390822259044274 - - -0.8805742425038149 - - - -0.47390822259044274 - - -0.8805742425038149 -- - - -0.4849471367174873 - - -0.8745434663808944 - - - -0.4849471367174873 - - -0.8745434663808944 -- - - -0.495909318684389 - - -0.8683743130942929 - - - -0.495909318684389 - - -0.8683743130942929 -- - - -0.5067930339682724 - - -0.8620677587760915 - - - -0.5067930339682724 - - -0.8620677587760915 -- - - -0.5175965604618782 - - -0.8556248012990468 - - - -0.5175965604618782 - - -0.8556248012990468 -- - - -0.5283181887460511 - - -0.849046460118698 - - - -0.5283181887460511 - - -0.849046460118698 -- - - -0.538956222360216 - - -0.842333776112062 - - - -0.538956222360216 - - -0.842333776112062 -- - - -0.5495089780708056 - - -0.8354878114129367 - - - -0.5495089780708056 - - -0.8354878114129367 -- - - -0.5599747861375949 - - -0.8285096492438424 - - - -0.5599747861375949 - - -0.8285096492438424 -- - - -0.5703519905779012 - - -0.8214003937446254 - - - -0.5703519905779012 - - -0.8214003937446254 -- - - -0.5806389494286053 - - -0.814161169797753 - - - -0.5806389494286053 - - -0.814161169797753 -- - - -0.5908340350059578 - - -0.8067931228503245 - - - -0.5908340350059578 - - -0.8067931228503245 -- - - -0.6009356341631226 - - -0.7992974187328304 - - - -0.6009356341631226 - - -0.7992974187328304 -- - - -0.6109421485454225 - - -0.7916752434746857 - - - -0.6109421485454225 - - -0.7916752434746857 -- - - -0.6208519948432432 - - -0.7839278031165661 - - - -0.6208519948432432 - - -0.7839278031165661 -- - - -0.630663605042557 - - -0.7760563235195791 - - - -0.630663605042557 - - -0.7760563235195791 -- - - -0.6403754266730258 - - -0.7680620501712998 - - - -0.6403754266730258 - - -0.7680620501712998 -- - - -0.6499859230536464 - - -0.7599462479886977 - - - -0.6499859230536464 - - -0.7599462479886977 -- - - -0.6594935735358957 - - -0.7517102011179935 - - - -0.6594935735358957 - - -0.7517102011179935 -- - - -0.6688968737443391 - - -0.7433552127314704 - - - -0.6688968737443391 - - -0.7433552127314704 -- - - -0.6781943358146659 - - -0.7348826048212762 - - - -0.6781943358146659 - - -0.7348826048212762 -- - - -0.6873844886291098 - - -0.7262937179902474 - - - -0.6873844886291098 - - -0.7262937179902474 -- - - -0.6964658780492216 - - -0.717589911239788 - - - -0.6964658780492216 - - -0.717589911239788 -- - - -0.7054370671459529 - - -0.7087725617548385 - - - -0.7054370671459529 - - -0.7087725617548385 -- - - -0.7142966364270207 - - -0.6998430646859656 - - - -0.7142966364270207 - - -0.6998430646859656 -- - - -0.723043184061509 - - -0.6908028329286112 - - - -0.723043184061509 - - -0.6908028329286112 -- - - -0.731675326101678 - - -0.6816532968995332 - - - -0.731675326101678 - - -0.6816532968995332 -- - - -0.7401916967019432 - - -0.6723959043104729 - - - -0.7401916967019432 - - -0.6723959043104729 -- - - -0.7485909483349904 - - -0.6630321199390868 - - - -0.7485909483349904 - - -0.6630321199390868 -- - - -0.7568717520049916 - - -0.6535634253971795 - - - -0.7568717520049916 - - -0.6535634253971795 -- - - -0.7650327974578898 - - -0.6439913188962686 - - - -0.7650327974578898 - - -0.6439913188962686 -- - - -0.7730727933887175 - - -0.634317315010528 - - - -0.7730727933887175 - - -0.634317315010528 -- - - -0.7809904676459172 - - -0.6245429444371393 - - - -0.7809904676459172 - - -0.6245429444371393 -- - - -0.788784567432631 - - -0.6146697537540928 - - - -0.788784567432631 - - -0.6146697537540928 -- - - -0.7964538595049286 - - -0.6046993051754759 - - - -0.7964538595049286 - - -0.6046993051754759 -- - - -0.8039971303669401 - - -0.5946331763042871 - - - -0.8039971303669401 - - -0.5946331763042871 -- - - -0.8114131864628653 - - -0.5844729598828156 - - - -0.8114131864628653 - - -0.5844729598828156 -- - - -0.8187008543658276 - - -0.5742202635406243 - - - -0.8187008543658276 - - -0.5742202635406243 -- - - -0.825858980963543 - - -0.5638767095401779 - - - -0.825858980963543 - - -0.5638767095401779 -- - - -0.8328864336407734 - - -0.5534439345201586 - - - -0.8328864336407734 - - -0.5534439345201586 -- - - -0.8397821004585396 - - -0.5429235892364995 - - - -0.8397821004585396 - - -0.5429235892364995 -- - - -0.8465448903300604 - - -0.5323173383011922 - - - -0.8465448903300604 - - -0.5323173383011922 -- - - -0.8531737331933926 - - -0.521626859918898 - - - -0.8531737331933926 - - -0.521626859918898 -- - - -0.8596675801807451 - - -0.5108538456214089 - - - -0.8596675801807451 - - -0.5108538456214089 -- - - -0.8660254037844384 - - -0.5000000000000004 - - - -0.8660254037844384 - - -0.5000000000000004 -- - - -0.872246198019486 - - -0.4890670404357173 - - - -0.872246198019486 - - -0.4890670404357173 -- - - -0.8783289785827684 - - -0.4780566968276366 - - - -0.8783289785827684 - - -0.4780566968276366 -- - - -0.8842727830087774 - - -0.46697071131914863 - - - -0.8842727830087774 - - -0.46697071131914863 -- - - -0.8900766708219056 - - -0.4558108380223019 - - - -0.8900766708219056 - - -0.4558108380223019 -- - - -0.895739723685255 - - -0.4445788427402534 - - - -0.895739723685255 - - -0.4445788427402534 -- - - -0.9012610455459443 - - -0.4332765026878693 - - - -0.9012610455459443 - - -0.4332765026878693 -- - - -0.9066397627768893 - - -0.4219056062105194 - - - -0.9066397627768893 - - -0.4219056062105194 -- - - -0.9118750243150336 - - -0.410467952501114 - - - -0.9118750243150336 - - -0.410467952501114 -- - - -0.9169660017960133 - - -0.39896535131541655 - - - -0.9169660017960133 - - -0.39896535131541655 -- - - -0.921911889685225 - - -0.38739962268569333 - - - -0.921911889685225 - - -0.38739962268569333 -- - - -0.9267119054052849 - - -0.37577259663273255 - - - -0.9267119054052849 - - -0.37577259663273255 -- - - -0.931365289459854 - - -0.3640861128762842 - - - -0.931365289459854 - - -0.3640861128762842 -- - - -0.9358713055538119 - - -0.3523420205439648 - - - -0.9358713055538119 - - -0.3523420205439648 -- - - -0.9402292407097588 - - -0.3405421778786742 - - - -0.9402292407097588 - - -0.3405421778786742 -- - - -0.9444384053808287 - - -0.32868845194456947 - - - -0.9444384053808287 - - -0.32868845194456947 -- - - -0.948498133559795 - - -0.3167827183316434 - - - -0.948498133559795 - - -0.3167827183316434 -- - - -0.9524077828844512 - - -0.30482686085895394 - - - -0.9524077828844512 - - -0.30482686085895394 -- - - -0.9561667347392507 - - -0.2928227712765512 - - - -0.9561667347392507 - - -0.2928227712765512 -- - - -0.959774394353189 - - -0.28077234896614933 - - - -0.959774394353189 - - -0.28077234896614933 -- - - -0.9632301908939126 - - -0.26867750064059465 - - - -0.9632301908939126 - - -0.26867750064059465 -- - - -0.9665335775580413 - - -0.25654014004216524 - - - -0.9665335775580413 - - -0.25654014004216524 -- - - -0.9696840316576876 - - -0.2443621876397672 - - - -0.9696840316576876 - - -0.2443621876397672 -- - - -0.97268105470316 - - -0.2321455703250619 - - - -0.97268105470316 - - -0.2321455703250619 -- - - -0.9755241724818386 - - -0.21989222110757806 - - - -0.9755241724818386 - - -0.21989222110757806 -- - - -0.9782129351332083 - - -0.2076040788088557 - - - -0.9782129351332083 - - -0.2076040788088557 -- - - -0.9807469172200395 - - -0.19528308775567055 - - - -0.9807469172200395 - - -0.19528308775567055 -- - - -0.9831257177957041 - - -0.18293119747238726 - - - -0.9831257177957041 - - -0.18293119747238726 -- - - -0.9853489604676163 - - -0.17055036237249038 - - - -0.9853489604676163 - - -0.17055036237249038 -- - - -0.9874162934567888 - - -0.15814254144934156 - - - -0.9874162934567888 - - -0.15814254144934156 -- - - -0.9893273896534934 - - -0.14570969796621222 - - - -0.9893273896534934 - - -0.14570969796621222 -- - - -0.9910819466690195 - - -0.1332537991456406 - - - -0.9910819466690195 - - -0.1332537991456406 -- - - -0.9926796868835203 - - -0.1207768158581612 - - - -0.9926796868835203 - - -0.1207768158581612 -- - - -0.9941203574899392 - - -0.10828072231046196 - - - -0.9941203574899392 - - -0.10828072231046196 -- - - -0.9954037305340125 - - -0.09576749573300417 - - - -0.9954037305340125 - - -0.09576749573300417 -- - - -0.9965296029503367 - - -0.08323911606717305 - - - -0.9965296029503367 - - -0.08323911606717305 -- - - -0.9974977965944997 - - -0.070697565651995 - - - -0.9974977965944997 - - -0.070697565651995 -- - - -0.9983081582712682 - - -0.05814482891047624 - - - -0.9983081582712682 - - -0.05814482891047624 -- - - -0.9989605597588274 - - -0.04558289203561173 - - - -0.9989605597588274 - - -0.04558289203561173 -- - - -0.9994548978290693 - - -0.0330137426761141 - - - -0.9994548978290693 - - -0.0330137426761141 -- - - -0.9997910942639261 - - -0.020439369621912166 - - - -0.9997910942639261 - - -0.020439369621912166 -- - - -0.9999690958677468 - - -0.007861762489468911 - - - -0.9999690958677468 - - -0.007861762489468911 -- - - -0.999988874475714 - - 0.004717088593031313 - - - -0.999988874475714 - - 0.004717088593031313 -- - - -0.9998504269583004 - - 0.01729519330057657 - - - -0.9998504269583004 - - 0.01729519330057657 -- - - -0.9995537752217639 - - 0.029870561426252256 - - - -0.9995537752217639 - - 0.029870561426252256 -- - - -0.9990989662046815 - - 0.04244120319614822 - - - -0.9990989662046815 - - 0.04244120319614822 -- - - -0.9984860718705224 - - 0.055005129584192916 - - - -0.9984860718705224 - - 0.055005129584192916 -- - - -0.9977151891962615 - - 0.06756035262687816 - - - -0.9977151891962615 - - 0.06756035262687816 -- - - -0.9967864401570343 - - 0.08010488573780679 - - - -0.9967864401570343 - - 0.08010488573780679 -- - - -0.9956999717068378 - - 0.09263674402202696 - - - -0.9956999717068378 - - 0.09263674402202696 -- - - -0.9944559557552776 - - 0.10515394459009784 - - - -0.9944559557552776 - - 0.10515394459009784 -- - - -0.9930545891403677 - - 0.11765450687183807 - - - -0.9930545891403677 - - 0.11765450687183807 -- - - -0.9914960935973849 - - 0.1301364529297071 - - - -0.9914960935973849 - - 0.1301364529297071 -- - - -0.9897807157237836 - - 0.1425978077717702 - - - -0.9897807157237836 - - 0.1425978077717702 -- - - -0.9879087269401782 - - 0.1550365996641971 - - - -0.9879087269401782 - - 0.1550365996641971 -- - - -0.9858804234473959 - - 0.16745086044324545 - - - -0.9858804234473959 - - 0.16745086044324545 -- - - -0.9836961261796103 - - 0.17983862582667898 - - - -0.9836961261796103 - - 0.17983862582667898 -- - - -0.9813561807535597 - - 0.19219793572457194 - - - -0.9813561807535597 - - 0.19219793572457194 -- - - -0.9788609574138615 - - 0.20452683454945075 - - - -0.9788609574138615 - - 0.20452683454945075 -- - - -0.9762108509744296 - - 0.21682337152571898 - - - -0.9762108509744296 - - 0.21682337152571898 -- - - -0.9734062807560028 - - 0.22908560099832972 - - - -0.9734062807560028 - - 0.22908560099832972 -- - - -0.9704476905197971 - - 0.24131158274063894 - - - -0.9704476905197971 - - 0.24131158274063894 -- - - -0.9673355483972903 - - 0.25349938226140434 - - - -0.9673355483972903 - - 0.25349938226140434 -- - - -0.9640703468161508 - - 0.2656470711108758 - - - -0.9640703468161508 - - 0.2656470711108758 -- - - -0.9606526024223212 - - 0.27775272718593 - - - -0.9606526024223212 - - 0.27775272718593 -- - - -0.957082855998271 - - 0.28981443503420057 - - - -0.957082855998271 - - 0.28981443503420057 -- - - -0.9533616723774295 - - 0.30183028615715607 - - - -0.9533616723774295 - - 0.30183028615715607 -- - - -0.9494896403548136 - - 0.31379837931207794 - - - -0.9494896403548136 - - 0.31379837931207794 -- - - -0.9454673725938637 - - 0.3257168208128897 - - - -0.9454673725938637 - - 0.3257168208128897 -- - - -0.9412955055295036 - - 0.33758372482979143 - - - -0.9412955055295036 - - 0.33758372482979143 -- - - -0.9369746992674384 - - 0.34939721368765 - - - -0.9369746992674384 - - 0.34939721368765 -- - - -0.9325056374797075 - - 0.361155418163101 - - - -0.9325056374797075 - - 0.361155418163101 -- - - -0.9278890272965095 - - 0.3728564777803084 - - - -0.9278890272965095 - - 0.3728564777803084 -- - - -0.9231255991943125 - - 0.3844985411053488 - - - -0.9231255991943125 - - 0.3844985411053488 -- - - -0.9182161068802741 - - 0.3960797660391565 - - - -0.9182161068802741 - - 0.3960797660391565 -- - - -0.9131613271729835 - - 0.4075983201089958 - - - -0.9131613271729835 - - 0.4075983201089958 -- - - -0.9079620598795464 - - 0.41905238075840945 - - - -0.9079620598795464 - - 0.41905238075840945 -- - - -0.9026191276690343 - - 0.4304401356355976 - - - -0.9026191276690343 - - 0.4304401356355976 -- - - -0.8971333759423143 - - 0.4417597828801825 - - - -0.8971333759423143 - - 0.4417597828801825 -- - - -0.8915056726982842 - - 0.4530095314083134 - - - -0.8915056726982842 - - 0.4530095314083134 -- - - -0.8857369083965297 - - 0.4641876011960654 - - - -0.8857369083965297 - - 0.4641876011960654 -- - - -0.8798279958164298 - - 0.4752922235610892 - - - -0.8798279958164298 - - 0.4752922235610892 -- - - -0.873779869912729 - - 0.486321641442466 - - - -0.873779869912729 - - 0.486321641442466 -- - - -0.8675934876676018 - - 0.49727410967872326 - - - -0.8675934876676018 - - 0.49727410967872326 -- - - -0.8612698279392309 - - 0.5081478952839691 - - - -0.8612698279392309 - - 0.5081478952839691 -- - - -0.8548098913069261 - - 0.5189412777220956 - - - -0.8548098913069261 - - 0.5189412777220956 -- - - -0.8482146999128025 - - 0.5296525491790203 - - - -0.8482146999128025 - - 0.5296525491790203 -- - - -0.8414852973000504 - - 0.5402800148329067 - - - -0.8414852973000504 - - 0.5402800148329067 -- - - -0.8346227482478176 - - 0.5508219931223336 - - - -0.8346227482478176 - - 0.5508219931223336 -- - - -0.8276281386027314 - - 0.5612768160123647 - - - -0.8276281386027314 - - 0.5612768160123647 -- - - -0.8205025751070878 - - 0.5716428292584782 - - - -0.8205025751070878 - - 0.5716428292584782 -- - - -0.8132471852237334 - - 0.5819183926683146 - - - -0.8132471852237334 - - 0.5819183926683146 -- - - -0.8058631169576695 - - 0.5921018803612005 - - - -0.8058631169576695 - - 0.5921018803612005 -- - - -0.7983515386744064 - - 0.6021916810254089 - - - -0.7983515386744064 - - 0.6021916810254089 -- - - -0.7907136389150943 - - 0.6121861981731129 - - - -0.7907136389150943 - - 0.6121861981731129 -- - - -0.7829506262084637 - - 0.6220838503929953 - - - -0.7829506262084637 - - 0.6220838503929953 -- - - -0.7750637288796017 - - 0.6318830716004721 - - - -0.7750637288796017 - - 0.6318830716004721 -- - - -0.7670541948555989 - - 0.6415823112854881 - - - -0.7670541948555989 - - 0.6415823112854881 -- - - -0.7589232914680891 - - 0.6511800347578556 - - - -0.7589232914680891 - - 0.6511800347578556 -- - - -0.7506723052527245 - - 0.6606747233900812 - - - -0.7506723052527245 - - 0.6606747233900812 -- - - -0.7423025417456096 - - 0.670064874857657 - - - -0.7423025417456096 - - 0.670064874857657 -- - - -0.7338153252767281 - - 0.6793490033767694 - - - -0.7338153252767281 - - 0.6793490033767694 -- - - -0.7252119987603977 - - 0.6885256399393918 - - - -0.7252119987603977 - - 0.6885256399393918 -- - - -0.7164939234827836 - - 0.6975933325457224 - - - -0.7164939234827836 - - 0.6975933325457224 -- - - -0.7076624788865049 - - 0.706550646433932 - - - -0.7076624788865049 - - 0.706550646433932 -- - - -0.698719062352368 - - 0.7153961643071813 - - - -0.698719062352368 - - 0.7153961643071813 -- - - -0.6896650889782625 - - 0.7241284865578796 - - - -0.6896650889782625 - - 0.7241284865578796 -- - - -0.6805019913552531 - - 0.7327462314891391 - - - -0.6805019913552531 - - 0.7327462314891391 -- - - -0.6712312193409035 - - 0.7412480355333995 - - - -0.6712312193409035 - - 0.7412480355333995 -- - - -0.6618542398298681 - - 0.7496325534681825 - - - -0.6618542398298681 - - 0.7496325534681825 -- - - -0.6523725365217912 - - 0.7578984586289408 - - - -0.6523725365217912 - - 0.7578984586289408 -- - - -0.6427876096865396 - - 0.7660444431189778 - - - -0.6427876096865396 - - 0.7660444431189778 -- - - -0.6331009759268216 - - 0.7740692180163904 - - - -0.6331009759268216 - - 0.7740692180163904 -- - - -0.623314167938217 - - 0.7819715135780128 - - - -0.623314167938217 - - 0.7819715135780128 -- - - -0.6134287342666622 - - 0.7897500794403256 - - - -0.6134287342666622 - - 0.7897500794403256 -- - - -0.6034462390634266 - - 0.7974036848172986 - - - -0.6034462390634266 - - 0.7974036848172986 -- - - -0.5933682618376209 - - 0.8049311186951345 - - - -0.5933682618376209 - - 0.8049311186951345 -- - - -0.5831963972062739 - - 0.8123311900238854 - - - -0.5831963972062739 - - 0.8123311900238854 -- - - -0.5729322546420206 - - 0.819602727905911 - - - -0.5729322546420206 - - 0.819602727905911 -- - - -0.5625774582184379 - - 0.826744581781146 - - - -0.5625774582184379 - - 0.826744581781146 -- - - -0.552133646353071 - - 0.8337556216091511 - - - -0.552133646353071 - - 0.8337556216091511 -- - - -0.541602471548191 - - 0.8406347380479176 - - - -0.541602471548191 - - 0.8406347380479176 -- - - -0.5309856001293205 - - 0.8473808426293961 - - - -0.5309856001293205 - - 0.8473808426293961 -- - - -0.5202847119815792 - - 0.8539928679317206 - - - -0.5202847119815792 - - 0.8539928679317206 -- - - -0.5095015002838734 - - 0.8604697677481075 - - - -0.5095015002838734 - - 0.8604697677481075 -- - - -0.4986376712409919 - - 0.8668105172523927 - - - -0.4986376712409919 - - 0.8668105172523927 -- - - -0.487694943813635 - - 0.8730141131611879 - - - -0.487694943813635 - - 0.8730141131611879 -- - - -0.47667504944642797 - - 0.8790795738926286 - - - -0.47667504944642797 - - 0.8790795738926286 -- - - -0.4655797317939577 - - 0.8850059397216871 - - - -0.4655797317939577 - - 0.8850059397216871 -- - - -0.45441074644487806 - - 0.890792272932028 - - - -0.45441074644487806 - - 0.890792272932028 -- - - -0.4431698606441268 - - 0.8964376579643814 - - - -0.4431698606441268 - - 0.8964376579643814 -- - - -0.4318588530132981 - - 0.9019412015614092 - - - -0.4318588530132981 - - 0.9019412015614092 -- - - -0.4204795132692152 - - 0.907302032909044 - - - -0.4204795132692152 - - 0.907302032909044 -- - - -0.4090336419407468 - - 0.9125193037742757 - - - -0.4090336419407468 - - 0.9125193037742757 -- - - -0.3975230500839139 - - 0.9175921886393661 - - - -0.3975230500839139 - - 0.9175921886393661 -- - - -0.38594955899532896 - - 0.9225198848324686 - - - -0.38594955899532896 - - 0.9225198848324686 -- - - -0.3743149999240192 - - 0.9273016126546322 - - - -0.3743149999240192 - - 0.9273016126546322 -- - - -0.3626212137816673 - - 0.9319366155031737 - - - -0.3626212137816673 - - 0.9319366155031737 -- - - -0.35087005085133094 - - 0.9364241599913922 - - - -0.35087005085133094 - - 0.9364241599913922 -- - - -0.3390633704946757 - - 0.9407635360646108 - - - -0.3390633704946757 - - 0.9407635360646108 -- - - -0.3272030408577722 - - 0.9449540571125281 - - - -0.3272030408577722 - - 0.9449540571125281 -- - - -0.3152909385755031 - - 0.9489950600778585 - - - -0.3152909385755031 - - 0.9489950600778585 -- - - -0.3033289484746273 - - 0.9528859055612465 - - - -0.3033289484746273 - - 0.9528859055612465 -- - - -0.29131896327554796 - - 0.9566259779224375 - - - -0.29131896327554796 - - 0.9566259779224375 -- - - -0.2792628832928309 - - 0.9602146853776892 - - - -0.2792628832928309 - - 0.9602146853776892 -- - - -0.26716261613452225 - - 0.9636514600934084 - - - -0.26716261613452225 - - 0.9636514600934084 -- - - -0.25502007640031144 - - 0.9669357582759981 - - - -0.25502007640031144 - - 0.9669357582759981 -- - - -0.24283718537858734 - - 0.9700670602579007 - - - -0.24283718537858734 - - 0.9700670602579007 -- - - -0.23061587074244044 - - 0.9730448705798238 - - - -0.23061587074244044 - - 0.9730448705798238 -- - - -0.21835806624464577 - - 0.975868718069136 - - - -0.21835806624464577 - - 0.975868718069136 -- - - -0.20606571141169297 - - 0.9785381559144195 - - - -0.20606571141169297 - - 0.9785381559144195 -- - - -0.19374075123689813 - - 0.981052761736168 - - - -0.19374075123689813 - - 0.981052761736168 -- - - -0.18138513587265162 - - 0.9834121376536186 - - - -0.18138513587265162 - - 0.9834121376536186 -- - - -0.16900082032184968 - - 0.9856159103477083 - - - -0.16900082032184968 - - 0.9856159103477083 -- - - -0.15658976412855838 - - 0.9876637311201432 - - - -0.15658976412855838 - - 0.9876637311201432 -- - - -0.14415393106795907 - - 0.9895552759485718 - - - -0.14415393106795907 - - 0.9895552759485718 -- - - -0.13169528883562445 - - 0.9912902455378553 - - - -0.13169528883562445 - - 0.9912902455378553 -- - - -0.11921580873617425 - - 0.9928683653674237 - - - -0.11921580873617425 - - 0.9928683653674237 -- - - -0.10671746537135988 - - 0.9942893857347128 - - - -0.10671746537135988 - - 0.9942893857347128 -- - - -0.0942022363276273 - - 0.9955530817946745 - - - -0.0942022363276273 - - 0.9955530817946745 -- - - -0.08167210186320688 - - 0.9966592535953529 - - - -0.08167210186320688 - - 0.9966592535953529 -- - - -0.06912904459478485 - - 0.9976077261095226 - - - -0.06912904459478485 - - 0.9976077261095226 -- - - -0.056575049183792726 - - 0.998398349262383 - - - -0.056575049183792726 - - 0.998398349262383 -- - - -0.04401210202238211 - - 0.9990309979553044 - - - -0.04401210202238211 - - 0.9990309979553044 -- - - -0.031442190919121114 - - 0.9995055720856215 - - - -0.031442190919121114 - - 0.9995055720856215 -- - - -0.018867304784467676 - - 0.9998219965624732 - - - -0.018867304784467676 - - 0.9998219965624732 -- - - -0.006289433316068405 - - 0.9999802213186832 - - - -0.006289433316068405 - - 0.9999802213186832 -- - - 0.006289433316067026 - - 0.9999802213186832 - - - 0.006289433316067026 - - 0.9999802213186832 -- - - 0.0188673047844663 - - 0.9998219965624732 - - - 0.0188673047844663 - - 0.9998219965624732 -- - - 0.03144219091911974 - - 0.9995055720856215 - - - 0.03144219091911974 - - 0.9995055720856215 -- - - 0.04401210202238073 - - 0.9990309979553045 - - - 0.04401210202238073 - - 0.9990309979553045 -- - - 0.056575049183791346 - - 0.9983983492623831 - - - 0.056575049183791346 - - 0.9983983492623831 -- - - 0.06912904459478347 - - 0.9976077261095226 - - - 0.06912904459478347 - - 0.9976077261095226 -- - - 0.08167210186320639 - - 0.9966592535953529 - - - 0.08167210186320639 - - 0.9966592535953529 -- - - 0.09420223632762592 - - 0.9955530817946746 - - - 0.09420223632762592 - - 0.9955530817946746 -- - - 0.10671746537135851 - - 0.994289385734713 - - - 0.10671746537135851 - - 0.994289385734713 -- - - 0.11921580873617288 - - 0.9928683653674238 - - - 0.11921580873617288 - - 0.9928683653674238 -- - - 0.13169528883562306 - - 0.9912902455378555 - - - 0.13169528883562306 - - 0.9912902455378555 -- - - 0.14415393106795768 - - 0.9895552759485721 - - - 0.14415393106795768 - - 0.9895552759485721 -- - - 0.15658976412855702 - - 0.9876637311201434 - - - 0.15658976412855702 - - 0.9876637311201434 -- - - 0.16900082032184832 - - 0.9856159103477086 - - - 0.16900082032184832 - - 0.9856159103477086 -- - - 0.18138513587265026 - - 0.9834121376536189 - - - 0.18138513587265026 - - 0.9834121376536189 -- - - 0.19374075123689677 - - 0.9810527617361683 - - - 0.19374075123689677 - - 0.9810527617361683 -- - - 0.2060657114116916 - - 0.9785381559144198 - - - 0.2060657114116916 - - 0.9785381559144198 -- - - 0.21835806624464443 - - 0.9758687180691363 - - - 0.21835806624464443 - - 0.9758687180691363 -- - - 0.2306158707424391 - - 0.9730448705798241 - - - 0.2306158707424391 - - 0.9730448705798241 -- - - 0.24283718537858687 - - 0.9700670602579009 - - - 0.24283718537858687 - - 0.9700670602579009 -- - - 0.2550200764003101 - - 0.9669357582759984 - - - 0.2550200764003101 - - 0.9669357582759984 -- - - 0.2671626161345209 - - 0.9636514600934087 - - - 0.2671626161345209 - - 0.9636514600934087 -- - - 0.2792628832928296 - - 0.9602146853776896 - - - 0.2792628832928296 - - 0.9602146853776896 -- - - 0.2913189632755466 - - 0.956625977922438 - - - 0.2913189632755466 - - 0.956625977922438 -- - - 0.30332894847462605 - - 0.952885905561247 - - - 0.30332894847462605 - - 0.952885905561247 -- - - 0.3152909385755018 - - 0.9489950600778589 - - - 0.3152909385755018 - - 0.9489950600778589 -- - - 0.3272030408577709 - - 0.9449540571125286 - - - 0.3272030408577709 - - 0.9449540571125286 -- - - 0.33906337049467444 - - 0.9407635360646113 - - - 0.33906337049467444 - - 0.9407635360646113 -- - - 0.3508700508513296 - - 0.9364241599913926 - - - 0.3508700508513296 - - 0.9364241599913926 -- - - 0.36262121378166595 - - 0.9319366155031743 - - - 0.36262121378166595 - - 0.9319366155031743 -- - - 0.3743149999240179 - - 0.9273016126546327 - - - 0.3743149999240179 - - 0.9273016126546327 -- - - 0.3859495589953277 - - 0.9225198848324692 - - - 0.3859495589953277 - - 0.9225198848324692 -- - - 0.39752305008391264 - - 0.9175921886393666 - - - 0.39752305008391264 - - 0.9175921886393666 -- - - 0.40903364194074554 - - 0.9125193037742763 - - - 0.40903364194074554 - - 0.9125193037742763 -- - - 0.4204795132692139 - - 0.9073020329090445 - - - 0.4204795132692139 - - 0.9073020329090445 -- - - 0.4318588530132969 - - 0.9019412015614098 - - - 0.4318588530132969 - - 0.9019412015614098 -- - - 0.44316986064412556 - - 0.896437657964382 - - - 0.44316986064412556 - - 0.896437657964382 -- - - 0.45441074644487683 - - 0.8907922729320287 - - - 0.45441074644487683 - - 0.8907922729320287 -- - - 0.46557973179395645 - - 0.8850059397216877 - - - 0.46557973179395645 - - 0.8850059397216877 -- - - 0.47667504944642675 - - 0.8790795738926293 - - - 0.47667504944642675 - - 0.8790795738926293 -- - - 0.48769494381363376 - - 0.8730141131611886 - - - 0.48769494381363376 - - 0.8730141131611886 -- - - 0.4986376712409907 - - 0.8668105172523933 - - - 0.4986376712409907 - - 0.8668105172523933 -- - - 0.5095015002838723 - - 0.8604697677481082 - - - 0.5095015002838723 - - 0.8604697677481082 -- - - 0.520284711981578 - - 0.8539928679317214 - - - 0.520284711981578 - - 0.8539928679317214 -- - - 0.5309856001293194 - - 0.8473808426293968 - - - 0.5309856001293194 - - 0.8473808426293968 -- - - 0.5416024715481897 - - 0.8406347380479183 - - - 0.5416024715481897 - - 0.8406347380479183 -- - - 0.5521336463530699 - - 0.8337556216091518 - - - 0.5521336463530699 - - 0.8337556216091518 -- - - 0.5625774582184366 - - 0.8267445817811466 - - - 0.5625774582184366 - - 0.8267445817811466 -- - - 0.5729322546420195 - - 0.8196027279059118 - - - 0.5729322546420195 - - 0.8196027279059118 -- - - 0.5831963972062728 - - 0.8123311900238863 - - - 0.5831963972062728 - - 0.8123311900238863 -- - - 0.5933682618376198 - - 0.8049311186951352 - - - 0.5933682618376198 - - 0.8049311186951352 -- - - 0.6034462390634255 - - 0.7974036848172994 - - - 0.6034462390634255 - - 0.7974036848172994 -- - - 0.6134287342666611 - - 0.7897500794403265 - - - 0.6134287342666611 - - 0.7897500794403265 -- - - 0.6233141679382159 - - 0.7819715135780135 - - - 0.6233141679382159 - - 0.7819715135780135 -- - - 0.6331009759268206 - - 0.7740692180163913 - - - 0.6331009759268206 - - 0.7740692180163913 -- - - 0.6427876096865385 - - 0.7660444431189787 - - - 0.6427876096865385 - - 0.7660444431189787 -- - - 0.6523725365217901 - - 0.7578984586289417 - - - 0.6523725365217901 - - 0.7578984586289417 -- - - 0.6618542398298678 - - 0.7496325534681827 - - - 0.6618542398298678 - - 0.7496325534681827 -- - - 0.6712312193409025 - - 0.7412480355334005 - - - 0.6712312193409025 - - 0.7412480355334005 -- - - 0.6805019913552521 - - 0.7327462314891401 - - - 0.6805019913552521 - - 0.7327462314891401 -- - - 0.6896650889782615 - - 0.7241284865578805 - - - 0.6896650889782615 - - 0.7241284865578805 -- - - 0.698719062352367 - - 0.7153961643071823 - - - 0.698719062352367 - - 0.7153961643071823 -- - - 0.7076624788865039 - - 0.7065506464339328 - - - 0.7076624788865039 - - 0.7065506464339328 -- - - 0.7164939234827827 - - 0.6975933325457234 - - - 0.7164939234827827 - - 0.6975933325457234 -- - - 0.7252119987603968 - - 0.6885256399393928 - - - 0.7252119987603968 - - 0.6885256399393928 -- - - 0.7338153252767271 - - 0.6793490033767704 - - - 0.7338153252767271 - - 0.6793490033767704 -- - - 0.7423025417456087 - - 0.670064874857658 - - - 0.7423025417456087 - - 0.670064874857658 -- - - 0.7506723052527237 - - 0.6606747233900823 - - - 0.7506723052527237 - - 0.6606747233900823 -- - - 0.7589232914680881 - - 0.6511800347578566 - - - 0.7589232914680881 - - 0.6511800347578566 -- - - 0.767054194855598 - - 0.6415823112854891 - - - 0.767054194855598 - - 0.6415823112854891 -- - - 0.7750637288796014 - - 0.6318830716004724 - - - 0.7750637288796014 - - 0.6318830716004724 -- - - 0.7829506262084629 - - 0.6220838503929964 - - - 0.7829506262084629 - - 0.6220838503929964 -- - - 0.7907136389150935 - - 0.612186198173114 - - - 0.7907136389150935 - - 0.612186198173114 -- - - 0.7983515386744056 - - 0.60219168102541 - - - 0.7983515386744056 - - 0.60219168102541 -- - - 0.8058631169576688 - - 0.5921018803612016 - - - 0.8058631169576688 - - 0.5921018803612016 -- - - 0.8132471852237325 - - 0.5819183926683157 - - - 0.8132471852237325 - - 0.5819183926683157 -- - - 0.820502575107087 - - 0.5716428292584793 - - - 0.820502575107087 - - 0.5716428292584793 -- - - 0.8276281386027308 - - 0.5612768160123658 - - - 0.8276281386027308 - - 0.5612768160123658 -- - - 0.8346227482478168 - - 0.5508219931223347 - - - 0.8346227482478168 - - 0.5508219931223347 -- - - 0.8414852973000496 - - 0.5402800148329078 - - - 0.8414852973000496 - - 0.5402800148329078 -- - - 0.8482146999128017 - - 0.5296525491790214 - - - 0.8482146999128017 - - 0.5296525491790214 -- - - 0.8548098913069254 - - 0.5189412777220967 - - - 0.8548098913069254 - - 0.5189412777220967 -- - - 0.8612698279392301 - - 0.5081478952839703 - - - 0.8612698279392301 - - 0.5081478952839703 -- - - 0.8675934876676011 - - 0.49727410967872443 - - - 0.8675934876676011 - - 0.49727410967872443 -- - - 0.8737798699127283 - - 0.48632164144246715 - - - 0.8737798699127283 - - 0.48632164144246715 -- - - 0.8798279958164291 - - 0.4752922235610904 - - - 0.8798279958164291 - - 0.4752922235610904 -- - - 0.8857369083965291 - - 0.4641876011960666 - - - 0.8857369083965291 - - 0.4641876011960666 -- - - 0.8915056726982836 - - 0.4530095314083147 - - - 0.8915056726982836 - - 0.4530095314083147 -- - - 0.8971333759423138 - - 0.4417597828801838 - - - 0.8971333759423138 - - 0.4417597828801838 -- - - 0.9026191276690336 - - 0.43044013563559885 - - - 0.9026191276690336 - - 0.43044013563559885 -- - - 0.9079620598795458 - - 0.4190523807584107 - - - 0.9079620598795458 - - 0.4190523807584107 -- - - 0.9131613271729829 - - 0.4075983201089971 - - - 0.9131613271729829 - - 0.4075983201089971 -- - - 0.9182161068802737 - - 0.39607976603915773 - - - 0.9182161068802737 - - 0.39607976603915773 -- - - 0.9231255991943119 - - 0.3844985411053501 - - - 0.9231255991943119 - - 0.3844985411053501 -- - - 0.9278890272965089 - - 0.37285647778030967 - - - 0.9278890272965089 - - 0.37285647778030967 -- - - 0.932505637479707 - - 0.36115541816310226 - - - 0.932505637479707 - - 0.36115541816310226 -- - - 0.9369746992674379 - - 0.3493972136876513 - - - 0.9369746992674379 - - 0.3493972136876513 -- - - 0.9412955055295031 - - 0.3375837248297927 - - - 0.9412955055295031 - - 0.3375837248297927 -- - - 0.9454673725938633 - - 0.32571682081289105 - - - 0.9454673725938633 - - 0.32571682081289105 -- - - 0.9494896403548132 - - 0.3137983793120792 - - - 0.9494896403548132 - - 0.3137983793120792 -- - - 0.9533616723774291 - - 0.3018302861571574 - - - 0.9533616723774291 - - 0.3018302861571574 -- - - 0.9570828559982706 - - 0.2898144350342019 - - - 0.9570828559982706 - - 0.2898144350342019 -- - - 0.9606526024223209 - - 0.27775272718593136 - - - 0.9606526024223209 - - 0.27775272718593136 -- - - 0.9640703468161504 - - 0.26564707111087715 - - - 0.9640703468161504 - - 0.26564707111087715 -- - - 0.96733554839729 - - 0.25349938226140567 - - - 0.96733554839729 - - 0.25349938226140567 -- - - 0.9704476905197967 - - 0.24131158274064027 - - - 0.9704476905197967 - - 0.24131158274064027 -- - - 0.9734062807560024 - - 0.22908560099833106 - - - 0.9734062807560024 - - 0.22908560099833106 -- - - 0.9762108509744293 - - 0.21682337152572034 - - - 0.9762108509744293 - - 0.21682337152572034 -- - - 0.9788609574138614 - - 0.20452683454945125 - - - 0.9788609574138614 - - 0.20452683454945125 -- - - 0.9813561807535595 - - 0.1921979357245733 - - - 0.9813561807535595 - - 0.1921979357245733 -- - - 0.98369612617961 - - 0.17983862582668034 - - - 0.98369612617961 - - 0.17983862582668034 -- - - 0.9858804234473957 - - 0.1674508604432468 - - - 0.9858804234473957 - - 0.1674508604432468 -- - - 0.987908726940178 - - 0.15503659966419847 - - - 0.987908726940178 - - 0.15503659966419847 -- - - 0.9897807157237833 - - 0.14259780777177156 - - - 0.9897807157237833 - - 0.14259780777177156 -- - - 0.9914960935973847 - - 0.13013645292970846 - - - 0.9914960935973847 - - 0.13013645292970846 -- - - 0.9930545891403676 - - 0.11765450687183943 - - - 0.9930545891403676 - - 0.11765450687183943 -- - - 0.9944559557552775 - - 0.1051539445900992 - - - 0.9944559557552775 - - 0.1051539445900992 -- - - 0.9956999717068375 - - 0.09263674402202833 - - - 0.9956999717068375 - - 0.09263674402202833 -- - - 0.9967864401570342 - - 0.08010488573780816 - - - 0.9967864401570342 - - 0.08010488573780816 -- - - 0.9977151891962615 - - 0.06756035262687954 - - - 0.9977151891962615 - - 0.06756035262687954 -- - - 0.9984860718705224 - - 0.05500512958419429 - - - 0.9984860718705224 - - 0.05500512958419429 -- - - 0.9990989662046814 - - 0.042441203196148705 - - - 0.9990989662046814 - - 0.042441203196148705 -- - - 0.9995537752217638 - - 0.029870561426253633 - - - 0.9995537752217638 - - 0.029870561426253633 -- - - 0.9998504269583004 - - 0.01729519330057795 - - - 0.9998504269583004 - - 0.01729519330057795 -- - - 0.999988874475714 - - 0.004717088593032691 - - - 0.999988874475714 - - 0.004717088593032691 -- - - 0.999969095867747 - - -0.007861762489467534 - - - 0.999969095867747 - - -0.007861762489467534 -- - - 0.9997910942639262 - - -0.020439369621910786 - - - 0.9997910942639262 - - -0.020439369621910786 -- - - 0.9994548978290694 - - -0.03301374267611272 - - - 0.9994548978290694 - - -0.03301374267611272 -- - - 0.9989605597588275 - - -0.045582892035610355 - - - 0.9989605597588275 - - -0.045582892035610355 -- - - 0.9983081582712683 - - -0.058144828910474865 - - - 0.9983081582712683 - - -0.058144828910474865 -- - - 0.9974977965944998 - - -0.07069756565199363 - - - 0.9974977965944998 - - -0.07069756565199363 -- - - 0.9965296029503368 - - -0.08323911606717167 - - - 0.9965296029503368 - - -0.08323911606717167 -- - - 0.9954037305340127 - - -0.09576749573300279 - - - 0.9954037305340127 - - -0.09576749573300279 -- - - 0.9941203574899394 - - -0.1082807223104606 - - - 0.9941203574899394 - - -0.1082807223104606 -- - - 0.9926796868835203 - - -0.12077681585816072 - - - 0.9926796868835203 - - -0.12077681585816072 -- - - 0.9910819466690197 - - -0.1332537991456392 - - - 0.9910819466690197 - - -0.1332537991456392 -- - - 0.9893273896534936 - - -0.14570969796621086 - - - 0.9893273896534936 - - -0.14570969796621086 -- - - 0.9874162934567892 - - -0.1581425414493393 - - - 0.9874162934567892 - - -0.1581425414493393 -- - - 0.9853489604676167 - - -0.17055036237248902 - - - 0.9853489604676167 - - -0.17055036237248902 -- - - 0.9831257177957046 - - -0.18293119747238504 - - - 0.9831257177957046 - - -0.18293119747238504 -- - - 0.9807469172200398 - - -0.1952830877556692 - - - 0.9807469172200398 - - -0.1952830877556692 -- - - 0.9782129351332084 - - -0.2076040788088552 - - - 0.9782129351332084 - - -0.2076040788088552 -- - - 0.9755241724818389 - - -0.2198922211075767 - - - 0.9755241724818389 - - -0.2198922211075767 -- - - 0.9726810547031601 - - -0.23214557032506142 - - - 0.9726810547031601 - - -0.23214557032506142 -- - - 0.9696840316576879 - - -0.24436218763976586 - - - 0.9696840316576879 - - -0.24436218763976586 -- - - 0.9665335775580415 - - -0.25654014004216474 - - - 0.9665335775580415 - - -0.25654014004216474 -- - - 0.9632301908939129 - - -0.2686775006405933 - - - 0.9632301908939129 - - -0.2686775006405933 -- - - 0.9597743943531892 - - -0.2807723489661489 - - - 0.9597743943531892 - - -0.2807723489661489 -- - - 0.9561667347392514 - - -0.29282277127654904 - - - 0.9561667347392514 - - -0.29282277127654904 -- - - 0.9524077828844516 - - -0.3048268608589526 - - - 0.9524077828844516 - - -0.3048268608589526 -- - - 0.9484981335597957 - - -0.3167827183316413 - - - 0.9484981335597957 - - -0.3167827183316413 -- - - 0.9444384053808291 - - -0.32868845194456814 - - - 0.9444384053808291 - - -0.32868845194456814 -- - - 0.9402292407097596 - - -0.340542177878672 - - - 0.9402292407097596 - - -0.340542177878672 -- - - 0.9358713055538124 - - -0.3523420205439635 - - - 0.9358713055538124 - - -0.3523420205439635 -- - - 0.9313652894598542 - - -0.36408611287628373 - - - 0.9313652894598542 - - -0.36408611287628373 -- - - 0.9267119054052854 - - -0.37577259663273127 - - - 0.9267119054052854 - - -0.37577259663273127 -- - - 0.9219118896852252 - - -0.38739962268569283 - - - 0.9219118896852252 - - -0.38739962268569283 -- - - 0.9169660017960138 - - -0.3989653513154153 - - - 0.9169660017960138 - - -0.3989653513154153 -- - - 0.9118750243150339 - - -0.4104679525011135 - - - 0.9118750243150339 - - -0.4104679525011135 -- - - 0.9066397627768898 - - -0.4219056062105182 - - - 0.9066397627768898 - - -0.4219056062105182 -- - - 0.901261045545945 - - -0.4332765026878681 - - - 0.901261045545945 - - -0.4332765026878681 -- - - 0.895739723685256 - - -0.44457884274025133 - - - 0.895739723685256 - - -0.44457884274025133 -- - - 0.8900766708219062 - - -0.45581083802230066 - - - 0.8900766708219062 - - -0.45581083802230066 -- - - 0.8842727830087785 - - -0.46697071131914664 - - - 0.8842727830087785 - - -0.46697071131914664 -- - - 0.878328978582769 - - -0.47805669682763535 - - - 0.878328978582769 - - -0.47805669682763535 -- - - 0.8722461980194871 - - -0.48906704043571536 - - - 0.8722461980194871 - - -0.48906704043571536 -- - - 0.8660254037844392 - - -0.4999999999999992 - - - 0.8660254037844392 - - -0.4999999999999992 -- - - 0.8596675801807453 - - -0.5108538456214086 - - - 0.8596675801807453 - - -0.5108538456214086 -- - - 0.8531737331933934 - - -0.5216268599188969 - - - 0.8531737331933934 - - -0.5216268599188969 -- - - 0.8465448903300608 - - -0.5323173383011919 - - - 0.8465448903300608 - - -0.5323173383011919 -- - - 0.8397821004585404 - - -0.5429235892364983 - - - 0.8397821004585404 - - -0.5429235892364983 -- - - 0.8328864336407736 - - -0.5534439345201582 - - - 0.8328864336407736 - - -0.5534439345201582 -- - - 0.8258589809635439 - - -0.5638767095401768 - - - 0.8258589809635439 - - -0.5638767095401768 -- - - 0.8187008543658284 - - -0.5742202635406232 - - - 0.8187008543658284 - - -0.5742202635406232 -- - - 0.8114131864628666 - - -0.5844729598828138 - - - 0.8114131864628666 - - -0.5844729598828138 -- - - 0.803997130366941 - - -0.5946331763042861 - - - 0.803997130366941 - - -0.5946331763042861 -- - - 0.7964538595049301 - - -0.6046993051754741 - - - 0.7964538595049301 - - -0.6046993051754741 -- - - 0.7887845674326319 - - -0.6146697537540917 - - - 0.7887845674326319 - - -0.6146697537540917 -- - - 0.7809904676459185 - - -0.6245429444371375 - - - 0.7809904676459185 - - -0.6245429444371375 -- - - 0.7730727933887184 - - -0.6343173150105269 - - - 0.7730727933887184 - - -0.6343173150105269 -- - - 0.76503279745789 - - -0.6439913188962683 - - - 0.76503279745789 - - -0.6439913188962683 -- - - 0.7568717520049925 - - -0.6535634253971785 - - - 0.7568717520049925 - - -0.6535634253971785 -- - - 0.7485909483349908 - - -0.6630321199390865 - - - 0.7485909483349908 - - -0.6630321199390865 -- - - 0.7401916967019444 - - -0.6723959043104716 - - - 0.7401916967019444 - - -0.6723959043104716 -- - - 0.7316753261016786 - - -0.6816532968995326 - - - 0.7316753261016786 - - -0.6816532968995326 -- - - 0.7230431840615102 - - -0.69080283292861 - - - 0.7230431840615102 - - -0.69080283292861 -- - - 0.7142966364270213 - - -0.6998430646859649 - - - 0.7142966364270213 - - -0.6998430646859649 -- - - 0.7054370671459542 - - -0.7087725617548373 - - - 0.7054370671459542 - - -0.7087725617548373 -- - - 0.6964658780492222 - - -0.7175899112397874 - - - 0.6964658780492222 - - -0.7175899112397874 -- - - 0.6873844886291115 - - -0.7262937179902459 - - - 0.6873844886291115 - - -0.7262937179902459 -- - - 0.678194335814667 - - -0.7348826048212753 - - - 0.678194335814667 - - -0.7348826048212753 -- - - 0.6688968737443408 - - -0.7433552127314689 - - - 0.6688968737443408 - - -0.7433552127314689 -- - - 0.6594935735358967 - - -0.7517102011179926 - - - 0.6594935735358967 - - -0.7517102011179926 -- - - 0.6499859230536468 - - -0.7599462479886974 - - - 0.6499859230536468 - - -0.7599462479886974 -- - - 0.6403754266730268 - - -0.7680620501712988 - - - 0.6403754266730268 - - -0.7680620501712988 -- - - 0.6306636050425575 - - -0.7760563235195788 - - - 0.6306636050425575 - - -0.7760563235195788 -- - - 0.6208519948432446 - - -0.7839278031165648 - - - 0.6208519948432446 - - -0.7839278031165648 -- - - 0.6109421485454233 - - -0.7916752434746851 - - - 0.6109421485454233 - - -0.7916752434746851 -- - - 0.600935634163124 - - -0.7992974187328293 - - - 0.600935634163124 - - -0.7992974187328293 -- - - 0.5908340350059585 - - -0.8067931228503239 - - - 0.5908340350059585 - - -0.8067931228503239 -- - - 0.5806389494286068 - - -0.8141611697977519 - - - 0.5806389494286068 - - -0.8141611697977519 -- - - 0.570351990577902 - - -0.8214003937446248 - - - 0.570351990577902 - - -0.8214003937446248 -- - - 0.5599747861375968 - - -0.8285096492438412 - - - 0.5599747861375968 - - -0.8285096492438412 -- - - 0.5495089780708068 - - -0.8354878114129359 - - - 0.5495089780708068 - - -0.8354878114129359 -- - - 0.5389562223602165 - - -0.8423337761120617 - - - 0.5389562223602165 - - -0.8423337761120617 -- - - 0.5283181887460523 - - -0.8490464601186973 - - - 0.5283181887460523 - - -0.8490464601186973 -- - - 0.5175965604618786 - - -0.8556248012990465 - - - 0.5175965604618786 - - -0.8556248012990465 -- - - 0.5067930339682736 - - -0.8620677587760909 - - - 0.5067930339682736 - - -0.8620677587760909 -- - - 0.49590931868438975 - - -0.8683743130942925 - - - 0.49590931868438975 - - -0.8683743130942925 -- - - 0.4849471367174889 - - -0.8745434663808935 - - - 0.4849471367174889 - - -0.8745434663808935 -- - - 0.4739082225904436 - - -0.8805742425038144 - - - 0.4739082225904436 - - -0.8805742425038144 -- - - 0.4627943229673003 - - -0.886465687226098 - - - 0.4627943229673003 - - -0.886465687226098 -- - - 0.4516071963768956 - - -0.8922168683569035 - - - 0.4516071963768956 - - -0.8922168683569035 -- - - 0.44034861293462074 - - -0.8978268758989985 - - - 0.44034861293462074 - - -0.8978268758989985 -- - - 0.42902035406232714 - - -0.903294822192752 - - - 0.42902035406232714 - - -0.903294822192752 -- - - 0.4176242122064685 - - -0.9086198420565812 - - - 0.4176242122064685 - - -0.9086198420565812 -- - - 0.4061619905544733 - - -0.9138010929238529 - - - 0.4061619905544733 - - -0.9138010929238529 -- - - 0.3946355027494409 - - -0.918837754976196 - - - 0.3946355027494409 - - -0.918837754976196 -- - - 0.38304657260316866 - - -0.9237290312732221 - - - 0.38304657260316866 - - -0.9237290312732221 -- - - 0.37139703380756833 - - -0.9284741478786256 - - - 0.37139703380756833 - - -0.9284741478786256 -- - - 0.3596887296445368 - - -0.9330723539826369 - - - 0.3596887296445368 - - -0.9330723539826369 -- - - 0.34792351269428423 - - -0.9375229220208273 - - - 0.34792351269428423 - - -0.9375229220208273 -- - - 0.3361032445422173 - - -0.9418251477892244 - - - 0.3361032445422173 - - -0.9418251477892244 -- - - 0.3242297954843714 - - -0.9459783505557422 - - - 0.3242297954843714 - - -0.9459783505557422 -- - - 0.31230504423149086 - - -0.9499818731678866 - - - 0.31230504423149086 - - -0.9499818731678866 -- - - 0.3003308776117511 - - -0.9538350821567402 - - - 0.3003308776117511 - - -0.9538350821567402 -- - - 0.28830919027222335 - - -0.9575373678371905 - - - 0.28830919027222335 - - -0.9575373678371905 -- - - 0.27624188437907515 - - -0.9610881444044025 - - - 0.27624188437907515 - - -0.9610881444044025 -- - - 0.264130869316608 - - -0.9644868500265066 - - - 0.264130869316608 - - -0.9644868500265066 -- - - 0.2519780613851261 - - -0.9677329469334987 - - - 0.2519780613851261 - - -0.9677329469334987 -- - - 0.2397853834977361 - - -0.9708259215023276 - - - 0.2397853834977361 - - -0.9708259215023276 -- - - 0.22755476487608342 - - -0.9737652843381666 - - - 0.22755476487608342 - - -0.9737652843381666 -- - - 0.2152881407450906 - - -0.9765505703518492 - - - 0.2152881407450906 - - -0.9765505703518492 -- - - 0.20298745202676252 - - -0.9791813388334577 - - - 0.20298745202676252 - - -0.9791813388334577 -- - - 0.19065464503306495 - - -0.9816571735220581 - - - 0.19065464503306495 - - -0.9816571735220581 -- - - 0.17829167115797728 - - -0.9839776826715613 - - - 0.17829167115797728 - - -0.9839776826715613 -- - - 0.1659004865687139 - - -0.9861424991127113 - - - 0.1659004865687139 - - -0.9861424991127113 -- - - 0.15348305189621775 - - -0.9881512803111794 - - - 0.15348305189621775 - - -0.9881512803111794 -- - - 0.14104133192492 - - -0.9900037084217637 - - - 0.14104133192492 - - -0.9900037084217637 -- - - 0.12857729528187029 - - -0.9916994903386805 - - - 0.12857729528187029 - - -0.9916994903386805 -- - - 0.11609291412523105 - - -0.9932383577419429 - - - 0.11609291412523105 - - -0.9932383577419429 -- - - 0.10359016383224108 - - -0.9946200671398147 - - - 0.10359016383224108 - - -0.9946200671398147 -- - - 0.09107102268664179 - - -0.9958443999073395 - - - 0.09107102268664179 - - -0.9958443999073395 -- - - 0.07853747156566976 - - -0.996911162320932 - - - 0.07853747156566976 - - -0.996911162320932 -- - - 0.0659914936266216 - - -0.9978201855890306 - - - 0.0659914936266216 - - -0.9978201855890306 -- - - 0.05343507399305771 - - -0.9985713258788059 - - - 0.05343507399305771 - - -0.9985713258788059 -- - - 0.04087019944071283 - - -0.9991644643389177 - - - 0.04087019944071283 - - -0.9991644643389177 -- - - 0.028298858083118522 - - -0.9995995071183216 - - - 0.028298858083118522 - - -0.9995995071183216 -- - - 0.01572303905704239 - - -0.9998763853811183 - - - 0.01572303905704239 - - -0.9998763853811183 -- - - 0.003144732207736932 - - -0.9999950553174458 - - - 0.003144732207736932 - - -0.9999950553174458 -- - - -0.009434072225895224 - - -0.999955498150411 - - - -0.009434072225895224 - - -0.999955498150411 -- - - -0.02201138392622685 - - -0.9997577201390606 - - - -0.02201138392622685 - - -0.9997577201390606 -- - - -0.03458521281181564 - - -0.9994017525773914 - - - -0.03458521281181564 - - -0.9994017525773914 -- - - -0.04715356935230482 - - -0.9988876517893979 - - - -0.04715356935230482 - - -0.9988876517893979 -- - - -0.05971446488320808 - - -0.9982154991201609 - - - -0.05971446488320808 - - -0.9982154991201609 -- - - -0.07226591192058601 - - -0.9973854009229762 - - - -0.07226591192058601 - - -0.9973854009229762 -- - - -0.08480592447550901 - - -0.9963974885425265 - - - -0.08480592447550901 - - -0.9963974885425265 -- - - -0.0973325183683015 - - -0.9952519182940992 - - - -0.0973325183683015 - - -0.9952519182940992 -- - - -0.1098437115424997 - - -0.9939488714388522 - - - -0.1098437115424997 - - -0.9939488714388522 -- - - -0.12233752437845594 - - -0.9924885541551351 - - - -0.12233752437845594 - - -0.9924885541551351 -- - - -0.13481198000658376 - - -0.9908711975058637 - - - -0.13481198000658376 - - -0.9908711975058637 -- - - -0.14726510462013975 - - -0.9890970574019616 - - - -0.14726510462013975 - - -0.9890970574019616 -- - - -0.15969492778754882 - - -0.9871664145618658 - - - -0.15969492778754882 - - -0.9871664145618658 -- - - -0.17209948276416748 - - -0.9850795744671118 - - - -0.17209948276416748 - - -0.9850795744671118 -- - - -0.18447680680349163 - - -0.9828368673139949 - - - -0.18447680680349163 - - -0.9828368673139949 -- - - -0.19682494146770374 - - -0.9804386479613271 - - - -0.19682494146770374 - - -0.9804386479613271 -- - - -0.2091419329375665 - - -0.9778852958742853 - - - -0.2091419329375665 - - -0.9778852958742853 -- - - -0.22142583232155733 - - -0.9751772150643726 - - - -0.22142583232155733 - - -0.9751772150643726 -- - - -0.23367469596425144 - - -0.9723148340254892 - - - -0.23367469596425144 - - -0.9723148340254892 -- - - -0.24588658575385006 - - -0.9692986056661356 - - - -0.24588658575385006 - - -0.9692986056661356 -- - - -0.2580595694288491 - - -0.9661290072377483 - - - -0.2580595694288491 - - -0.9661290072377483 -- - - -0.2701917208837818 - - -0.9628065402591844 - - - -0.2701917208837818 - - -0.9628065402591844 -- - - -0.2822811204739704 - - -0.9593317304373705 - - - -0.2822811204739704 - - -0.9593317304373705 -- - - -0.29432585531928135 - - -0.9557051275841171 - - - -0.29432585531928135 - - -0.9557051275841171 -- - - -0.30632401960678207 - - -0.951927305529127 - - - -0.30632401960678207 - - -0.951927305529127 -- - - -0.31827371489230794 - - -0.9479988620291956 - - - -0.31827371489230794 - - -0.9479988620291956 -- - - -0.3301730504008353 - - -0.9439204186736335 - - - -0.3301730504008353 - - -0.9439204186736335 -- - - -0.342020143325668 - - -0.9396926207859086 - - - -0.342020143325668 - - -0.9396926207859086 -- - - -0.35381311912633706 - - -0.9353161373215435 - - - -0.35381311912633706 - - -0.9353161373215435 -- - - -0.3655501118252182 - - -0.9307916607622624 - - - -0.3655501118252182 - - -0.9307916607622624 -- - - -0.37722926430276815 - - -0.9261199070064267 - - - -0.37722926430276815 - - -0.9261199070064267 -- - - -0.3888487285913865 - - -0.9213016152557545 - - - -0.3888487285913865 - - -0.9213016152557545 -- - - -0.4004066661678036 - - -0.9163375478983632 - - - -0.4004066661678036 - - -0.9163375478983632 -- - - -0.4119012482439916 - - -0.9112284903881362 - - - -0.4119012482439916 - - -0.9112284903881362 -- - - -0.4233306560565341 - - -0.9059752511204401 - - - -0.4233306560565341 - - -0.9059752511204401 -- - - -0.4346930811543944 - - -0.9005786613042189 - - - -0.4346930811543944 - - -0.9005786613042189 -- - - -0.4459867256850755 - - -0.8950395748304681 - - - -0.4459867256850755 - - -0.8950395748304681 -- - - -0.4572098026790778 - - -0.8893588681371309 - - - -0.4572098026790778 - - -0.8893588681371309 -- - - -0.46836053633265995 - - -0.8835374400704156 - - - -0.46836053633265995 - - -0.8835374400704156 -- - - -0.47943716228880834 - - -0.8775762117425784 - - - -0.47943716228880834 - - -0.8775762117425784 -- - - -0.4904379279164198 - - -0.8714761263861728 - - - -0.4904379279164198 - - -0.8714761263861728 -- - - -0.5013610925876044 - - -0.8652381492048091 - - - -0.5013610925876044 - - -0.8652381492048091 -- - - -0.5122049279531135 - - -0.8588632672204265 - - - -0.5122049279531135 - - -0.8588632672204265 -- - - -0.5229677182158008 - - -0.852352489117125 - - - -0.5229677182158008 - - -0.852352489117125 -- - - -0.5336477604021214 - - -0.8457068450815567 - - - -0.5336477604021214 - - -0.8457068450815567 -- - - -0.5442433646315787 - - -0.8389273866399275 - - - -0.5442433646315787 - - -0.8389273866399275 -- - - -0.5547528543841161 - - -0.8320151864916143 - - - -0.5547528543841161 - - -0.8320151864916143 -- - - -0.5651745667653925 - - -0.8249713383394304 - - - -0.5651745667653925 - - -0.8249713383394304 -- - - -0.5755068527698889 - - -0.8177969567165786 - - - -0.5755068527698889 - - -0.8177969567165786 -- - - -0.5857480775418389 - - -0.8104931768102923 - - - -0.5857480775418389 - - -0.8104931768102923 -- - - -0.5958966206338965 - - -0.8030611542822266 - - - -0.5958966206338965 - - -0.8030611542822266 -- - - -0.6059508762635476 - - -0.7955020650855904 - - - -0.6059508762635476 - - -0.7955020650855904 -- - - -0.6159092535671783 - - -0.7878171052790878 - - - -0.6159092535671783 - - -0.7878171052790878 -- - - -0.6257701768518052 - - -0.7800074908376589 - - - -0.6257701768518052 - - -0.7800074908376589 -- - - -0.6355320858443827 - - -0.7720744574600873 - - - -0.6355320858443827 - - -0.7720744574600873 -- - - -0.6451934359386927 - - -0.76401926037347 - - - -0.6451934359386927 - - -0.76401926037347 -- - - -0.6547526984397336 - - -0.7558431741346133 - - - -0.6547526984397336 - - -0.7558431741346133 -- - - -0.6642083608056132 - - -0.7475474924283543 - - - -0.6642083608056132 - - -0.7475474924283543 -- - - -0.6735589268868657 - - -0.7391335278628713 - - - -0.6735589268868657 - - -0.7391335278628713 -- - - -0.6828029171631881 - - -0.7306026117619896 - - - -0.6828029171631881 - - -0.7306026117619896 -- - - -0.6919388689775459 - - -0.7219560939545248 - - - -0.6919388689775459 - - -0.7219560939545248 -- - - -0.7009653367675964 - - -0.7131953425607112 - - - -0.7009653367675964 - - -0.7131953425607112 -- - - -0.7098808922944282 - - -0.7043217437757168 - - - -0.7098808922944282 - - -0.7043217437757168 -- - - -0.7186841248685372 - - -0.695336701650319 - - - -0.7186841248685372 - - -0.695336701650319 -- - - -0.7273736415730482 - - -0.6862416378687342 - - - -0.7273736415730482 - - -0.6862416378687342 -- - - -0.7359480674841022 - - -0.6770379915236775 - - - -0.7359480674841022 - - -0.6770379915236775 -- - - -0.7444060458884184 - - -0.6677272188886492 - - - -0.7444060458884184 - - -0.6677272188886492 -- - - -0.7527462384979536 - - -0.6583107931875202 - - - -0.7527462384979536 - - -0.6583107931875202 -- - - -0.7609673256616669 - - -0.648790204361418 - - - -0.7609673256616669 - - -0.648790204361418 -- - - -0.7690680065743155 - - -0.6391669588329865 - - - -0.7690680065743155 - - -0.6391669588329865 -- - - -0.7770469994822877 - - -0.6294425792680167 - - - -0.7770469994822877 - - -0.6294425792680167 -- - - -0.7849030418864043 - - -0.619618604334529 - - - -0.7849030418864043 - - -0.619618604334529 -- - - -0.7926348907416839 - - -0.609696588459308 - - - -0.7926348907416839 - - -0.609696588459308 -- - - -0.8002413226540318 - - -0.5996781015819452 - - - -0.8002413226540318 - - -0.5996781015819452 -- - - -0.807721134073806 - - -0.5895647289064406 - - - -0.807721134073806 - - -0.5895647289064406 -- - - -0.8150731414862619 - - -0.5793580706503675 - - - -0.8150731414862619 - - -0.5793580706503675 -- - - -0.8222961815988086 - - -0.5690597417916851 - - - -0.8222961815988086 - - -0.5690597417916851 -- - - -0.8293891115250823 - - -0.5586713718131927 - - - -0.8293891115250823 - - -0.5586713718131927 -- - - -0.8363508089657752 - - -0.5481946044447112 - - - -0.8363508089657752 - - -0.5481946044447112 -- - - -0.8431801723862219 - - -0.537631097402988 - - - -0.8431801723862219 - - -0.537631097402988 -- - - -0.8498761211906855 - - -0.5269825221294112 - - - -0.8498761211906855 - - -0.5269825221294112 -- - - -0.8564375958933453 - - -0.5162505635255297 - - - -0.8564375958933453 - - -0.5162505635255297 -- - - -0.8628635582859301 - - -0.5054369196864662 - - - -0.8628635582859301 - - -0.5054369196864662 -- - - -0.8691529916019983 - - -0.49454330163221977 - - - -0.8691529916019983 - - -0.49454330163221977 -- - - -0.8753049006778127 - - -0.4835714330369447 - - - -0.8753049006778127 - - -0.4835714330369447 -- - - -0.8813183121098064 - - -0.4725230499562131 - - - -0.8813183121098064 - - -0.4725230499562131 -- - - -0.8871922744086038 - - -0.46139990055231767 - - - -0.8871922744086038 - - -0.46139990055231767 -- - - -0.8929258581495678 - - -0.4502037448176746 - - - -0.8929258581495678 - - -0.4502037448176746 -- - - -0.898518156119867 - - -0.43893635429633115 - - - -0.898518156119867 - - -0.43893635429633115 -- - - -0.9039682834620154 - - -0.42759951180367056 - - - -0.9039682834620154 - - -0.42759951180367056 -- - - -0.9092753778138881 - - -0.4161950111443084 - - - -0.9092753778138881 - - -0.4161950111443084 -- - - -0.914438599445165 - - -0.40472465682827513 - - - -0.914438599445165 - - -0.40472465682827513 -- - - -0.919457131390205 - - -0.39319026378547983 - - - -0.919457131390205 - - -0.39319026378547983 -- - - -0.9243301795773077 - - -0.38159365707855025 - - - -0.9243301795773077 - - -0.38159365707855025 -- - - -0.9290569729543624 - - -0.36993667161404425 - - - -0.9290569729543624 - - -0.36993667161404425 -- - - -0.9336367636108461 - - -0.3582211518521277 - - - -0.9336367636108461 - - -0.3582211518521277 -- - - -0.9380688268961654 - - -0.34644895151472466 - - - -0.9380688268961654 - - -0.34644895151472466 -- - - -0.9423524615343185 - - -0.3346219332922018 - - - -0.9423524615343185 - - -0.3346219332922018 -- - - -0.946486989734852 - - -0.32274196854865056 - - - -0.946486989734852 - - -0.32274196854865056 -- - - -0.9504717573001114 - - -0.31081093702577167 - - - -0.9504717573001114 - - -0.31081093702577167 -- - - -0.9543061337287484 - - -0.2988307265454612 - - - -0.9543061337287484 - - -0.2988307265454612 -- - - -0.9579895123154887 - - -0.2868032327110909 - - - -0.9579895123154887 - - -0.2868032327110909 -- - - -0.9615213102471251 - - -0.27473035860758444 - - - -0.9615213102471251 - - -0.27473035860758444 -- - - -0.9649009686947388 - - -0.2626140145002827 - - - -0.9649009686947388 - - -0.2626140145002827 -- - - -0.9681279529021183 - - -0.25045611753270025 - - - -0.9681279529021183 - - -0.25045611753270025 -- - - -0.9712017522703761 - - -0.23825859142316594 - - - -0.9712017522703761 - - -0.23825859142316594 -- - - -0.9741218804387358 - - -0.22602336616045093 - - - -0.9741218804387358 - - -0.22602336616045093 -- - - -0.9768878753614922 - - -0.21375237769837674 - - - -0.9768878753614922 - - -0.21375237769837674 -- - - -0.9794992993811164 - - -0.2014475676495055 - - - -0.9794992993811164 - - -0.2014475676495055 -- - - -0.9819557392975065 - - -0.18911088297791753 - - - -0.9819557392975065 - - -0.18911088297791753 -- - - -0.9842568064333685 - - -0.17674427569114207 - - - -0.9842568064333685 - - -0.17674427569114207 -- - - -0.9864021366957143 - - -0.1643497025313075 - - - -0.9864021366957143 - - -0.1643497025313075 -- - - -0.9883913906334727 - - -0.1519291246655162 - - - -0.9883913906334727 - - -0.1519291246655162 -- - - -0.9902242534911982 - - -0.1394845073755471 - - - -0.9902242534911982 - - -0.1394845073755471 -- - - -0.9919004352588768 - - -0.12701781974687945 - - - -0.9919004352588768 - - -0.12701781974687945 -- - - -0.9934196707178105 - - -0.11453103435714257 - - - -0.9934196707178105 - - -0.11453103435714257 -- - - -0.9947817194825852 - - -0.10202612696398496 - - - -0.9947817194825852 - - -0.10202612696398496 -- - - -0.9959863660391042 - - -0.08950507619246842 - - - -0.9959863660391042 - - -0.08950507619246842 -- - - -0.9970334197786901 - - -0.07696986322198038 - - - -0.9970334197786901 - - -0.07696986322198038 -- - - -0.9979227150282431 - - -0.0644224714727701 - - - -0.9979227150282431 - - -0.0644224714727701 -- - - -0.9986541110764564 - - -0.051864886292102175 - - - -0.9986541110764564 - - -0.051864886292102175 -- - - -0.9992274921960794 - - -0.03929909464013164 - - - -0.9992274921960794 - - -0.03929909464013164 -- - - -0.9996427676622299 - - -0.026727084775506123 - - - -0.9996427676622299 - - -0.026727084775506123 -- - - -0.9998998717667489 - - -0.014150845940762564 - - - -0.9998998717667489 - - -0.014150845940762564 -- - - -0.9999987638285974 - - -0.001572368047586014 - - - -0.9999987638285974 - - -0.001572368047586014 -- - - -0.9999394282002937 - - 0.0110063586380641 - - - -0.9999394282002937 - - 0.0110063586380641 -- - - -0.9997218742703887 - - 0.02358334381085534 - - - -0.9997218742703887 - - 0.02358334381085534 -- - - -0.9993461364619809 - - 0.036156597441018276 - - - -0.9993461364619809 - - 0.036156597441018276 -- - - -0.9988122742272693 - - 0.04872413008921046 - - - -0.9988122742272693 - - 0.04872413008921046 -- - - -0.9981203720381463 - - 0.06128395322131545 - - - -0.9981203720381463 - - 0.06128395322131545 -- - - -0.9972705393728328 - - 0.0738340795230701 - - - -0.9972705393728328 - - 0.0738340795230701 -- - - -0.9962629106985544 - - 0.08637252321452737 - - - -0.9962629106985544 - - 0.08637252321452737 -- - - -0.9950976454502662 - - 0.09889730036424782 - - - -0.9950976454502662 - - 0.09889730036424782 -- - - -0.9937749280054243 - - 0.11140642920322712 - - - -0.9937749280054243 - - 0.11140642920322712 -- - - -0.9922949676548137 - - 0.12389793043845473 - - - -0.9922949676548137 - - 0.12389793043845473 -- - - -0.9906579985694319 - - 0.1363698275660986 - - - -0.9906579985694319 - - 0.1363698275660986 -- - - -0.9888642797634358 - - 0.14882014718424852 - - - -0.9888642797634358 - - 0.14882014718424852 -- - - -0.9869140950531602 - - 0.16124691930515087 - - - -0.9869140950531602 - - 0.16124691930515087 -- - - -0.9848077530122081 - - 0.17364817766692972 - - - -0.9848077530122081 - - 0.17364817766692972 -- - - -0.9825455869226281 - - 0.18602196004469043 - - - -0.9825455869226281 - - 0.18602196004469043 -- - - -0.9801279547221767 - - 0.19836630856101212 - - - -0.9801279547221767 - - 0.19836630856101212 -- - - -0.9775552389476866 - - 0.21067926999572462 - - - -0.9775552389476866 - - 0.21067926999572462 -- - - -0.9748278466745344 - - 0.2229588960949763 - - - -0.9748278466745344 - - 0.2229588960949763 -- - - -0.9719462094522341 - - 0.23520324387948816 - - - -0.9719462094522341 - - 0.23520324387948816 -- - - -0.9689107832361499 - - 0.24741037595200138 - - - -0.9689107832361499 - - 0.24741037595200138 -- - - -0.9657220483153551 - - 0.25957836080381363 - - - -0.9657220483153551 - - 0.25957836080381363 -- - - -0.9623805092366339 - - 0.27170527312041143 - - - -0.9623805092366339 - - 0.27170527312041143 -- - - -0.9588866947246498 - - 0.2837891940860965 - - - -0.9588866947246498 - - 0.2837891940860965 -- - - -0.9552411575982872 - - 0.29582821168760115 - - - -0.9552411575982872 - - 0.29582821168760115 -- - - -0.9514444746831768 - - 0.30782042101662727 - - - -0.9514444746831768 - - 0.30782042101662727 -- - - -0.9474972467204302 - - 0.31976392457124386 - - - -0.9474972467204302 - - 0.31976392457124386 -- - - -0.9434000982715814 - - 0.3316568325561384 - - - -0.9434000982715814 - - 0.3316568325561384 -- - - -0.9391536776197683 - - 0.3434972631816217 - - - -0.9391536776197683 - - 0.3434972631816217 -- - - -0.9347586566671513 - - 0.35528334296139286 - - - -0.9347586566671513 - - 0.35528334296139286 -- - - -0.9302157308286049 - - 0.3670132070089637 - - - -0.9302157308286049 - - 0.3670132070089637 -- - - -0.9255256189216783 - - 0.3786849993327492 - - - -0.9255256189216783 - - 0.3786849993327492 -- - - -0.9206890630528639 - - 0.3902968731297237 - - - -0.9206890630528639 - - 0.3902968731297237 -- - - -0.9157068285001696 - - 0.40184699107765015 - - - -0.9157068285001696 - - 0.40184699107765015 -- - - -0.9105797035920364 - - 0.41333352562578207 - - - -0.9105797035920364 - - 0.41333352562578207 -- - - -0.9053084995825972 - - 0.4247546592840467 - - - -0.9053084995825972 - - 0.4247546592840467 -- - - -0.8998940505233184 - - 0.4361085849106107 - - - -0.8998940505233184 - - 0.4361085849106107 -- - - -0.8943372131310279 - - 0.4473935059978257 - - - -0.8943372131310279 - - 0.4473935059978257 -- - - -0.8886388666523561 - - 0.45860763695649037 - - - -0.8886388666523561 - - 0.45860763695649037 -- - - -0.8827999127246203 - - 0.4697492033983695 - - - -0.8827999127246203 - - 0.4697492033983695 -- - - -0.8768212752331539 - - 0.48081644241696414 - - - -0.8768212752331539 - - 0.48081644241696414 -- - - -0.8707039001651283 - - 0.49180760286644026 - - - -0.8707039001651283 - - 0.49180760286644026 -- - - -0.8644487554598653 - - 0.502720945638721 - - - -0.8644487554598653 - - 0.502720945638721 -- - - -0.8580568308556884 - - 0.5135547439386501 - - - -0.8580568308556884 - - 0.5135547439386501 -- - - -0.8515291377333118 - - 0.5243072835572309 - - - -0.8515291377333118 - - 0.5243072835572309 -- - - -0.8448667089558188 - - 0.53497686314285 - - - -0.8448667089558188 - - 0.53497686314285 -- - - -0.838070598705227 - - 0.5455617944704909 - - - -0.838070598705227 - - 0.5455617944704909 -- - - -0.8311418823156947 - - 0.5560604027088458 - - - -0.8311418823156947 - - 0.5560604027088458 -- - - -0.8240816561033651 - - 0.5664710266853329 - - - -0.8240816561033651 - - 0.5664710266853329 -- - - -0.8168910371929057 - - 0.5767920191489293 - - - -0.8168910371929057 - - 0.5767920191489293 -- - - -0.8095711633407447 - - 0.5870217470308176 - - - -0.8095711633407447 - - 0.5870217470308176 -- - - -0.8021231927550442 - - 0.5971585917027857 - - - -0.8021231927550442 - - 0.5971585917027857 -- - - -0.7945483039124446 - - 0.6072009492333305 - - - -0.7945483039124446 - - 0.6072009492333305 -- - - -0.7868476953715905 - - 0.6171472306414546 - - - -0.7868476953715905 - - 0.6171472306414546 -- - - -0.7790225855834922 - - 0.6269958621480771 - - - -0.7790225855834922 - - 0.6269958621480771 -- - - -0.7710742126987252 - - 0.6367452854250599 - - - -0.7710742126987252 - - 0.6367452854250599 -- - - -0.7630038343715285 - - 0.6463939578417678 - - - -0.7630038343715285 - - 0.6463939578417678 -- - - -0.7548127275607995 - - 0.6559403527091668 - - - -0.7548127275607995 - - 0.6559403527091668 -- - - -0.7465021883280534 - - 0.6653829595213779 - - - -0.7465021883280534 - - 0.6653829595213779 -- - - -0.7380735316323398 - - 0.6747202841946918 - - - -0.7380735316323398 - - 0.6747202841946918 -- - - -0.7295280911221899 - - 0.6839508493039641 - - - -0.7295280911221899 - - 0.6839508493039641 -- - - -0.7208672189245859 - - 0.6930731943163961 - - - -0.7208672189245859 - - 0.6930731943163961 -- - - -0.7120922854310258 - - 0.7020858758226223 - - - -0.7120922854310258 - - 0.7020858758226223 -- - - -0.703204679080685 - - 0.7109874677651012 - - - -0.703204679080685 - - 0.7109874677651012 -- - - -0.694205806140723 - - 0.719776561663763 - - - -0.694205806140723 - - 0.719776561663763 -- - - -0.685097090483782 - - 0.7284517668388598 - - - -0.685097090483782 - - 0.7284517668388598 -- - - -0.6758799733626797 - - 0.7370117106310208 - - - -0.6758799733626797 - - 0.7370117106310208 -- - - -0.6665559131823733 - - 0.745455038618435 - - - -0.6665559131823733 - - 0.745455038618435 -- - - -0.6571263852691893 - - 0.7537804148311689 - - - -0.6571263852691893 - - 0.7537804148311689 -- - - -0.6475928816373955 - - 0.7619865219625438 - - - -0.6475928816373955 - - 0.7619865219625438 -- - - -0.6379569107531127 - - 0.7700720615775806 - - - -0.6379569107531127 - - 0.7700720615775806 -- - - -0.6282199972956439 - - 0.7780357543184383 - - - -0.6282199972956439 - - 0.7780357543184383 -- - - -0.6183836819162163 - - 0.7858763401068541 - - - -0.6183836819162163 - - 0.7858763401068541 -- - - -0.6084495209942188 - - 0.7935925783435136 - - - -0.6084495209942188 - - 0.7935925783435136 -- - - -0.5984190863909279 - - 0.8011832481043567 - - - -0.5984190863909279 - - 0.8011832481043567 -- - - -0.5882939652008056 - - 0.8086471483337546 - - - -0.5882939652008056 - - 0.8086471483337546 -- - - -0.5780757595003719 - - 0.8159830980345537 - - - -0.5780757595003719 - - 0.8159830980345537 -- - - -0.5677660860947084 - - 0.8231899364549449 - - - -0.5677660860947084 - - 0.8231899364549449 -- - - -0.5573665762616435 - - 0.8302665232721198 - - - -0.5573665762616435 - - 0.8302665232721198 -- - - -0.546878875493628 - - 0.8372117387727103 - - - -0.546878875493628 - - 0.8372117387727103 -- - - -0.5363046432373839 - - 0.8440244840299495 - - - -0.5363046432373839 - - 0.8440244840299495 -- - - -0.5256455526313215 - - 0.850703681077561 - - - -0.5256455526313215 - - 0.850703681077561 -- - - -0.5149032902408143 - - 0.8572482730803158 - - - -0.5149032902408143 - - 0.8572482730803158 -- - - -0.5040795557913256 - - 0.86365722450126 - - - -0.5040795557913256 - - 0.86365722450126 -- - - -0.49317606189947616 - - 0.8699295212655587 - - - -0.49317606189947616 - - 0.8699295212655587 -- - - -0.4821945338020488 - - 0.8760641709209576 - - - -0.4821945338020488 - - 0.8760641709209576 -- - - -0.4711367090830182 - - 0.8820602027948112 - - - -0.4711367090830182 - - 0.8820602027948112 -- - - -0.46000433739861224 - - 0.8879166681476723 - - - -0.46000433739861224 - - 0.8879166681476723 -- - - -0.44879918020046267 - - 0.893632640323412 - - - -0.44879918020046267 - - 0.893632640323412 -- - - -0.43752301045690567 - - 0.8992072148958361 - - - -0.43752301045690567 - - 0.8992072148958361 -- - - -0.4261776123724359 - - 0.9046395098117977 - - - -0.4261776123724359 - - 0.9046395098117977 -- - - -0.4147647811054085 - - 0.909928665530756 - - - -0.4147647811054085 - - 0.909928665530756 -- - - -0.403286322483982 - - 0.9150738451607857 - - - -0.403286322483982 - - 0.9150738451607857 -- - - -0.39174405272039897 - - 0.9200742345909907 - - - -0.39174405272039897 - - 0.9200742345909907 -- - - -0.3801397981235976 - - 0.9249290426203247 - - - -0.3801397981235976 - - 0.9249290426203247 -- - - -0.3684753948102517 - - 0.9296375010827764 - - - -0.3684753948102517 - - 0.9296375010827764 -- - - -0.3567526884142328 - - 0.9341988649689195 - - - -0.3567526884142328 - - 0.9341988649689195 -- - - -0.34497353379459245 - - 0.9386124125437886 - - - -0.34497353379459245 - - 0.9386124125437886 -- - - -0.33313979474205874 - - 0.9428774454610838 - - - -0.33313979474205874 - - 0.9428774454610838 -- - - -0.3212533436841441 - - 0.9469932888736632 - - - -0.3212533436841441 - - 0.9469932888736632 -- - - -0.30931606138887024 - - 0.9509592915403249 - - - -0.30931606138887024 - - 0.9509592915403249 -- - - -0.2973298366671729 - - 0.9547748259288534 - - - -0.2973298366671729 - - 0.9547748259288534 -- - - -0.28529656607405124 - - 0.9584392883153082 - - - -0.28529656607405124 - - 0.9584392883153082 -- - - -0.2732181536084666 - - 0.9619520988795546 - - - -0.2732181536084666 - - 0.9619520988795546 -- - - -0.26109651041208987 - - 0.9653127017970029 - - - -0.26109651041208987 - - 0.9653127017970029 -- - - -0.24893355446689247 - - 0.9685205653265596 - - - -0.24893355446689247 - - 0.9685205653265596 -- - - -0.2367312102916815 - - 0.9715751818947599 - - - -0.2367312102916815 - - 0.9715751818947599 -- - - -0.22449140863757358 - - 0.974476068176083 - - - -0.22449140863757358 - - 0.974476068176083 -- - - -0.2122160861825098 - - 0.9772227651694252 - - - -0.2122160861825098 - - 0.9772227651694252 -- - - -0.19990718522480572 - - 0.9798148382707292 - - - -0.19990718522480572 - - 0.9798148382707292 -- - - -0.1875666533758392 - - 0.9822518773417477 - - - -0.1875666533758392 - - 0.9822518773417477 -- - - -0.17519644325187023 - - 0.9845334967749417 - - - -0.17519644325187023 - - 0.9845334967749417 -- - - -0.16279851216509478 - - 0.9866593355544919 - - - -0.16279851216509478 - - 0.9866593355544919 -- - - -0.1503748218139381 - - 0.9886290573134224 - - - -0.1503748218139381 - - 0.9886290573134224 -- - - -0.1379273379726542 - - 0.9904423503868245 - - - -0.1379273379726542 - - 0.9904423503868245 -- - - -0.12545803018029758 - - 0.9920989278611683 - - - -0.12545803018029758 - - 0.9920989278611683 -- - - -0.11296887142907358 - - 0.9935985276197029 - - - -0.11296887142907358 - - 0.9935985276197029 -- - - -0.10046183785216964 - - 0.9949409123839287 - - - -0.10046183785216964 - - 0.9949409123839287 -- - - -0.08793890841106214 - - 0.9961258697511428 - - - -0.08793890841106214 - - 0.9961258697511428 -- - - -0.07540206458240344 - - 0.9971532122280462 - - - -0.07540206458240344 - - 0.9971532122280462 -- - - -0.06285329004448297 - - 0.9980227772604111 - - - -0.06285329004448297 - - 0.9980227772604111 -- - - -0.05029457036336817 - - 0.9987344272588005 - - - -0.05029457036336817 - - 0.9987344272588005 -- - - -0.037727892678718344 - - 0.99928804962034 - - - -0.037727892678718344 - - 0.99928804962034 -- - - -0.025155245389377974 - - 0.9996835567465338 - - - -0.025155245389377974 - - 0.9996835567465338 -- - - -0.012578617838742366 - - 0.9999208860571255 - - - -0.012578617838742366 - - 0.9999208860571255 -- - - -4.898587196589413e-16 - - 1.0 - - - -4.898587196589413e-16 - - 1.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 2.5 + - - 2.5 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 +- - - 0.0 + - - 0.0 init_spikes: - - 0.0 - 0.0 diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 3c48a6a0..237d5328 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -214,19 +214,25 @@ # %% -# ## Recurrently Coupled GLM. -# Defining a recurrent model follows the same syntax. Here -# we will import some configuration parameters to simplify the data generation process. +# ## Recurrently Coupled GLM +# Defining a recurrent model follows the same syntax. In this example +# we will simulate two coupled neurons. and we will inject a transient +# input driving the rate of one of the neurons. +# +# For brevity, we will import the model parameters instead of generating +# them on the fly. # load parameters with open("coupled_neurons_params.yml", "r") as fh: config_dict = yaml.safe_load(fh) # basis weights & intercept for the GLM (both coupling and feedforward) +# (the last coefficient is the weight of the feedforward input) basis_coeff = np.asarray(config_dict["basis_coeff_"])[:, :-1] -# Only neuron 1 gets +# Mask the weights so that only the first neuron receives the imput basis_coeff[:, 40:] = np.abs(basis_coeff[:, 40:]) * np.array([[1.], [0.]]) + baseline_log_fr = np.asarray(config_dict["baseline_link_fr_"]) # basis function, inputs and initial spikes @@ -234,30 +240,46 @@ feedforward_input = jax.numpy.asarray(config_dict["feedforward_input"]) init_spikes = jax.numpy.asarray(config_dict["init_spikes"]) +# %% +# We can explore visualize the coupling filters and the input. + +# plot coupling functions +n_basis_coupling = coupling_basis.shape[1] +fig, axs = plt.subplots(2,2) +plt.suptitle("Coupling filters") +for neu_i in range(2): + for neu_j in range(2): + axs[neu_i,neu_j].set_title(f"neu {neu_j} -> neu {neu_i}") + coeff = basis_coeff[neu_i, neu_j*n_basis_coupling: (neu_j+1)*n_basis_coupling] + axs[neu_i, neu_j].plot(np.dot(coupling_basis, coeff)) +plt.tight_layout() + +fig, axs = plt.subplots(1,1) +plt.title("Feedforward inputs") +plt.plot(feedforward_input[:, 0]) -####################### -# test stim -####################### +# %% +# We can now simulate spikes by calling the `simulate` method. model = nsl.glm.GLMRecurrent() model.basis_coeff_ = jax.numpy.asarray(basis_coeff) -model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr -1) +model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr) -stim_step = np.zeros((1000, 2, 1)) -stim_step[200:500] = 2.5 - # call simulate, with both the recurrent coupling # and the input spikes, rates = model.simulate( random_key, - feedforward_input=stim_step, + feedforward_input=feedforward_input, coupling_basis_matrix=coupling_basis, init_y=init_spikes ) +# %% +# And finally plot the results for both neurons. +# mkdocs_gallery_thumbnail_number = 4 plt.figure() ax = plt.subplot(111) @@ -271,69 +293,10 @@ plt.vlines(np.where(spikes[:, 0])[0], 0.00, 0.01, color=p0.get_color(), label="neu 0") plt.vlines(np.where(spikes[:, 1])[0], -0.01, 0.00, color=p1.get_color(), label="neu 1") -plt.plot(np.exp(basis_coeff[0,-1] * stim_step[:, 0, 0] + baseline_log_fr[0]-1), color='k', lw=0.8, label="stimulus") +plt.plot(np.exp(basis_coeff[0, -1] * feedforward_input[:, 0, 0] + baseline_log_fr[0]), color='k', lw=0.8, label="stimulus") ax.add_patch(patch) plt.ylim(-0.011, .13) +plt.ylabel("count/bin") plt.legend() -model.set_params(noise_model__inverse_link_function=jax.nn.softplus) -spikes_sp, rates_sp = model.simulate( - random_key, - feedforward_input=stim_step, - coupling_basis_matrix=coupling_basis, - init_y=init_spikes -) - -linkr = basis_coeff[0,-1] * stim_step[:, 0, 0] + baseline_log_fr[0]-1 - -plt.figure() -plt.plot(rates[:, 0]) -plt.plot(rates_sp[:, 0]) - - -# # plot coupling functions -# n_basis_coupling = coupling_basis.shape[1] -# fig, axs = plt.subplots(2,2) -# plt.suptitle("Coupling filters") -# for neu_i in range(2): -# for neu_j in range(2): -# axs[neu_i,neu_j].set_title(f"neu {neu_j} -> neu {neu_i}") -# coeff = basis_coeff[neu_i, neu_j*n_basis_coupling: (neu_j+1)*n_basis_coupling] -# axs[neu_i, neu_j].plot(np.dot(coupling_basis, coeff)) -# plt.tight_layout() -# -# fig, axs = plt.subplots(1,1) -# plt.title("Feedforward inputs") -# plt.plot(feedforward_input[:, 0]) -# -# # %% -# # We can now generate spikes from the model by defining a Recurrent GLM, -# # setting the parameters and calling the simulate method -# -# # define the model and set the parameters -# model = nsl.glm.GLMRecurrent() -# model.basis_coeff_ = jax.numpy.asarray(basis_coeff) -# model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr) -# -# # call simulate, with both the recurrent coupling -# # and the input -# spikes, rates = model.simulate( -# random_key, -# feedforward_input=feedforward_input, -# coupling_basis_matrix=coupling_basis, -# init_y=init_spikes -# ) -# -# plt.figure() -# plt.subplot(121) -# plt.title("Rate") -# plt.plot(rates[:, 0], label="neu 0") -# plt.plot(rates[:, 1], label="neu 1") -# -# plt.subplot(122) -# plt.title("Spikes") -# spike_ind, spike_neu = np.where(spikes) -# plt.eventplot([spike_ind[spike_neu == 0], spike_ind[spike_neu == 1]]) -# plt.yticks([0, 1], ["neu 0", "neu 1"]) -# From 93c3cb6e85f5a031070822cdd014dfbb78cb114a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 13 Sep 2023 16:35:09 -0400 Subject: [PATCH 070/250] refractor of test to be continued --- tests/test_glm.py | 295 +++++++++++++++++++++++----------------------- 1 file changed, 147 insertions(+), 148 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index d3b9e288..b87f9c92 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -209,52 +209,52 @@ def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, error, match_str init_b = jnp.zeros((n_neurons + delta_n_neuron,)) _test_class_method(model, "fit", [X, y], {"init_params": (true_params[0], init_b)}, error, match_str) - - - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_fit_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "The number of neuron in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neuron in the model parameters") + ] + ) + def test_fit_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method ensuring the number of neurons in X matches the number of neurons in the model. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) + n_neurons = X.shape[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_fit_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "The number of neuron in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neuron in the model parameters") + ] + ) + def test_fit_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method ensuring the number of neurons in y matches the number of neurons in the model. """ - raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) + n_neurons = X.shape[1] y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_fit_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_dim, error, match_str", + [ + (-1, ValueError, "X must be three-dimensional"), + (0, None, None), + (1, ValueError, "X must be three-dimensional") + ] + ) + def test_fit_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with X input data of different dimensionalities. Ensure correct dimensionality for X. """ - raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) if delta_dim == -1: # remove a dimension @@ -263,22 +263,21 @@ def test_fit_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): # add a dimension X = np.zeros((n_samples, n_neurons, n_features, 1)) - if raise_exception: - with pytest.raises(ValueError, match="X must be three-dimensional"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_fit_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_dim, error, match_str", + [ + (-1, ValueError, "y must be two-dimensional"), + (0, None, None), + (1, ValueError, "y must be two-dimensional") + ] + ) + def test_fit_y_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method with y target data of different dimensionalities. Ensure correct dimensionality for y. """ - raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) if delta_dim == -1: # remove a dimension @@ -287,42 +286,41 @@ def test_fit_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): # add a dimension y = np.zeros((n_samples, n_neurons, 1)) - if raise_exception: - with pytest.raises(ValueError, match="y must be two-dimensional"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) - def test_fit_n_feature_consistency_weights(self, delta_n_features, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_features, error, match_str", + [ + (-1, ValueError, "Inconsistent number of features"), + (0, None, None), + (1, ValueError, "Inconsistent number of features") + ] + ) + def test_fit_n_feature_consistency_weights(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method for inconsistencies between data features and initial weights provided. Ensure the number of features align. """ - raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape # add/remove a feature from weights init_w = jnp.zeros((n_neurons, n_features + delta_n_features)) init_b = jnp.zeros((n_neurons,)) + _test_class_method(model, "fit", [X, y], {"init_params": (init_w,init_b)}, error, match_str) - if raise_exception: - with pytest.raises(ValueError, match="Inconsistent number of features"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) - - @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) - def test_fit_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_features, error, match_str", + [ + (-1, ValueError, "Inconsistent number of features"), + (0, None, None), + (1, ValueError, "Inconsistent number of features") + ] + ) + def test_fit_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method for inconsistencies between data features and model's expectations. Ensure the number of features in X aligns. """ raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) if delta_n_features == 1: # add a feature @@ -331,117 +329,121 @@ def test_fit_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_in # remove a feature X = X[..., :-1] - if raise_exception: - with pytest.raises(ValueError, match="Inconsistent number of features"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_fit_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_tp, error, match_str", + [ + (-1, ValueError, "The number of time-points in X and y"), + (0, None, None), + (1, ValueError, "The number of time-points in X and y") + ] + ) + def test_fit_time_points_x(self, delta_tp, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method for inconsistencies in time-points in data X. Ensure the correct number of time-points. """ - raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) X = jnp.zeros((X.shape[0] + delta_tp, ) + X.shape[1:]) - if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and y"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_fit_time_points_y(self, delta_tp, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_tp, error, match_str", + [ + (-1, ValueError, "The number of time-points in X and y"), + (0, None, None), + (1, ValueError, "The number of time-points in X and y") + ] + ) + def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_instantiation): """ Test the `fit` method for inconsistencies in time-points in y. Ensure the correct number of time-points. """ - raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) - if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and y"): - model.fit(X, y, init_params=(init_w, init_b)) - else: - model.fit(X, y, init_params=(init_w, init_b)) + _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none", 1]) - def test_fit_device_spec(self, device_spec, + @pytest.mark.parametrize("device_spec, error, match_str", + [ + ("cpu", None, None), + ("tpu", None, None), + ("gpu", None, None), + ("none", ValueError, "Invalid device specification: %s"), + (1, ValueError, "Invalid device specification: %s") + ] + ) + def test_fit_device_spec(self, device_spec, error, match_str, poissonGLM_model_instantiation): """ Test `simulate` across different device specifications. Validates if unsupported or absent devices raise exception or warning respectively. """ - raise_exception = not (device_spec in ["cpu", "tpu", "gpu"]) + if match_str is not None: + match_str = match_str % str(device_spec) raise_warning = all(device_spec != device.device_kind.lower() for device in jax.local_devices()) - raise_warning = raise_warning and (not raise_exception) - + raise_warning = raise_warning and (error is not ValueError) X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons, n_features)) - init_b = jnp.zeros((n_neurons,)) - if raise_exception: - with pytest.raises(ValueError, match=f"Invalid device specification: {device_spec}"): - model.fit(X, y, init_params=(init_w, init_b), device=device_spec) - elif raise_warning: + if raise_warning: with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): - model.fit(X, y, init_params=(init_w, init_b), device=device_spec) + model.fit(X, y, init_params=true_params, device=device_spec) else: - model.fit(X, y, init_params=(init_w, init_b), device=device_spec) + _test_class_method(model, "fit", [X, y], {"init_params": true_params, "device":device_spec}, + error, match_str) ####################### # Test model.score ####################### - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_score_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "The number of neuron in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neuron in the model parameters") + ] + ) + def test_score_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method when the number of neurons in X differs. Ensure the correct number of neurons. """ - raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape + n_neurons = X.shape[1] # set model coeff model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.score(X, y) - else: - model.score(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_score_n_neuron_match_y(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "The number of neuron in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neuron in the model parameters") + ] + ) + def test_score_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method when the number of neurons in y differs. Ensure the correct number of neurons. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape + n_neurons = X.shape[1] # set model coeff model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.score(X, y) - else: - model.score(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_score_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_dim, error, match_str", + [ + (-1, ValueError, "X must be three-dimensional"), + (0, None, None), + (1, ValueError, "X must be three-dimensional") + ] + ) + def test_score_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method with X input data of different dimensionalities. Ensure correct dimensionality for X. """ - raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape # set model coeff @@ -454,20 +456,20 @@ def test_score_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation) elif delta_dim == 1: # add a dimension X = np.zeros((n_samples, n_neurons, n_features, 1)) + _test_class_method(model, "score", [X, y], {}, error, match_str) - if raise_exception: - with pytest.raises(ValueError, match="X must be three-dimensional"): - model.score(X, y) - else: - model.score(X, y) - - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_dim, error, match_str", + [ + (-1, ValueError, "y must be two-dimensional, with shape"), + (0, None, None), + (1, ValueError, "y must be two-dimensional, with shape") + ] + ) + def test_score_y_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method with y of different dimensionalities. Ensure correct dimensionality for y. """ - raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, _ = X.shape # set model coeff @@ -481,19 +483,20 @@ def test_score_y_dimensionality(self, delta_dim, poissonGLM_model_instantiation) # add a dimension y = np.zeros((n_samples, n_neurons, 1)) - if raise_exception: - with pytest.raises(ValueError, match="y must be two-dimensional"): - model.score(X, y) - else: - model.score(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) - def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_features, error, match_str", + [ + (-1, ValueError, "Inconsistent number of features"), + (0, None, None), + (1, ValueError, "Inconsistent number of features") + ] + ) + def test_score_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method for inconsistencies in features of X. Ensure the number of features in X aligns with the model params. """ - raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff model.basis_coeff_ = true_params[0] @@ -505,14 +508,15 @@ def test_score_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_ # remove a feature X = X[..., :-1] - if raise_exception: - with pytest.raises(ValueError, match="Inconsistent number of features"): - model.score(X, y) - else: - model.score(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - @pytest.mark.parametrize("is_fit", [True, False]) - def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): + @pytest.mark.parametrize("is_fit, error, match_str", + [ + (True, None, None), + (False, ValueError, "This GLM instance is not fitted yet") + ] + ) + def test_score_is_fit(self, is_fit, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method on models based on their fit status. Ensure scoring is only possible on fitted models. @@ -521,14 +525,9 @@ def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if is_fit: model.fit(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - if raise_exception: - with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): - model.score(X, y) - else: - model.score(X, y) - - + #<<<<<<<<<<<<<<<<<<<<<< REFRACTOR UNTIL HERE @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): """ From 7fc6d6eb7d726c328aad418a0af6ddc5c3eb42c4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 14 Sep 2023 15:57:55 -0400 Subject: [PATCH 071/250] refractored glm tests with helper function --- src/neurostatslib/base_class.py | 4 +- tests/test_glm.py | 581 ++++++++++++++++++-------------- 2 files changed, 330 insertions(+), 255 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 5f2aa80a..e8a541cd 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -354,7 +354,7 @@ def _check_input_and_params_consistency( if y is not None: if y.shape[1] != n_neurons: raise ValueError( - "The number of neuron in the model parameters and in the inputs" + "The number of neurons in the model parameters and in the inputs" "must match." f"parameters has n_neurons: {n_neurons}, " f"the input provided has n_neurons: {y.shape[1]}" @@ -363,7 +363,7 @@ def _check_input_and_params_consistency( if X is not None: if X.shape[1] != n_neurons: raise ValueError( - "The number of neuron in the model parameters and in the inputs" + "The number of neurons in the model parameters and in the inputs" "must match." f"parameters has n_neurons: {n_neurons}, " f"the input provided has n_neurons: {X.shape[1]}" diff --git a/tests/test_glm.py b/tests/test_glm.py index b87f9c92..db56e1a8 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -185,7 +185,7 @@ def test_fit_init_params_type(self, init_params, error, match_str, poissonGLM_mo ]) def test_fit_n_neuron_match_weights(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `fit` method ensuring the number of neurons in the weights matches the expected number. + Test the `fit` method ensuring The number of neurons in the weights matches the expected number. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -201,7 +201,7 @@ def test_fit_n_neuron_match_weights(self, delta_n_neuron, error, match_str, pois ]) def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `fit` method ensuring the number of neurons in the baseline rate matches the expected number. + Test the `fit` method ensuring The number of neurons in the baseline rate matches the expected number. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -211,14 +211,14 @@ def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, error, match_str @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ - (-1, ValueError, "The number of neuron in the model parameters"), + (-1, ValueError, "The number of neurons in the model parameters"), (0, None, None), - (1, ValueError, "The number of neuron in the model parameters") + (1, ValueError, "The number of neurons in the model parameters") ] ) def test_fit_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `fit` method ensuring the number of neurons in X matches the number of neurons in the model. + Test the `fit` method ensuring The number of neurons in X matches The number of neurons in the model. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -228,14 +228,14 @@ def test_fit_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ - (-1, ValueError, "The number of neuron in the model parameters"), + (-1, ValueError, "The number of neurons in the model parameters"), (0, None, None), - (1, ValueError, "The number of neuron in the model parameters") + (1, ValueError, "The number of neurons in the model parameters") ] ) def test_fit_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `fit` method ensuring the number of neurons in y matches the number of neurons in the model. + Test the `fit` method ensuring The number of neurons in y matches The number of neurons in the model. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] @@ -396,14 +396,14 @@ def test_fit_device_spec(self, device_spec, error, match_str, ####################### @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ - (-1, ValueError, "The number of neuron in the model parameters"), + (-1, ValueError, "The number of neurons in the model parameters"), (0, None, None), - (1, ValueError, "The number of neuron in the model parameters") + (1, ValueError, "The number of neurons in the model parameters") ] ) def test_score_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `score` method when the number of neurons in X differs. Ensure the correct number of neurons. + Test the `score` method when The number of neurons in X differs. Ensure the correct number of neurons. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] @@ -415,14 +415,14 @@ def test_score_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonG @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ - (-1, ValueError, "The number of neuron in the model parameters"), + (-1, ValueError, "The number of neurons in the model parameters"), (0, None, None), - (1, ValueError, "The number of neuron in the model parameters") + (1, ValueError, "The number of neurons in the model parameters") ] ) def test_score_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `score` method when the number of neurons in y differs. Ensure the correct number of neurons. + Test the `score` method when The number of neurons in y differs. Ensure the correct number of neurons. """ raise_exception = delta_n_neuron != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -527,59 +527,62 @@ def test_score_is_fit(self, is_fit, error, match_str, poissonGLM_model_instantia model.fit(X, y) _test_class_method(model, "score", [X, y], {}, error, match_str) - #<<<<<<<<<<<<<<<<<<<<<< REFRACTOR UNTIL HERE - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_score_time_points_x(self, delta_tp, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_tp, error, match_str", + [ + (-1, ValueError, "The number of time-points in X and y"), + (0, None, None), + (1, ValueError, "The number of time-points in X and y") + + ] + ) + def test_score_time_points_x(self, delta_tp, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method for inconsistencies in time-points in X. Ensure that the number of time-points in X and y matches. """ - raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) - if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and y"): - model.score(X, y) - else: - model.score(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_score_time_points_y(self, delta_tp, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_tp, error, match_str", + [ + (-1, ValueError, "The number of time-points in X and y"), + (0, None, None), + (1, ValueError, "The number of time-points in X and y") + + ] + ) + def test_score_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method for inconsistencies in time-points in y. Ensure that the number of time-points in X and y matches. """ - raise_exception = delta_tp != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) - if raise_exception: - with pytest.raises(ValueError, match="The number of time-points in X and y"): - model.score(X, y) - else: - model.score(X, y) + _test_class_method(model, "score", [X, y], {}, error, match_str) - @pytest.mark.parametrize("score_type", ["pseudo-r2", "log-likelihood", "not-implemented"]) - def test_score_type_r2(self, score_type, poissonGLM_model_instantiation): + @pytest.mark.parametrize("score_type, error, match_str", [ + ("pseudo-r2", None, None), + ("log-likelihood", None, None), + ("not-implemented", NotImplementedError, "Scoring method %s not implemented") + ] + ) + def test_score_type_r2(self, score_type, error, match_str, poissonGLM_model_instantiation): """ Test the `score` method for unsupported scoring types. Ensure only valid score types are used. """ - raise_exception = score_type not in ["pseudo-r2", "log-likelihood"] X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + match_str = match_str % score_type if type(match_str) is str else None model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] - - if raise_exception: - with pytest.raises(NotImplementedError, match=f"Scoring method {score_type} not implemented"): - model.score(X, y, score_type=score_type) - else: - model.score(X, y, score_type=score_type) + _test_class_method(model, "score", [X, y], {"score_type": score_type}, error, match_str) def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): """ @@ -599,15 +602,19 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) raise ValueError("Log-likelihood of PoissonModel does not match" "that of jax.scipy!") - ####################### # Test model.predict ####################### - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_predict_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ + (-1, ValueError, "The number of neurons in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neurons in the model parameters") + ] + ) + def test_predict_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): """ - Test the `predict` method when the number of neurons in X differs. - Ensure that the number of neurons in X, y and params matches. + Test the `predict` method when The number of neurons in X differs. + Ensure that The number of neurons in X, y and params matches. """ raise_exception = delta_n_neuron != 0 X, _, model, true_params, _ = poissonGLM_model_instantiation @@ -616,45 +623,43 @@ def test_predict_n_neuron_match_x(self, delta_n_neuron, poissonGLM_model_instant model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.predict(X) - else: - model.predict(X) + _test_class_method(model, "predict", [X], {}, error, match_str) - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_predict_x_dimensionality(self, delta_dim, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_dim, error, match_str", [ + (-1, ValueError, "X must be three-dimensional"), + (0, None, None), + (1, ValueError, "X must be three-dimensional") + ] + ) + def test_predict_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): """ Test the `predict` method with x input data of different dimensionalities. Ensure correct dimensionality for x. """ - raise_exception = delta_dim != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape # set model coeff model.basis_coeff_ = true_params[0] model.baseline_link_fr_ = true_params[1] - if delta_dim == -1: # remove a dimension X = np.zeros((n_samples, n_neurons)) elif delta_dim == 1: # add a dimension X = np.zeros((n_samples, n_neurons, n_features, 1)) + _test_class_method(model, "predict", [X], {}, error, match_str) - if raise_exception: - with pytest.raises(ValueError, match="X must be three-dimensional"): - model.predict(X) - else: - model.predict(X) - - @pytest.mark.parametrize("delta_n_features", [-1, 0, 1]) - def test_predict_n_feature_consistency_x(self, delta_n_features, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_features, error, match_str", [ + (-1, ValueError, "Inconsistent number of features"), + (0, None, None), + (1, ValueError, "Inconsistent number of features") + ] + ) + def test_predict_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): """ Test the `predict` method ensuring the number of features in x input data is consistent with the model's `model.`basis_coeff_`. """ - raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff model.basis_coeff_ = true_params[0] @@ -665,69 +670,74 @@ def test_predict_n_feature_consistency_x(self, delta_n_features, poissonGLM_mode elif delta_n_features == -1: # remove a feature X = X[..., :-1] + _test_class_method(model, "predict", [X], {}, error, match_str) - if raise_exception: - with pytest.raises(ValueError, match="Inconsistent number of features"): - model.predict(X) - else: - model.predict(X) - - @pytest.mark.parametrize("is_fit", [True, False]) - def test_score_is_fit(self, is_fit, poissonGLM_model_instantiation): + @pytest.mark.parametrize("is_fit, error, match_str", + [ + (True, None, None), + (False, ValueError, "This GLM instance is not fitted yet") + ] + ) + def test_predict_is_fit(self, is_fit, error, match_str, poissonGLM_model_instantiation): """ Test if the model raises a ValueError when trying to score before it's fitted. """ - raise_exception = not is_fit X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if is_fit: model.fit(X, y) - - if raise_exception: - with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): - model.predict(X) - else: - model.predict(X) + _test_class_method(model, "predict", [X], {}, error, match_str) ####################### # Test model.simulate ####################### - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_simulate_n_neuron_match_input(self, delta_n_neuron, + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "The number of neurons in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neurons in the model parameters") + ] + ) + def test_simulate_n_neuron_match_input(self, delta_n_neuron, error, match_str, poissonGLM_coupled_model_config_simulate): """ - Test the `simulate` method to ensure that the number of neurons in the input + Test the `simulate` method to ensure that The number of neurons in the input matches the model's parameters. """ - raise_exception = delta_n_neuron != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate n_neurons, n_features = model.basis_coeff_.shape n_time_points, _, n_basis_input = feedforward_input.shape if delta_n_neuron != 0: feedforward_input = np.zeros((n_time_points, n_neurons+delta_n_neuron, n_basis_input)) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_simulate_input_dimensionality(self, delta_dim, + @pytest.mark.parametrize("delta_dim, error, match_str", + [ + (-1, ValueError, "X must be three-dimensional"), + (0, None, None), + (1, ValueError, "X must be three-dimensional") + ] + ) + def test_simulate_input_dimensionality(self, delta_dim, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method with input data of different dimensionalities. Ensure correct dimensionality for input. """ - raise_exception = delta_dim != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate if delta_dim == -1: @@ -737,28 +747,35 @@ def test_simulate_input_dimensionality(self, delta_dim, # add a dimension feedforward_input = np.zeros(feedforward_input.shape + (1,)) - if raise_exception: - with pytest.raises(ValueError, match="X must be three-dimensional"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_dim", [-1, 0, 1]) - def test_simulate_y_dimensionality(self, delta_dim, + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) + + @pytest.mark.parametrize("delta_dim, error, match_str", + [ + (-1, ValueError, "y must be two-dimensional"), + (0, None, None), + (1, ValueError, "y must be two-dimensional") + ] + ) + def test_simulate_y_dimensionality(self, delta_dim, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method with init_spikes of different dimensionalities. Ensure correct dimensionality for init_spikes. """ - raise_exception = delta_dim != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate n_samples, n_neurons = feedforward_input.shape[:2] @@ -769,132 +786,168 @@ def test_simulate_y_dimensionality(self, delta_dim, # add a dimension init_spikes = np.zeros((n_samples, n_neurons, 1)) - if raise_exception: - with pytest.raises(ValueError, match="y must be two-dimensional"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_n_neuron", [-1, 0, 1]) - def test_simulate_n_neuron_match_y(self, delta_n_neuron, + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) + + @pytest.mark.parametrize("delta_n_neuron, error, match_str", + [ + (-1, ValueError, "The number of neurons in the model parameters"), + (0, None, None), + (1, ValueError, "The number of neurons in the model parameters") + ] + ) + def test_simulate_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_coupled_model_config_simulate): """ - Test the `simulate` method to ensure that the number of neurons in init_spikes + Test the `simulate` method to ensure that The number of neurons in init_spikes matches the model's parameters. """ - raise_exception = delta_n_neuron != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate n_samples, n_neurons = feedforward_input.shape[:2] init_spikes = jnp.zeros((init_spikes.shape[0], n_neurons + delta_n_neuron)) - if raise_exception: - with pytest.raises(ValueError, match="The number of neuron in the model parameters"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("is_fit", [True, False]) - def test_simulate_is_fit(self, is_fit, + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) + + @pytest.mark.parametrize("is_fit, error, match_str", + [ + (True, None, None), + (False, ValueError, "This GLM instance is not fitted yet") + ] + ) + def test_simulate_is_fit(self, is_fit, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test if the model raises a ValueError when trying to simulate before it's fitted. """ - raise_exception = not is_fit model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate if not is_fit: model.baseline_link_fr_ = None + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) - if raise_exception: - with pytest.raises(ValueError, match="This GLM instance is not fitted yet"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_simulate_time_point_match_y(self, delta_tp, + @pytest.mark.parametrize("delta_tp, error, match_str", + [ + (-1, ValueError, "`init_y` and `coupling_basis_matrix`"), + (0, None, None), + (1, ValueError, "`init_y` and `coupling_basis_matrix`") + + ] + ) + def test_simulate_time_point_match_y(self, delta_tp, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method to ensure that the time points in init_y are consistent with the coupling_basis window size (they must be equal). """ - raise_exception = delta_tp != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate - init_spikes = jnp.zeros((init_spikes.shape[0]+delta_tp, + init_spikes = jnp.zeros((init_spikes.shape[0] + delta_tp, init_spikes.shape[1])) + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) - if raise_exception: - with pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_tp", [-1, 0, 1]) - def test_simulate_time_point_match_coupling_basis(self, delta_tp, + @pytest.mark.parametrize("delta_tp, error, match_str", + [ + (-1, ValueError, "`init_y` and `coupling_basis_matrix`"), + (0, None, None), + (1, ValueError, "`init_y` and `coupling_basis_matrix`") + + ] + ) + def test_simulate_time_point_match_coupling_basis(self, delta_tp, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method to ensure that the window size in coupling_basis is consistent with the time-points in init_spikes (they must be equal). """ - raise_exception = delta_tp != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate coupling_basis = jnp.zeros((coupling_basis.shape[0] + delta_tp,) + coupling_basis.shape[1:]) + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) - if raise_exception: - with pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_features", [-1, 0, 1]) - def test_simulate_feature_consistency_input(self, delta_features, + + @pytest.mark.parametrize("delta_features, error, match_str", + [ + (-1, ValueError, "Inconsistent number of features. spike basis coefficients has"), + (0, None, None), + (1, ValueError, "Inconsistent number of features. spike basis coefficients has") + + ] + ) + def test_simulate_feature_consistency_input(self, delta_features, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method ensuring the number of features in `feedforward_input` is @@ -905,28 +958,36 @@ def test_simulate_feature_consistency_input(self, delta_features, The total feature number `model.basis_coeff_.shape[1]` must be equal to `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` """ - raise_exception = delta_features != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate feedforward_input = jnp.zeros((feedforward_input.shape[0], feedforward_input.shape[1], - feedforward_input.shape[2]+delta_features)) - if raise_exception: - with pytest.raises(ValueError, match="Inconsistent number of features"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("delta_features", [-1, 0, 1]) - def test_simulate_feature_consistency_coupling_basis(self, delta_features, + feedforward_input.shape[2] + delta_features)) + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) + + @pytest.mark.parametrize("delta_features, error, match_str", + [ + (-1, ValueError, "Inconsistent number of features"), + (0, None, None), + (1, ValueError, "Inconsistent number of features") + + ] + ) + def test_simulate_feature_consistency_coupling_basis(self, delta_features, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method ensuring the number of features in `coupling_basis` is @@ -937,50 +998,53 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, The total feature number `model.basis_coeff_.shape[1]` must be equal to `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` """ - raise_exception = delta_features != 0 model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate coupling_basis = jnp.zeros((coupling_basis.shape[0], coupling_basis.shape[1] + delta_features)) - if raise_exception: - with pytest.raises(ValueError, match="Inconsistent number of features"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") - - @pytest.mark.parametrize("device_spec", ["cpu", "tpu", "gpu", "none", 1]) - def test_simulate_device_spec(self, device_spec, + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": "cpu" + + }, + error, + match_str + ) + + @pytest.mark.parametrize("device_spec, error, match_str", + [ + ("cpu", None, None), + ("tpu", None, None), + ("gpu", None, None), + ("none", ValueError, "Invalid device specification: %s"), + (1, ValueError, "Invalid device specification: %s") + ] + ) + def test_simulate_device_spec(self, device_spec, error, match_str, poissonGLM_coupled_model_config_simulate): """ Test `simulate` across different device specifications. Validates if unsupported or absent devices raise exception or warning respectively. """ - raise_exception = not (device_spec in ["cpu", "tpu", "gpu"]) - print(device_spec, raise_exception) + if match_str is not None: + match_str = match_str % str(device_spec) + raise_warning = all(device_spec != device.device_kind.lower() for device in jax.local_devices()) - raise_warning = raise_warning and (not raise_exception) + raise_warning = raise_warning and (error is None) model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate - if raise_exception: - with pytest.raises(ValueError, match=f"Invalid device specification: {device_spec}"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device=device_spec) - elif raise_warning: + if raise_warning: with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): model.simulate(random_key=random_key, init_y=init_spikes, @@ -988,11 +1052,22 @@ def test_simulate_device_spec(self, device_spec, feedforward_input=feedforward_input, device=device_spec) else: - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device=device_spec) + _test_class_method( + model, + "simulate", + [], + { + "random_key": random_key, + "init_y": init_spikes, + "coupling_basis_matrix": coupling_basis, + "feedforward_input": feedforward_input, + "device": device_spec + + }, + error, + match_str + ) + ####################################### # Compare with standard implementation ####################################### From f87e01d43ed72df51979ecda85f222f829aefa5b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 18 Sep 2023 18:14:04 -0400 Subject: [PATCH 072/250] removed data_type, improved docstrings, fixed tests --- src/neurostatslib/base_class.py | 109 ++- src/neurostatslib/glm.py | 1057 ++---------------------- src/neurostatslib/noise_model.py | 71 +- src/neurostatslib/proximal_operator.py | 14 +- src/neurostatslib/solver.py | 295 ++++++- tests/conftest.py | 17 +- tests/test_base_class.py | 181 +++- tests/test_glm.py | 118 +-- tests/test_solver.py | 20 +- 9 files changed, 687 insertions(+), 1195 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index e8a541cd..b103ef0e 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -15,6 +15,31 @@ class _Base: + """Base class for neurostatslib estimators. + + A base class for estimators with utilities for getting and setting parameters, + and for interacting with specific devices like CPU, GPU, and TPU. + + This class provides utilities for: + - Getting and setting parameters using introspection. + - Sending arrays to target devices (CPU, GPU, TPU). + + Parameters + ---------- + **kwargs : dict + Arbitrary keyword arguments. + + Attributes + ---------- + _kwargs_keys : list + List of keyword arguments provided during the initialization. + + Notes + ----- + The class provides helper methods mimicking scikit-learn's get_params and set_params. + Additionally, it has methods for selecting target devices and sending arrays to them. + """ + def __init__(self, **kwargs): self._kwargs_keys = list(kwargs.keys()) for key in kwargs: @@ -122,6 +147,11 @@ def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Dev Returns ------- The selected device. + + Raises + ------ + ValueError + If the an invalid device name is provided. """ if device == "cpu": target_device = jax.devices(device)[0] @@ -143,7 +173,7 @@ def device_put( ) -> Union[Any, jnp.ndarray]: """Send arrays to device. - This function sends the arrays to the target devices, if the arrays are + This function sends the arrays to the target device, if the arrays are not already there. Parameters @@ -205,7 +235,7 @@ def _get_param_names(cls): return sorted(parameters) -class _BaseRegressor(_Base, abc.ABC): +class BaseRegressor(_Base, abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps @abc.abstractmethod @@ -231,7 +261,6 @@ def simulate( self, random_key: jax.random.PRNGKeyArray, feed_forward_input: Union[NDArray, jnp.ndarray], - device: Literal["cpu", "gpu", "tpu"] = "cpu", # feed-forward input and/coupling basis **kwargs, ): @@ -239,7 +268,7 @@ def simulate( @staticmethod def _convert_to_jnp_ndarray( - *args: Union[NDArray, jnp.ndarray], data_type: jnp.dtype = jnp.float32 + *args: Union[NDArray, jnp.ndarray], data_type: Optional[jnp.dtype] = None ) -> Tuple[jnp.ndarray, ...]: """Convert provided arrays to jnp.ndarray of specified type. @@ -248,7 +277,8 @@ def _convert_to_jnp_ndarray( *args : Input arrays to convert. data_type : - Data type to convert to. Default is jnp.float32. + Data type to convert to. Default is None, which means that the data-type + is inferred from the input. Returns ------- @@ -275,7 +305,7 @@ def _has_invalid_entry(array: jnp.ndarray) -> bool: @staticmethod def _check_and_convert_params( - params: ArrayLike, data_type: jnp.dtype = jnp.float32 + params: ArrayLike, data_type: Optional[jnp.dtype] = None ) -> Tuple[jnp.ndarray, ...]: """ Validate the dimensions and consistency of parameters and data. @@ -289,7 +319,7 @@ def _check_and_convert_params( raise TypeError("Initial parameters must be array-like!") try: params = tuple(jnp.asarray(par, dtype=data_type) for par in params) - except ValueError: + except (ValueError, TypeError): raise TypeError( "Initial parameters must be array-like of array-like objects" "with numeric data-type!" @@ -384,20 +414,24 @@ def _check_input_n_timepoints(X: jnp.ndarray, y: jnp.ndarray): f"y has {y.shape[0]} instead!" ) - def _preprocess_fit( + def preprocess_fit( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[ArrayLike, ArrayLike]] = None, - data_type: jnp.dtype = jnp.float32, ) -> Tuple[jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]]: """Preprocess input data and initial parameters for the fit method. This method carries out the following preprocessing steps: - - Convert input data `X` and `y` to `jnp.ndarray` of type float32. + + - Convert to jax.numpy.ndarray + - Check the dimensionality of the inputs. + - Check for any NaNs or Infs in the inputs. + - If `init_params` is not provided, initialize it with default values. + - Validate the consistency of input dimensions with the initial parameters. Parameters @@ -408,25 +442,22 @@ def _preprocess_fit( Target values, expected to be of shape (n_timebins, n_neurons). init_params : Initial parameters for the model. If None, they are initialized with default values. - data_type : - Data type to convert to. Default is jnp.float32. Returns ------- X : - Preprocessed input data `X` converted to jnp.ndarray with the desired floating point precision. + Preprocessed input data `X` converted to jnp.ndarray. y : - Target values `y` converted to jnp.ndarray with the desired floating point precision - init_params : - Initialized parameters converted to jnp.ndarray with the desired floating point precision. + Target values `y` converted to jnp.ndarray. + init_param : + Initialized parameters converted to jnp.ndarray. Raises ------ ValueError If there are inconsistencies in the input shapes or if NaNs or Infs are detected. """ - # convert to jnp.ndarray of float32 - X, y = self._convert_to_jnp_ndarray(X, y, data_type=data_type) + X, y = self._convert_to_jnp_ndarray(X, y) # check input dimensionality self._check_input_dimensionality(X, y) @@ -457,23 +488,53 @@ def _preprocess_fit( return X, y, init_params - def _preprocess_simulate( + def preprocess_simulate( self, feedforward_input: Union[NDArray, jnp.ndarray], params_f: Tuple[jnp.ndarray, jnp.ndarray], - init_y: Optional[jnp.ndarray] = None, + init_y: Optional[Union[NDArray, jnp.ndarray]] = None, params_r: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, - data_type: jnp.dtype = jnp.float32, ) -> Tuple[jnp.ndarray, ...]: - (feedforward_input,) = self._convert_to_jnp_ndarray( - feedforward_input, data_type=data_type - ) + """ + Preprocess the input data and parameters for simulation. + + This method handles the conversion of the input data to `jnp.ndarray`, checks the + input's dimensionality, and ensures the input's consistency with the provided parameters. + It also verifies that the feedforward input does not have any invalid entries (NaNs or Infs). + + Parameters + ---------- + feedforward_input : + Input data for the feedforward process. Expected shape: (n_timesteps, n_neurons, n_basis_input). + params_f : + Parameters corresponding to the feedforward input. Expected shape: (n_neurons, n_basis_input). + init_y : + Initial values for the feedback process. If provided, its dimensionality and consistency + with params_r will be checked. Expected shape if provided: (window_size, n_neurons). + params_r : + Parameters corresponding to the feedback input (init_y). Required if init_y is provided. + Expected shape if provided: (window_size, n_basis_coupling) + + Returns + ------- + : + Preprocessed input data, optionally with the initial values for feedback if provided. + + Raises + ------ + ValueError + If the feedforward_input contains NaNs or Infs. + If the dimensionality or consistency checks fail for the provided data and parameters. + """ + (feedforward_input,) = self._convert_to_jnp_ndarray(feedforward_input) self._check_input_dimensionality(X=feedforward_input) self._check_input_and_params_consistency(params_f, X=feedforward_input) if self._has_invalid_entry(feedforward_input): raise ValueError("feedforward_input contains a NaNs or Infs!") + if init_y is not None: + (init_y,) = self._convert_to_jnp_ndarray(init_y) self._check_input_dimensionality(y=init_y) self._check_input_and_params_consistency(params_r, y=init_y) return feedforward_input, init_y diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 8bea1bf2..b8afd216 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -7,17 +7,16 @@ from . import noise_model as nsm from . import solver as slv -from .base_class import _BaseRegressor +from .base_class import BaseRegressor from .exceptions import NotFittedError from .utils import convolve_1d_trials -class GLM(_BaseRegressor): +class GLM(BaseRegressor): def __init__( self, noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), solver: slv.Solver = slv.RidgeSolver("GradientDescent"), - data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, **kwargs: Any, ): super().__init__() @@ -37,23 +36,6 @@ def __init__( self.noise_model = noise_model self.solver = solver - if not jax.config.values["jax_enable_x64"] and (data_type == jnp.float64): - raise TypeError( - "JAX is currently not set up to support `jnp.float64`. " - "To enable 64-bit precision, use " - '`jax.config.update("jax_enable_x64", True)` ' - "before your computations." - ) - - if data_type is None: - # set to jnp.float64, if float64 are enabled - if jax.config.jax_enable_x64: - self.data_type = jnp.float64 - else: - self.data_type = jnp.float32 - else: - self.data_type = data_type - self.baseline_link_fr_ = None self.basis_coeff_ = None # scale parameter (=1 for poisson and Gaussian, needs to be estimated for Gamma) @@ -87,7 +69,9 @@ def _predict( The predicted rates. Shape (n_time_bins, n_neurons). """ Ws, bs = params - return self.noise_model.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) + return self.noise_model.inverse_link_function( + jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :] + ) def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: """Predict rates based on fit parameters. @@ -129,7 +113,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - (X,) = self._convert_to_jnp_ndarray(X, data_type=self.data_type) + (X,) = self._convert_to_jnp_ndarray(X) # check input dimensionality self._check_input_dimensionality(X=X) @@ -247,7 +231,7 @@ def score( Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - X, y = self._convert_to_jnp_ndarray(X, y, data_type=self.data_type) + X, y = self._convert_to_jnp_ndarray(X, y) self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) @@ -265,7 +249,6 @@ def fit( X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, - device: Literal["cpu", "gpu", "tpu"] = "cpu", ): """Fit GLM to neural activity. @@ -281,8 +264,6 @@ def fit( init_params : Initial values for the activity basis coefficients and bias terms. If None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) - device: - Device used for optimizing model parameters. Raises ------ @@ -299,19 +280,11 @@ def fit( - If `init_params[i]` cannot be converted to jnp.ndarray for all i """ # convert to jnp.ndarray & perform checks - X, y, init_params = self._preprocess_fit( - X, y, init_params, data_type=self.data_type - ) - - # send to device - X, y = self.device_put(X, y, device=device) - init_params = self.device_put(*init_params, device=device) + X, y, init_params = self.preprocess_fit(X, y, init_params) - # Make sure mask is of the same floating type, - # and put to the correct device. + # Make sure mask is of floating type if isinstance(self.solver, slv.GroupLassoSolver): - self.solver.mask = jnp.asarray(self.solver.mask, dtype=self.data_type) - self.solver.mask = self.device_put(self.solver.mask, device=device)[0] + self.solver.mask = jnp.asarray(self.solver.mask, dtype=float) # Run optimization runner = self.solver.instantiate_solver(self._score) @@ -338,7 +311,6 @@ def simulate( self, random_key: jax.random.PRNGKeyArray, feedforward_input: Union[NDArray, jnp.ndarray], - device: Literal["cpu", "gpu", "tpu"] = "cpu", # feed-forward input and/coupling basis **kwargs, ) -> Tuple[jnp.ndarray, jnp.ndarray]: @@ -346,25 +318,50 @@ def simulate( Parameters ---------- - random_key - feedforward_input - device - kwargs + random_key : + PRNGKey for seeding the simulation. + feedforward_input : + External input matrix to the model, representing factors like convolved currents, + light intensities, etc. When not provided, the simulation is done with coupling-only. + Expected shape: (n_timesteps, n_neurons, n_basis_input). Returns ------- + simulated_activity : + Simulated activity (spike counts for PoissonGLMs) for each neuron over time. + Shape: (n_neurons, n_timesteps). + firing_rates : + Simulated rates for each neuron over time. + Shape: (n_neurons, n_timesteps). + Raises + ------ + NotFittedError + If the model hasn't been fitted prior to calling this method. + ValueError + - If the instance has not been previously fitted. + - If there's an inconsistency between the number of neurons in model parameters. + - If the number of neurons in input arguments doesn't match with model parameters. + + + See Also + -------- + [predict](./#neurostatslib.glm.GLM.predict) : + Method to predict rates based on the model's parameters. """ # check if the model is fit self._check_is_fit() Ws, bs = self.basis_coeff_, self.baseline_link_fr_ - (feedforward_input,) = self._preprocess_simulate( + (feedforward_input,) = self.preprocess_simulate( feedforward_input, params_f=(Ws, bs) ) predicted_rate = self._predict((Ws, bs), feedforward_input) - return self.noise_model.emission_probability( - key=random_key, predicted_rate=predicted_rate - ), predicted_rate + return ( + self.noise_model.emission_probability( + key=random_key, predicted_rate=predicted_rate + ), + predicted_rate, + ) class GLMRecurrent(GLM): @@ -374,13 +371,12 @@ def __init__( solver: slv.Solver = slv.RidgeSolver(), data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, ): - super().__init__(noise_model=noise_model, solver=solver, data_type=data_type) + super().__init__(noise_model=noise_model, solver=solver) def simulate( self, random_key: jax.random.PRNGKeyArray, feedforward_input: Union[NDArray, jnp.ndarray], - device: Literal["cpu", "gpu", "tpu"] = "cpu", coupling_basis_matrix: Union[NDArray, jnp.ndarray] = None, init_y: Union[NDArray, jnp.ndarray] = None, ): @@ -406,8 +402,6 @@ def simulate( coupling_basis_matrix : Basis matrix for coupling, representing between-neuron couplings and auto-correlations. Expected shape: (window_size, n_basis_coupling). - device : - Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. Returns ------- @@ -426,7 +420,6 @@ def simulate( - If the instance has not been previously fitted. - If there's an inconsistency between the number of neurons in model parameters. - If the number of neurons in input arguments doesn't match with model parameters. - - For an invalid computational device selection. See Also @@ -444,7 +437,6 @@ def simulate( The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure consistency in the model's input feature dimensionality. """ - if coupling_basis_matrix is None: raise ValueError( "GLMRecurrent simulate method requires a coupling basis" @@ -460,9 +452,7 @@ def simulate( self._check_is_fit() # convert to jnp.ndarray - (coupling_basis_matrix,) = self._convert_to_jnp_ndarray( - coupling_basis_matrix, data_type=self.data_type - ) + (coupling_basis_matrix,) = self._convert_to_jnp_ndarray(coupling_basis_matrix) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] @@ -472,15 +462,10 @@ def simulate( Wr = self.basis_coeff_[:, : n_basis_coupling * n_neurons] bs = self.baseline_link_fr_ - feedforward_input, init_y = self._preprocess_simulate( + feedforward_input, init_y = self.preprocess_simulate( feedforward_input, params_f=(Wf, bs), init_y=init_y, params_r=(Wr, bs) ) - # Transfer data to the target device - init_y, coupling_basis_matrix, feedforward_input = self.device_put( - init_y, coupling_basis_matrix, feedforward_input, device=device - ) - if ( feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] != self.basis_coeff_.shape[1] @@ -523,7 +508,9 @@ def scan_fn( activity, chunk = data # Convolve the neural activity with the coupling basis matrix - conv_act = convolve_1d_trials(coupling_basis_matrix, activity[None, :, :])[0] + conv_act = convolve_1d_trials(coupling_basis_matrix, activity[None, :, :])[ + 0 + ] # Extract the corresponding slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( @@ -553,946 +540,6 @@ def scan_fn( _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) simulated_activity, firing_rates = outputs - return jnp.squeeze(simulated_activity, axis=1), jnp.squeeze(firing_rates, axis=1) - - -# class _BaseGLM(_BaseRegressor, abc.ABC): -# """Abstract base class for Poisson GLMs. -# -# Provides methods for score computation, simulation, and prediction. -# Must be subclassed with a method for fitting to data. -# -# Parameters -# ---------- -# solver_name -# Name of the solver to use when fitting the GLM. Must be an attribute of -# ``jaxopt``. -# solver_kwargs -# Dictionary of keyword arguments to pass to the solver during its -# initialization. -# inverse_link_function -# Function to transform outputs of convolution with basis to firing rate. -# Must accept any number as input and return all non-negative values. -# kwargs: -# Additional keyword arguments. ``kwargs`` may depend on the concrete -# subclass implementation (e.g. alpha, the regularization hyperparamter, will be present for -# penalized GLMs but not for the un-penalized case). -# -# """ -# -# def __init__( -# self, -# solver_name: str = "GradientDescent", -# solver_kwargs: dict = dict(), -# inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, -# score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", -# **kwargs, -# ): -# self.solver_name = solver_name -# try: -# solver_args = inspect.getfullargspec(getattr(jaxopt, solver_name)).args -# except AttributeError: -# raise AttributeError( -# f"module jaxopt has no attribute {solver_name}, pick a different solver!" -# ) -# -# undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) -# if undefined_kwargs: -# raise NameError( -# f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" -# ) -# -# if score_type not in ["log-likelihood", "pseudo-r2"]: -# raise NotImplementedError( -# f"Scoring method {score_type} not implemented! " -# f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." -# ) -# self.score_type = score_type -# self.solver_kwargs = solver_kwargs -# -# if not callable(inverse_link_function): -# raise ValueError("inverse_link_function must be a callable!") -# -# self.inverse_link_function = inverse_link_function -# # set additional kwargs e.g. regularization hyperparameters and so on... -# super().__init__(**kwargs) -# # initialize parameters to None -# self.baseline_link_fr_ = None -# self.basis_coeff_ = None -# -# def _predict( -# self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray -# ) -> jnp.ndarray: -# """ -# Predict firing rates given predictors and parameters. -# -# Parameters -# ---------- -# params : -# Tuple containing the spike basis coefficients and bias terms. -# X : -# Predictors. Shape (n_time_bins, n_neurons, n_features). -# -# Returns -# ------- -# jnp.ndarray -# The predicted firing rates. Shape (n_time_bins, n_neurons). -# """ -# Ws, bs = params -# return self.inverse_link_function(jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :]) -# -# @abc.abstractmethod -# def residual_deviance(self, predicted_rate, y): -# r"""Compute the residual deviance for a GLM model. -# -# Parameters -# ---------- -# predicted_rate: -# The predicted rate of the GLM. -# y: -# The observations. -# -# Returns -# ------- -# The residual deviance of the model. -# -# Notes -# ----- -# Deviance is a measure of the goodness of fit of a statistical model. -# For a Poisson model, the residual deviance is computed as: -# -# $$ -# \begin{aligned} -# D(y, \hat{y}) &= -2 \left( \text{LL}\left(y | \hat{y}\right) - \text{LL}\left(y | y\right)\right) -# \end{aligned} -# $$ -# where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model -# log-likelihood. Lower values of deviance indicate a better fit. -# -# """ -# pass -# -# def _pseudo_r2(self, params, X, y): -# r"""Pseudo-R^2 calculation for a GLM. -# -# The Pseudo-R^2 metric gives a sense of how well the model fits the data, -# relative to a null (or baseline) model. -# -# Parameters -# ---------- -# params : -# Tuple containing the spike basis coefficients and bias terms. -# X: -# The predictors. -# y: -# The neural activity. -# -# Returns -# ------- -# : -# The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, -# whereas a value closer to 0 suggests that the model doesn't improve much over the null model. -# -# """ -# mu = self._predict(params, X) -# res_dev_t = self.residual_deviance(mu, y) -# resid_deviance = jnp.sum(res_dev_t**2) -# -# null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() -# null_dev_t = self.residual_deviance(null_mu, y) -# null_deviance = jnp.sum(null_dev_t**2) -# -# return (null_deviance - resid_deviance) / null_deviance -# -# def _check_is_fit(self): -# """Ensure the instance has been fitted.""" -# if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): -# raise NotFittedError( -# "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." -# ) -# -# def _safe_predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: -# """Predict firing rates based on fit parameters. -# -# Parameters -# ---------- -# X : -# The exogenous variables. Shape (n_time_bins, n_neurons, n_features). -# -# Returns -# ------- -# predicted_firing_rates : jnp.ndarray -# The predicted firing rates with shape (n_neurons, n_time_bins). -# -# Raises -# ------ -# NotFittedError -# If ``fit`` has not been called first with this instance. -# ValueError -# - If `params` is not a JAX pytree of size two. -# - If weights and bias terms in `params` don't have the expected dimensions. -# - If the number of neurons in the model parameters and in the inputs do not match. -# - If `X` is not three-dimensional. -# - If there's an inconsistent number of features between spike basis coefficients and `X`. -# -# See Also -# -------- -# score -# Score predicted firing rates against target spike counts. -# """ -# # check that the model is fitted -# self._check_is_fit() -# # extract model params -# Ws = self.basis_coeff_ -# bs = self.baseline_link_fr_ -# -# (X,) = self._convert_to_jnp_ndarray(X, data_type=jnp.float32) -# -# # check input dimensionality -# self._check_input_dimensionality(X=X) -# # check consistency between X and params -# self._check_input_and_params_consistency((Ws, bs), X=X) -# -# return self._predict((Ws, bs), X) -# -# def _safe_score( -# self, -# X: Union[NDArray, jnp.ndarray], -# y: Union[NDArray, jnp.ndarray], -# score_func: Callable[ -# [jnp.ndarray, jnp.ndarray, Tuple[jnp.ndarray, jnp.ndarray]], jnp.ndarray -# ], -# score_type: Optional[Literal["log-likelihood", "pseudo-r2"]] = None, -# ) -> jnp.ndarray: -# r"""Score the predicted firing rates (based on fit) to the target spike counts. -# -# This computes the GLM mean log-likelihood or the pseudo-$R^2$, thus the higher the -# number the better. -# -# The pseudo-$R^2$ can be computed as follows, -# -# $$ -# \begin{aligned} -# R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ -# &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) -# - \log \text{LL}(\bar{\lambda}| y)}, -# \end{aligned} -# $$ -# -# where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is -# the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model -# predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate. See [1]. -# -# Parameters -# ---------- -# X : -# The exogenous variables. Shape (n_time_bins, n_neurons, n_features) -# y : -# Neural activity arranged in a matrix. n_neurons must be the same as -# during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). -# score_type: -# String indicating the type of scoring to return. Options are: -# - `log-likelihood` for the model log-likelihood. -# - `pseudo-r2` for the model pseudo-$R^2$. -# Default is defined at class initialization. -# -# Returns -# ------- -# score : (1,) -# The Poisson log-likelihood or the pseudo-$R^2$ of the current model. -# -# Raises -# ------ -# NotFittedError -# If ``fit`` has not been called first with this instance. -# ValueError -# If attempting to simulate a different number of neurons than were -# present during fitting (i.e., if ``init_y.shape[0] != -# self.baseline_link_fr_.shape[0]``). -# -# Notes -# ----- -# The log-likelihood is not on a standard scale, its value is influenced by many factors, -# among which the number of model parameters. The log-likelihood can assume both positive -# and negative values. -# -# The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure -# of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. -# The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. -# -# Refer to the concrete subclass docstrings `_score` for the specific likelihood equations. -# -# References -# ---------- -# [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. -# Routledge, 2013. -# -# """ -# # ignore the last time point from predict, because that corresponds to -# # the next time step, which we have no observed data for -# self._check_is_fit() -# Ws = self.basis_coeff_ -# bs = self.baseline_link_fr_ -# -# X, y = self._convert_to_jnp_ndarray(X, y, data_type=jnp.float32) -# -# self._check_input_dimensionality(X, y) -# self._check_input_n_timepoints(X, y) -# self._check_input_and_params_consistency((Ws, bs), X=X, y=y) -# -# if score_type is None: -# score_type = self.score_type -# -# if score_type == "log-likelihood": -# score = -(score_func(X, y, (Ws, bs))) -# elif score_type == "pseudo-r2": -# score = self._pseudo_r2((Ws, bs), X, y) -# else: -# # this should happen only if one manually set score_type -# raise NotImplementedError( -# f"Scoring method {score_type} not implemented! " -# f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." -# ) -# return score -# -# def _safe_fit( -# self, -# X: Union[NDArray, jnp.ndarray], -# y: Union[NDArray, jnp.ndarray], -# loss: Callable[ -# [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.float32 -# ], -# init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, -# device: Literal["cpu", "gpu", "tpu"] = "gpu", -# ): -# """Fit GLM to neuroal activity. -# -# Following scikit-learn API, the solutions are stored as attributes -# ``basis_coeff_`` and ``baseline_link_fr``. -# -# Parameters -# ---------- -# X : -# Predictors, shape (n_time_bins, n_neurons, n_features) -# y : -# Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). -# loss: -# The loss function to be minimized. -# init_params : -# Initial values for the spike basis coefficients and bias terms. If -# None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) -# device: -# Device used for optimizing model parameters. -# -# Raises -# ------ -# ValueError -# - If `init_params` is not of length two. -# - If dimensionality of `init_params` are not correct. -# - If the number of neurons in the model parameters and in the inputs do not match. -# - If `X` is not three-dimensional. -# - If spike_data is not two-dimensional. -# - If solver returns at least one NaN parameter, which means it found -# an invalid solution. Try tuning optimization hyperparameters. -# TypeError -# - If `init_params` are not array-like -# - If `init_params[i]` cannot be converted to jnp.ndarray for all i -# """ -# # convert to jnp.ndarray & perform checks -# X, y, init_params = self._preprocess_fit(X, y, init_params) -# -# # send to device -# target_device = self.select_target_device(device) -# X, y = self.device_put(X, y, device=target_device) -# init_params = self.device_put(*init_params, device=target_device) -# -# # Run optimization -# solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) -# params, state = solver.run(init_params, X=X, y=y) -# -# if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): -# raise ValueError( -# "Solver returned at least one NaN parameter, so solution is invalid!" -# " Try tuning optimization hyperparameters." -# ) -# -# # Store parameters -# self.basis_coeff_ = params[0] -# self.baseline_link_fr_ = params[1] -# # note that this will include an error value, which is not the same as -# # the output of loss. I believe it's the output of -# # solver.l2_optimality_error -# self.solver_state = state -# self.solver = solver -# -# def _safe_simulate( -# self, -# random_key: jax.random.PRNGKeyArray, -# n_timesteps: int, -# init_y: Union[NDArray, jnp.ndarray], -# coupling_basis_matrix: Union[NDArray, jnp.ndarray], -# random_function: Callable[[jax.random.PRNGKeyArray, ArrayLike], jnp.ndarray], -# feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, -# device: Literal["cpu", "gpu", "tpu"] = "cpu", -# ) -> Tuple[jnp.ndarray, jnp.ndarray]: -# """ -# Simulate spike trains using the GLM as a recurrent network. -# -# This function projects neural activity into the future, employing the fitted -# parameters of the GLM. It is capable of simulating activity based on a combination -# of historical spike activity and external feedforward inputs like convolved currents, light -# intensities, etc. -# -# Parameters -# ---------- -# random_key : -# PRNGKey for seeding the simulation. -# n_timesteps : -# Duration of the simulation in terms of time steps. -# init_y : -# Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. -# Expected shape: (window_size, n_neurons). -# coupling_basis_matrix : -# Basis matrix for coupling, representing between-neuron couplings -# and auto-correlations. Expected shape: (window_size, n_basis_coupling). -# random_function : -# A probability emission function, like jax.random.poisson, which takes as input a random.PRNGKeyArray -# and the mean rate, and samples observations, (spike counts for a poisson). -# feedforward_input : -# External input matrix to the model, representing factors like convolved currents, -# light intensities, etc. When not provided, the simulation is done with coupling-only. -# Expected shape: (n_timesteps, n_neurons, n_basis_input). -# device : -# Computation device to use ('cpu', 'gpu', or 'tpu'). Default is 'cpu'. -# -# Returns -# ------- -# simulated_obs : -# Simulated observations (spike counts for PoissonGLMs) for each neuron over time. -# Shape: (n_neurons, n_timesteps). -# firing_rates : -# Simulated firing rates for each neuron over time. -# Shape: (n_neurons, n_timesteps). -# -# Raises -# ------ -# NotFittedError -# If the model hasn't been fitted prior to calling this method. -# ValueError -# - If the instance has not been previously fitted. -# - If there's an inconsistency between the number of neurons in model parameters. -# - If the number of neurons in input arguments doesn't match with model parameters. -# - For an invalid computational device selection. -# -# -# See Also -# -------- -# predict : Method to predict firing rates based on the model's parameters. -# -# Notes -# ----- -# The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients -# (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. -# The remaining coefficients correspond to the weights for the feed-forward input. -# -# -# The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` -# to ensure consistency in the model's input feature dimensionality. -# """ -# target_device = self.select_target_device(device) -# # check if the model is fit -# self._check_is_fit() -# -# # convert to jnp.ndarray -# init_y, coupling_basis_matrix, feedforward_input = self._convert_to_jnp_ndarray( -# init_y, coupling_basis_matrix, feedforward_input, data_type=jnp.float32 -# ) -# -# # Transfer data to the target device -# init_y, coupling_basis_matrix, feedforward_input = self.device_put( -# init_y, coupling_basis_matrix, feedforward_input, device=target_device -# ) -# -# n_basis_coupling = coupling_basis_matrix.shape[1] -# n_neurons = self.baseline_link_fr_.shape[0] -# -# # add an empty input (simulate with coupling-only) -# if feedforward_input is None: -# feedforward_input = jnp.zeros( -# (n_timesteps, n_neurons, 0), dtype=jnp.float32 -# ) -# -# Ws = self.basis_coeff_ -# bs = self.baseline_link_fr_ -# -# self._check_input_dimensionality(feedforward_input, init_y) -# -# if ( -# feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] -# != Ws.shape[1] -# ): -# raise ValueError( -# "The number of feed forward input features " -# "and the number of recurrent features must add up to " -# "the overall model features." -# f"The total number of feature of the model is {Ws.shape[1]}. {feedforward_input.shape[1]} " -# f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " -# f"provided instead." -# ) -# -# self._check_input_and_params_consistency( -# (Ws[:, n_basis_coupling * n_neurons :], bs), -# X=feedforward_input, -# y=init_y, -# ) -# -# if init_y.shape[0] != coupling_basis_matrix.shape[0]: -# raise ValueError( -# "`init_y` and `coupling_basis_matrix`" -# " should have the same window size! " -# f"`init_y` window size: {init_y.shape[1]}, " -# f"`spike_basis_matrix` window size: {coupling_basis_matrix.shape[1]}" -# ) -# -# if feedforward_input.shape[0] != n_timesteps: -# raise ValueError( -# "`feedforward_input` must be of length `n_timesteps`. " -# f"`feedforward_input` has length {len(feedforward_input)}, " -# f"`n_timesteps` is {n_timesteps} instead!" -# ) -# subkeys = jax.random.split(random_key, num=n_timesteps) -# -# def scan_fn( -# data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray -# ) -> Tuple[Tuple[jnp.ndarray, int], Tuple[jnp.ndarray, jnp.ndarray]]: -# """Scan over time steps and simulate spikes and firing rates. -# -# This function simulates the spikes and firing rates for each time step -# based on the previous spike data, feedforward input, and model coefficients. -# """ -# spikes, chunk = data -# -# # Convolve the spike data with the coupling basis matrix -# conv_spk = convolve_1d_trials(coupling_basis_matrix, spikes[None, :, :])[0] -# -# # Extract the corresponding slice of the feedforward input for the current time step -# input_slice = jax.lax.dynamic_slice( -# feedforward_input, -# (chunk, 0, 0), -# (1, feedforward_input.shape[1], feedforward_input.shape[2]), -# ) -# -# # Reshape the convolved spikes and concatenate with the input slice to form the model input -# conv_spk = jnp.tile( -# conv_spk.reshape(conv_spk.shape[0], -1), conv_spk.shape[1] -# ).reshape(conv_spk.shape[0], conv_spk.shape[1], -1) -# X = jnp.concatenate([conv_spk, input_slice], axis=2) -# -# # Predict the firing rate using the model coefficients -# firing_rate = self._predict((Ws, bs), X) -# -# # Simulate spikes based on the predicted firing rate -# new_spikes = random_function(key, firing_rate) -# -# # Prepare the spikes for the next iteration (keeping the most recent spikes) -# concat_spikes = jnp.row_stack((spikes[1:], new_spikes)), chunk + 1 -# return concat_spikes, (new_spikes, firing_rate) -# -# _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) -# simulated_spikes, firing_rates = outputs -# return jnp.squeeze(simulated_spikes, axis=1), jnp.squeeze(firing_rates, axis=1) -# -# -# class PoissonGLM(_BaseGLM): -# """Un-regularized Poisson-GLM. -# -# The class fits the un-penalized maximum likelihood Poisson GLM parameter estimate. -# -# Parameters -# ---------- -# solver_name -# Name of the solver to use when fitting the GLM. Must be an attribute of -# ``jaxopt``. -# solver_kwargs -# Dictionary of keyword arguments to pass to the solver during its -# initialization. -# inverse_link_function -# Function to transform outputs of convolution with basis to firing rate. -# Must accept any number as input and return all non-negative values. -# -# Attributes -# ---------- -# solver -# jaxopt solver, set during ``fit()`` -# solver_state -# state of the solver, set during ``fit()`` -# basis_coeff_ : jnp.ndarray, (n_neurons, n_basis_funcs, n_neurons) -# Solutions for the spike basis coefficients, set during ``fit()`` -# baseline_link_fr : jnp.ndarray, (n_neurons,) -# Solutions for bias terms, set during ``fit()`` -# """ -# -# def __init__( -# self, -# solver_name: str = "GradientDescent", -# solver_kwargs: dict = dict(), -# inverse_link_function: Callable[[jnp.ndarray], jnp.ndarray] = jax.nn.softplus, -# score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", -# ): -# super().__init__( -# solver_name=solver_name, -# solver_kwargs=solver_kwargs, -# inverse_link_function=inverse_link_function, -# score_type=score_type, -# ) -# -# def _score( -# self, -# X: jnp.ndarray, -# target_spikes: jnp.ndarray, -# params: Tuple[jnp.ndarray, jnp.ndarray], -# ) -> jnp.ndarray: -# r"""Score the predicted firing rates against target spike counts. -# -# This computes the Poisson negative log-likelihood up to a constant. -# -# Note that you can end up with infinities in here if there are zeros in -# ``predicted_firing_rates``. We raise a warning in that case. -# -# The formula for the Poisson mean log-likelihood is the following, -# -# $$ -# \begin{aligned} -# \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} -# [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ -# &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] -# \end{aligned} -# $$ -# -# Because $\Gamma(k+1)=k!$, see -# https://en.wikipedia.org/wiki/Gamma_function. -# -# Parameters -# ---------- -# X : -# The exogenous variables. Shape (n_time_bins, n_neurons, n_features). -# target_spikes : -# The target spikes to compare against. Shape (n_time_bins, n_neurons). -# params : -# Values for the spike basis coefficients and bias terms. Shape ((n_neurons, n_features), (n_neurons,)). -# -# Returns -# ------- -# : -# The Poisson negative log-likehood. Shape (1,). -# -# Notes -# ----- -# The Poisson probability mass function is: -# -# $$ -# \frac{\lambda^k \exp(-\lambda)}{k!} -# $$ -# -# But the $k!$ term is not a function of the parameters and can be disregarded -# when computing the loss-function. Thus, the negative log of it is: -# -# $$ -# -\log{\frac{\lambda^k\exp{-\lambda}}{k!}} &= -[\log(\lambda^k)+\log(\exp{-\lambda})-\log(k!)] -# &= -k\log(\lambda)-\lambda + \text{const} -# $$ -# -# """ -# # Avoid the edge-case of 0*log(0), much faster than -# # where on large arrays. -# predicted_firing_rates = jnp.clip(self._predict(params, X), a_min=10**-10) -# x = target_spikes * jnp.log(predicted_firing_rates) -# # see above for derivation of this. -# return jnp.mean(predicted_firing_rates - x) -# -# def residual_deviance( -# self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray -# ) -> jnp.ndarray: -# r"""Compute the residual deviance for a Poisson model. -# -# Parameters -# ---------- -# predicted_rate: -# The predicted firing rates. -# spike_counts: -# The spike counts. -# -# Returns -# ------- -# : -# The residual deviance of the model. -# -# Notes -# ----- -# Deviance is a measure of the goodness of fit of a statistical model. -# For a Poisson model, the residual deviance is computed as: -# -# $$ -# \begin{aligned} -# D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) -# - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ -# &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - -# \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) -# \end{aligned} -# $$ -# where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model -# log-likelihood. Lower values of deviance indicate a better fit. -# -# """ -# # this takes care of 0s in the log -# ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) -# resid_dev = 2 * ( -# spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) -# ) -# return resid_dev -# -# def predict(self, X: Union[NDArray, jnp.ndarray]): -# """Predict firing rates based on fit parameters. -# -# Parameters -# ---------- -# X : -# The exogenous variables. Shape (n_time_bins, n_neurons, n_features). -# -# Returns -# ------- -# predicted_firing_rates : jnp.ndarray -# The predicted firing rates with shape (n_neurons, n_time_bins). -# -# Raises -# ------ -# NotFittedError -# If ``fit`` has not been called first with this instance. -# ValueError -# - If `params` is not a JAX pytree of size two. -# - If weights and bias terms in `params` don't have the expected dimensions. -# - If the number of neurons in the model parameters and in the inputs do not match. -# - If `X` is not three-dimensional. -# - If there's an inconsistent number of features between spike basis coefficients and `X`. -# -# See Also -# -------- -# [score](../glm/#neurostatslib.glm.PoissonGLM.score) -# Score predicted firing rates against target spike counts. -# """ -# return self._safe_predict(X) -# -# def score( -# self, -# X: Union[NDArray, jnp.ndarray], -# y: Union[NDArray, jnp.ndarray], -# score_type: Literal["log-likelihood", "pseudo-r2"] = "log-likelihood", -# ) -> jnp.ndarray: -# r"""Score the predicted firing rates (based on fit) to the target spike counts. -# -# This computes the Poisson mean log-likelihood or the pseudo-$R^2$, thus the higher the -# number the better. -# -# The formula for the mean log-likelihood is the following, -# -# $$ -# \begin{aligned} -# \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} -# [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ -# &= \frac{1}{T \cdot N} [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] -# \end{aligned} -# $$ -# -# Because $\Gamma(k+1)=k!$, see -# https://en.wikipedia.org/wiki/Gamma_function. -# -# The pseudo-$R^2$ can be computed as follows, -# -# $$ -# \begin{aligned} -# R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ -# &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) -# - \log \text{LL}(\bar{\lambda}| y)}, -# \end{aligned} -# $$ -# -# where $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is the deviance for -# the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the spike counts and the model predicted rate -# of neuron $n$ at time-point $t$ respectively, and $\bar{\lambda}$ is the mean firing rate. See [1]. -# -# Parameters -# ---------- -# X : -# The exogenous variables. Shape (n_time_bins, n_neurons, n_features) -# y : -# Spike counts arranged in a matrix. n_neurons must be the same as -# during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). -# score_type: -# String indicating the type of scoring to return. Options are: -# - `log-likelihood` for the model log-likelihood. -# - `pseudo-r2` for the model pseudo-$R^2$. -# Default is defined at class initialization. -# -# Returns -# ------- -# score : -# The Poisson log-likelihood or the pseudo-$R^2$ of the current model. -# -# Raises -# ------ -# NotFittedError -# If ``fit`` has not been called first with this instance. -# ValueError -# If attempting to simulate a different number of neurons than were -# present during fitting (i.e., if ``init_y.shape[0] != -# self.baseline_link_fr_.shape[0]``). -# -# Notes -# ----- -# The log-likelihood is not on a standard scale, its value is influenced by many factors, -# among which the number of model parameters. The log-likelihood can assume both positive -# and negative values. -# -# The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure -# of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. -# The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. -# -# References -# ---------- -# [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. -# Routledge, 2013. -# -# """ -# norm_constant = jax.scipy.special.gammaln(y + 1).mean() -# return ( -# super()._safe_score(X=X, y=y, score_type=score_type, score_func=self._score) -# - norm_constant -# ) -# -# def fit( -# self, -# X: Union[NDArray, jnp.ndarray], -# y: Union[NDArray, jnp.ndarray], -# init_params: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, -# device: Literal["cpu", "gpu", "tpu"] = "gpu", -# ): -# """Fit GLM to spiking data. -# -# Following scikit-learn API, the solutions are stored as attributes -# ``basis_coeff_`` and ``baseline_link_fr``. -# -# Parameters -# ---------- -# X : -# Predictors, shape (n_time_bins, n_neurons, n_features) -# y : -# Spike counts arranged in a matrix, shape (n_time_bins, n_neurons). -# init_params : -# Initial values for the spike basis coefficients and bias terms. If -# None, we initialize with zeros. shape. ((n_neurons, n_features), (n_neurons,)) -# device: -# Device used for optimizing model parameters. -# -# Raises -# ------ -# ValueError -# - If `init_params` is not of length two. -# - If dimensionality of `init_params` are not correct. -# - If the number of neurons in the model parameters and in the inputs do not match. -# - If `X` is not three-dimensional. -# - If spike_data is not two-dimensional. -# - If solver returns at least one NaN parameter, which means it found -# an invalid solution. Try tuning optimization hyperparameters. -# TypeError -# - If `init_params` are not array-like -# - If `init_params[i]` cannot be converted to jnp.ndarray for all i -# -# """ -# -# def loss(params, X, y): -# return self._score(X, y, params) -# -# self._safe_fit(X=X, y=y, loss=loss, init_params=init_params, device=device) -# -# def simulate( -# self, -# random_key: jax.random.PRNGKeyArray, -# n_timesteps: int, -# init_y: Union[NDArray, jnp.ndarray], -# coupling_basis_matrix: Union[NDArray, jnp.ndarray], -# feedforward_input: Optional[Union[NDArray, jnp.ndarray]] = None, -# device: Literal["cpu", "gpu", "tpu"] = "cpu", -# ) -> Tuple[jnp.ndarray, jnp.ndarray]: -# """ -# Simulate spike trains using the Poisson-GLM as a recurrent network. -# -# This function projects spike trains into the future, employing the fitted -# parameters of the GLM. It is capable of simulating spike trains based on a combination -# of historical spike activity and external feedforward inputs like convolved currents, light -# intensities, etc. -# -# -# Parameters -# ---------- -# random_key : -# PRNGKey for seeding the simulation. -# n_timesteps : -# Duration of the simulation in terms of time steps. -# init_y : -# Initial spike counts matrix that kickstarts the simulation. -# Expected shape: (window_size, n_neurons). -# coupling_basis_matrix : -# Basis matrix for coupling, representing between-neuron couplings -# and auto-correlations. Expected shape: (window_size, n_basis_coupling). -# feedforward_input : -# External input matrix to the model, representing factors like convolved currents, -# light intensities, etc. When not provided, the simulation is done with coupling-only. -# Expected shape: (n_timesteps, n_neurons, n_basis_input). -# device : -# Computation device to use ('cpu', 'gpu' or 'tpu'). Default is 'cpu'. -# -# Returns -# ------- -# simulated_spikes : -# Simulated spike counts for each neuron over time. -# Shape: (n_neurons, n_timesteps). -# firing_rates : -# Simulated firing rates for each neuron over time. -# Shape: (n_neurons, n_timesteps). -# -# Raises -# ------ -# NotFittedError -# If the model hasn't been fitted prior to calling this method. -# ValueError -# - If the instance has not been previously fitted. -# - If there's an inconsistency between the number of neurons in model parameters. -# - If the number of neurons in input arguments doesn't match with model parameters. -# - For an invalid computational device selection. -# -# -# See Also -# -------- -# [predict](../glm/#neurostatslib.glm.PoissonGLM.predict) : Method to predict firing rates based on -# the model's parameters. -# -# Notes -# ----- -# The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients -# (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. -# The remaining coefficients correspond to the weights for the feed-forward input. -# -# -# The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` -# to ensure consistency in the model's input feature dimensionality. -# """ -# simulated_spikes, firing_rates = super()._safe_simulate( -# random_key=random_key, -# n_timesteps=n_timesteps, -# init_y=init_y, -# coupling_basis_matrix=coupling_basis_matrix, -# random_function=jax.random.poisson, -# feedforward_input=feedforward_input, -# device=device, -# ) -# return simulated_spikes, firing_rates + return jnp.squeeze(simulated_activity, axis=1), jnp.squeeze( + firing_rates, axis=1 + ) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index c65454bd..2d9ecdcf 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -29,10 +29,9 @@ def __init__(self, inverse_link_function, **kwargs): def negative_log_likelihood(self, firing_rate, y): pass - @staticmethod @abc.abstractmethod def emission_probability( - key: KeyArray, predicted_rate: jnp.ndarray, **kwargs + self, key: KeyArray, predicted_rate: jnp.ndarray ) -> jnp.ndarray: pass @@ -49,9 +48,9 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): Parameters ---------- predicted_rate: - The mean neural activity. + The mean neural activity. Expected shape: (n_time_bins, n_neurons) y: - The neural activity. + The neural activity. Expected shape: (n_time_bins, n_neurons) Returns ------- @@ -79,16 +78,70 @@ def negative_log_likelihood( self, predicted_rate: jnp.ndarray, y: jnp.ndarray, - ): + ) -> jnp.ndarray: + r"""Compute the Poisson negative log-likelihood. + + This computes the Poisson negative log-likelihood of the predicted rates + for the observed spike counts up to a constant. + + The formula for the Poisson mean log-likelihood is the following, + + $$ + \begin{aligned} + \text{LL}(\hat{\lambda} | y) &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} + [y\_{tn} \log(\hat{\lambda}\_{tn}) - \hat{\lambda}\_{tn} - \log({y\_{tn}!})] \\\ + &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} [y\_{tn} \log(\hat{\lambda}\_{tn}) - + \hat{\lambda}\_{tn} - \Gamma({y\_{tn}+1})] \\\ + &= \frac{1}{T \cdot N} \sum_{n=1}^{N} \sum_{t=1}^{T} [y\_{tn} \log(\hat{\lambda}\_{tn}) - + \hat{\lambda}\_{tn}] + \\text{const} + \end{aligned} + $$ + + Because $\Gamma(k+1)=k!$, see [wikipedia](https://en.wikipedia.org/wiki/Gamma_function) for example. + + Parameters + ---------- + predicted_rate : + The predicted rate of the current model. Shape (n_time_bins, n_neurons). + y : + The target spikes to compare against. Shape (n_time_bins, n_neurons). + + Returns + ------- + : + The Poisson negative log-likehood. Shape (1,). + + Notes + ----- + The $\log({y\_{tn}!})$ term is not a function of the parameters and can be disregarded + when computing the loss-function. This is why we incorporated it into the `const` term. + """ predicted_firing_rates = jnp.clip(predicted_rate, a_min=self.FLOAT_EPS) x = y * jnp.log(predicted_firing_rates) # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) - @staticmethod def emission_probability( - key: KeyArray, predicted_rate: jnp.ndarray, **kwargs + self, key: KeyArray, predicted_rate: jnp.ndarray ) -> jnp.ndarray: + """ + Calculate the emission probability using a Poisson distribution. + + This method generates random numbers from a Poisson distribution based on the given + `predicted_rate`. + + Parameters + ---------- + key : + Random key used for the generation of random numbers in JAX. + predicted_rate : + Expected rate (lambda) of the Poisson distribution. Shape (n_time_bins, n_neurons). + + Returns + ------- + jnp.ndarray + Random numbers generated from the Poisson distribution based on the `predicted_rate`. + """ return jax.random.poisson(key, predicted_rate) def residual_deviance( @@ -99,9 +152,9 @@ def residual_deviance( Parameters ---------- predicted_rate: - The predicted firing rates. + The predicted firing rates. Shape (n_time_bins, n_neurons). spike_counts: - The spike counts. + The spike counts. Shape (n_time_bins, n_neurons). Returns ------- diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index 39d4d254..e7fb8bd4 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -13,7 +13,7 @@ def _norm2_masked(weight_neuron: jnp.ndarray, mask: jnp.ndarray) -> jnp.ndarray: The feature vector for a neuron. Shape (n_features, ). mask: The mask vector for group. mask[i] = 1, if the i-th element of weight_neuron - belongs to the group, 0 otherwise. + belongs to the group, 0 otherwise. Shape (n_features, ) Returns ------- @@ -37,31 +37,33 @@ def _norm2_masked(weight_neuron: jnp.ndarray, mask: jnp.ndarray) -> jnp.ndarray: def prox_group_lasso( params: Tuple[jnp.ndarray, jnp.ndarray], - alpha: float, + regularizer_strength: float, mask: jnp.ndarray, scaling: float = 1.0, ) -> Tuple[jnp.ndarray, jnp.ndarray]: - """Proximal gradient for group lasso. + """Proximal gradient operator for group lasso. Parameters ---------- params: Weights, shape (n_neurons, n_features) - alpha: + regularizer_strength: The regularization hyperparameter. mask: - ND array of 0,1 as float32, feature mask. size (n_groups x n_features) + ND array of 0,1 as float32, feature mask. size (n_groups, n_features) scaling: The scaling factor for the group-lasso (it will be set depending on the step-size). + Returns ------- + : The rescaled weights. """ weights, intercepts = params # returns a (n_neurons, n_groups) matrix of norm 2s. l2_norm = _vmap_norm2_masked_2(weights, mask) - factor = 1 - alpha * scaling / l2_norm + factor = 1 - regularizer_strength * scaling / l2_norm factor = jax.nn.relu(factor) # Avoid shrinkage of features that do not belong to any group # by setting the shrinkage factor to 1. diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index e37e1642..3bd3a209 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -17,18 +17,44 @@ def __dir__() -> list[str]: class Solver(_Base, abc.ABC): + """ + Abstract base class for optimization solvers. + + This class is designed to provide a consistent interface for optimization solvers, + enabling users to easily switch between different solvers and ensure compatibility + with various loss functions and regularization schemes. + + Attributes + ---------- + allowed_optimizers : + List of optimizer names that are allowed for use with this solver. + regularizer_strength : + Strength of the regularization to be applied. + solver_name : + Name of the solver being used. + solver_kwargs : + Additional keyword arguments to be passed to the solver during instantiation. + + Methods + ------- + instantiate_solver(loss) : + Abstract method to instantiate a solver with a given loss function. + get_runner(solver_kwargs, run_kwargs) : + Get the solver runner with provided arguments. + """ + allowed_optimizers = [] def __init__( self, solver_name: str, solver_kwargs: Optional[dict] = None, - alpha: Optional[float] = None, + regularizer_strength: Optional[float] = None, **kwargs, ): super().__init__(**kwargs) self._check_solver(solver_name) - self.alpha = alpha + self.regularizer_strength = regularizer_strength self.solver_name = solver_name if solver_kwargs is None: self.solver_kwargs = dict() @@ -37,6 +63,19 @@ def __init__( self._check_solver_kwargs(self.solver_name, self.solver_kwargs) def _check_solver(self, solver_name: str): + """ + Ensure the provided solver name is allowed. + + Parameters + ---------- + solver_name : + Name of the solver to be checked. + + Raises + ------ + ValueError + If the provided solver name is not in the list of allowed optimizers. + """ if solver_name not in self.allowed_optimizers: raise ValueError( f"Solver `{solver_name}` not allowed for " @@ -46,6 +85,21 @@ def _check_solver(self, solver_name: str): @staticmethod def _check_solver_kwargs(solver_name, solver_kwargs): + """ + Check if provided solver keyword arguments are valid. + + Parameters + ---------- + solver_name : + Name of the solver. + solver_kwargs : + Additional keyword arguments for the solver. + + Raises + ------ + NameError + If any of the solver keyword arguments are not valid. + """ solver_args = inspect.getfullargspec(getattr(jaxopt, solver_name)).args undefined_kwargs = set(solver_kwargs.keys()).difference(solver_args) if undefined_kwargs: @@ -54,10 +108,40 @@ def _check_solver_kwargs(solver_name, solver_kwargs): ) @staticmethod - def _check_is_callable(func): + def _check_is_callable_from_jax(func: Callable): + """ + Check if the provided function is callable and from the jax namespace. + + Ensures that the given function is not only callable, but also belongs to + the `jax` namespace, ensuring compatibility and safety when using jax-based + operations. + + Parameters + ---------- + func : + The function to check. + + Raises + ------ + TypeError + If the provided function is not callable. + ValueError + If the function does not belong to the `jax` or `neurostatslib.glm` namespaces. + """ if not callable(func): raise TypeError("The loss function must a Callable!") + if (not hasattr(func, "__module__")) or ( + not ( + func.__module__.startswith("jax.") + or func.__module__.startswith("neurostatslib.glm") + ) + ): + raise ValueError( + f"The function {func.__name__} is not from the jax namespace. " + "Only functions from the jax namespace are allowed." + ) + @abc.abstractmethod def instantiate_solver( self, @@ -67,6 +151,7 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: + """Abstract method to instantiate a solver with a given loss function.""" pass def get_runner( @@ -76,6 +161,21 @@ def get_runner( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: + """ + Get the solver runner with provided arguments. + + Parameters + ---------- + solver_kwargs : + Additional keyword arguments for the solver instantiation. + run_kwargs : + Additional keyword arguments for the solver run. + + Returns + ------- + : + The solver runner. + """ solver = getattr(jaxopt, self.solver_name)(**solver_kwargs) def solver_run( @@ -116,6 +216,18 @@ def instantiate_solver( class RidgeSolver(Solver): + """ + Solver for Ridge regularization using various optimization algorithms. + + This class uses `jaxopt` optimizers to perform Ridge regularization. It extends + the base Solver class, with the added feature of Ridge penalization. + + Attributes + ---------- + allowed_optimizers : List[..., str] + A list of optimizer names that are allowed to be used with this solver. + """ + allowed_optimizers = [ "GradientDescent", "BFGS", @@ -130,12 +242,34 @@ def __init__( self, solver_name: str = "GradientDescent", solver_kwargs: Optional[dict] = None, - alpha: float = 1.0, + regularizer_strength: float = 1.0, ): - super().__init__(solver_name, solver_kwargs=solver_kwargs, alpha=alpha) + super().__init__( + solver_name, + solver_kwargs=solver_kwargs, + regularizer_strength=regularizer_strength, + ) - def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]): - return 0.5 * self.alpha * jnp.sum(jnp.power(params[0], 2)) / params[1].shape[0] + def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: + """ + Compute the Ridge penalization for given parameters. + + Parameters + ---------- + params : + Model parameters for which to compute the penalization. + + Returns + ------- + float + The Ridge penalization value. + """ + return ( + 0.5 + * self.regularizer_strength + * jnp.sum(jnp.power(params[0], 2)) + / params[1].shape[0] + ) def instantiate_solver( self, @@ -145,7 +279,20 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_is_callable(loss) + """ + Instantiate the solver with a penalized loss function. + + Parameters + ---------- + loss : + The original loss function to be optimized. + + Returns + ------- + Callable + A function that runs the solver with the penalized loss. + """ + self._check_is_callable_from_jax(loss) def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) @@ -156,16 +303,34 @@ def penalized_loss(params, X, y): class ProxGradientSolver(Solver, abc.ABC): + """ + Solver for optimization using the Proximal Gradient method. + + This class utilizes the `jaxopt` library's Proximal Gradient optimizer. It extends + the base Solver class, with the added functionality of a proximal operator. + + Attributes + ---------- + allowed_optimizers : List[...,str] + A list of optimizer names that are allowed to be used with this solver. + mask : Optional[Union[NDArray, jnp.ndarray]] + An optional mask array for element-wise operations. Shape (n_groups, n_features) + """ + allowed_optimizers = ["ProximalGradient"] def __init__( self, solver_name: str, solver_kwargs: Optional[dict] = None, - alpha: float = 1.0, + regularizer_strength: float = 1.0, mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): - super().__init__(solver_name, solver_kwargs=solver_kwargs, alpha=alpha) + super().__init__( + solver_name, + solver_kwargs=solver_kwargs, + regularizer_strength=regularizer_strength, + ) self.mask = mask @abc.abstractmethod @@ -174,6 +339,14 @@ def get_prox_operator( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] ]: + """ + Abstract method to retrieve the proximal operator for this solver. + + Returns + ------- + : + The proximal operator, which typically applies a form of regularization. + """ pass def instantiate_solver( @@ -184,27 +357,50 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: - self._check_is_callable(loss) + """ + Instantiate the solver with the provided loss function and proximal operator. + + Parameters + ---------- + loss : + The original loss function to be optimized. + + Returns + ------- + : + A function that runs the solver with the provided loss and proximal operator. + """ + self._check_is_callable_from_jax(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss solver_kwargs["prox"] = self.get_prox_operator() - run_kwargs = dict(hyperparams_prox=self.alpha) + run_kwargs = dict(hyperparams_prox=self.regularizer_strength) return self.get_runner(solver_kwargs, run_kwargs) class LassoSolver(ProxGradientSolver): + """ + Solver for optimization using the Lasso (L1 regularization) method with Proximal Gradient. + + This class is a specialized version of the ProxGradientSolver with the proximal operator + set for L1 regularization (Lasso). It utilizes the `jaxopt` library's proximal gradient optimizer. + """ + def __init__( self, solver_name: str = "ProximalGradient", solver_kwargs: Optional[dict] = None, - alpha: float = 1.0, + regularizer_strength: float = 1.0, mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): super().__init__( - solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask + solver_name, + solver_kwargs=solver_kwargs, + regularizer_strength=regularizer_strength, + mask=mask, ) def get_prox_operator( @@ -212,6 +408,16 @@ def get_prox_operator( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] ]: + """ + Retrieve the proximal operator for Lasso regularization (L1 penalty). + + Returns + ------- + : + The proximal operator, applying L1 regularization to the provided parameters. The intercept + term is not regularized. + """ + def prox_op(params, l1reg, scaling=1.0): Ws, bs = params return jaxopt.prox.prox_lasso(Ws, l1reg, scaling=scaling), bs @@ -220,19 +426,60 @@ def prox_op(params, l1reg, scaling=1.0): class GroupLassoSolver(ProxGradientSolver): + """ + Solver for optimization using the Group Lasso regularization method with Proximal Gradient. + + This class is a specialized version of the ProxGradientSolver with the proximal operator + set for Group Lasso regularization. The Group Lasso regularization induces sparsity on groups + of features rather than individual features. + + Attributes + ---------- + mask : Union[jnp.ndarray, NDArray] + A mask array indicating groups of features for regularization. + Each row represents a group of features. + Each column corresponds to a feature, where a value of 1 indicates that the feature belongs + to the group, and a value of 0 indicates it doesn't. + + Methods + ------- + _check_mask(): + Validate the mask array to ensure it meets the requirements for Group Lasso regularization. + get_prox_operator(): + Retrieve the proximal operator for Group Lasso regularization. + """ + def __init__( self, solver_name: str, mask: Union[jnp.ndarray, NDArray], solver_kwargs: Optional[dict] = None, - alpha: float = 1.0, + regularizer_strength: float = 1.0, ): super().__init__( - solver_name, solver_kwargs=solver_kwargs, alpha=alpha, mask=mask + solver_name, + solver_kwargs=solver_kwargs, + regularizer_strength=regularizer_strength, + mask=mask, ) self._check_mask() def _check_mask(self): + """ + Validate the mask array. + + This method ensures the mask adheres to requirements: + - It should be 2-dimensional. + - Each element must be either 0 or 1. + - Each feature should belong to only one group. + - The mask should not be empty. + - The mask is an array of float type. + + Raises + ------ + ValueError + If any of the above conditions are not met. + """ if self.mask.ndim != 2: raise ValueError( "`mask` must be 2-dimensional. " @@ -265,7 +512,19 @@ def get_prox_operator( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] ]: - def prox_op(params, alpha, scaling=1.0): - return prox_group_lasso(params, alpha, mask=self.mask, scaling=scaling) + """ + Retrieve the proximal operator for Group Lasso regularization. + + Returns + ------- + : + The proximal operator, applying Group Lasso regularization to the provided parameters. The + intercept term is not regularized. + """ + + def prox_op(params, regularizer_strength, scaling=1.0): + return prox_group_lasso( + params, regularizer_strength, mask=self.mask, scaling=scaling + ) return prox_op diff --git a/tests/conftest.py b/tests/conftest.py index b284ed41..4bc0cbe5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,7 @@ def poissonGLM_coupled_model_config_simulate(): config_dict = yaml.safe_load(fh) noise = nsl.noise_model.PoissonNoiseModel(jnp.exp) - solver = nsl.solver.RidgeSolver("BFGS", alpha=0.1) + solver = nsl.solver.RidgeSolver("BFGS", regularizer_strength=0.1) model = nsl.glm.GLMRecurrent(noise_model=noise, solver=solver) model.basis_coeff_ = jnp.asarray(config_dict["basis_coeff_"]) model.baseline_link_fr_ = jnp.asarray(config_dict["baseline_link_fr_"]) @@ -141,11 +141,11 @@ def example_data_prox_operator(): n_features = 4 params = (jnp.ones((n_neurons, n_features)), jnp.zeros(n_neurons)) - alpha = 0.1 + regularizer_strength = 0.1 mask = jnp.array([[1, 0, 1, 0], [0, 1, 0, 1]], dtype=jnp.float32) scaling = 0.5 - return params, alpha, mask, scaling + return params, regularizer_strength, mask, scaling @pytest.fixture def poisson_noise_model(): @@ -154,12 +154,12 @@ def poisson_noise_model(): @pytest.fixture def ridge_solver(): - return nsl.solver.RidgeSolver(solver_name="LBFGS", alpha=0.1) + return nsl.solver.RidgeSolver(solver_name="LBFGS", regularizer_strength=0.1) @pytest.fixture def lasso_solver(): - return nsl.solver.LassoSolver(solver_name="ProximalGradient", alpha=0.1) + return nsl.solver.LassoSolver(solver_name="ProximalGradient", regularizer_strength=0.1) @pytest.fixture @@ -167,4 +167,9 @@ def group_lasso_2groups_5features_solver(): mask = np.zeros((2, 5)) mask[0, :2] = 1 mask[1, 2:] = 1 - return nsl.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask, alpha=0.1) + return nsl.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) + + +@pytest.fixture +def mock_data(): + return jnp.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]), jnp.array([[1, 2], [3, 4]]) diff --git a/tests/test_base_class.py b/tests/test_base_class.py index c9ac98df..690c3ab8 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -1,14 +1,18 @@ from typing import Union import jax.numpy as jnp +import jax import pytest from numpy.typing import NDArray -from neurostatslib.base_class import _BaseRegressor +from neurostatslib.base_class import BaseRegressor +@pytest.fixture +def mock_regressor(): + return MockBaseRegressor() # Sample subclass to test instantiation and methods -class MockBaseRegressor(_BaseRegressor): +class MockBaseRegressor(BaseRegressor): """ Mock implementation of the BaseRegressor abstract class for testing purposes. Implements all required abstract methods as empty methods. @@ -24,22 +28,24 @@ def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass - def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + def score( + self, + X: Union[NDArray, jnp.ndarray], + y: Union[NDArray, jnp.ndarray], + **kwargs, + ) -> jnp.ndarray: pass def simulate( self, - random_key, - n_timesteps, - init_spikes, - coupling_basis_matrix, - feedforward_input=None, - device="cpu" - ) -> jnp.ndarray: + random_key: jax.random.PRNGKeyArray, + feed_forward_input: Union[NDArray, jnp.ndarray], + **kwargs, + ): pass -class MockBaseRegressor_Invalid(_BaseRegressor): +class MockBaseRegressor_Invalid(BaseRegressor): """ Mock model that intentionally doesn't implement all the required abstract methods. Used for testing the instantiation of incomplete concrete classes. @@ -81,6 +87,7 @@ def set_params(): assert model.param1 == "changed" assert model.std_param == 1 + def test_invalid_set_params(): """Test invalid parameter setting using the set_params method.""" model = MockBaseRegressor() @@ -98,7 +105,7 @@ def test_get_param_names(): def test_convert_to_jnp_ndarray(): """Test data conversion to JAX NumPy arrays.""" data = [1, 2, 3] - jnp_data, = _BaseRegressor._convert_to_jnp_ndarray(data) + jnp_data, = BaseRegressor._convert_to_jnp_ndarray(data) assert isinstance(jnp_data, jnp.ndarray) assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) @@ -107,15 +114,16 @@ def test_has_invalid_entry(): """Test validation of data arrays.""" valid_data = jnp.array([1, 2, 3]) invalid_data = jnp.array([1, 2, jnp.nan]) - assert not _BaseRegressor._has_invalid_entry(valid_data) - assert _BaseRegressor._has_invalid_entry(invalid_data) + assert not BaseRegressor._has_invalid_entry(valid_data) + assert BaseRegressor._has_invalid_entry(invalid_data) # To ensure abstract methods aren't callable def test_abstract_class(): """Ensure that abstract methods aren't callable.""" with pytest.raises(TypeError, match="Can't instantiate abstract"): - _BaseRegressor() + BaseRegressor() + def test_invalid_concrete_class(): """Ensure that classes missing implementation of required abstract methods raise errors.""" @@ -123,4 +131,147 @@ def test_invalid_concrete_class(): model = MockBaseRegressor_Invalid() +def test_preprocess_fit(mock_data, mock_regressor): + X, y = mock_data + X_out, y_out, params_out = mock_regressor.preprocess_fit(X, y) + assert X_out.shape == X.shape + assert y_out.shape == y.shape + assert params_out[0].shape == (2, 2) # Mock data shapes + assert params_out[1].shape == (2,) + + +def test_preprocess_fit_empty_data(mock_regressor): + """Test behavior with empty data input.""" + X, y = jnp.array([[]]), jnp.array([]) + with pytest.raises(ValueError): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_mismatched_shapes(mock_regressor): + """Test behavior with mismatched X and y shapes.""" + X = jnp.array([[1, 2], [3, 4]]) + y = jnp.array([1, 2, 3]) + with pytest.raises(ValueError): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_invalid_datatypes(mock_regressor): + """Test behavior with invalid data types.""" + X = "invalid_data_type" + y = "invalid_data_type" + with pytest.raises(TypeError): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_with_nan_in_X(mock_regressor): + """Test behavior with NaN values in data.""" + X = jnp.array([[[1, 2], [jnp.nan, 4]]]) + y = jnp.array([[1, 2]]) + with pytest.raises(ValueError, match="Input X contains a NaNs or Infs"): + mock_regressor.preprocess_fit(X, y) + +def test_preprocess_fit_with_inf_in_X(mock_regressor): + """Test behavior with inf values in data.""" + X = jnp.array([[[1, 2], [jnp.inf, 4]]]) + y = jnp.array([[1, 2]]) + with pytest.raises(ValueError, match="Input X contains a NaNs or Infs"): + mock_regressor.preprocess_fit(X, y) + +def test_preprocess_fit_with_nan_in_y(mock_regressor): + """Test behavior with NaN values in data.""" + X = jnp.array([[[1, 2], [2, 4]]]) + y = jnp.array([[1, jnp.nan]]) + with pytest.raises(ValueError, match="Input y contains a NaNs or Infs"): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_with_inf_in_y(mock_regressor): + """Test behavior with inf values in data.""" + X = jnp.array([[[1, 2], [2, 4]]]) + y = jnp.array([[1, jnp.inf]]) + with pytest.raises(ValueError, match="Input y contains a NaNs or Infs"): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_higher_dimensional_data_X(mock_regressor): + """Test behavior with higher-dimensional input data.""" + X = jnp.array([[[[1, 2], [3, 4]]]]) + y = jnp.array([[1, 2]]) + with pytest.raises(ValueError, match="X must be three-dimensional"): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_higher_dimensional_data_y(mock_regressor): + """Test behavior with higher-dimensional input data.""" + X = jnp.array([[[[1, 2], [3, 4]]]]) + y = jnp.array([[[1, 2]]]) + with pytest.raises(ValueError, match="y must be two-dimensional"): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_lower_dimensional_data_X(mock_regressor): + """Test behavior with lower-dimensional input data.""" + X = jnp.array([[1, 2], [3, 4]]) + y = jnp.array([[1, 2]]) + with pytest.raises(ValueError, match="X must be three-dimensional"): + mock_regressor.preprocess_fit(X, y) + + +def test_preprocess_fit_lower_dimensional_data_y(mock_regressor): + """Test behavior with lower-dimensional input data.""" + X = jnp.array([[[[1, 2], [3, 4]]]]) + y = jnp.array([1, 2]) + with pytest.raises(ValueError, match="y must be two-dimensional"): + mock_regressor.preprocess_fit(X, y) + + +# Preprocess Simulate Tests +def test_preprocess_simulate_empty_data(mock_regressor): + """Test behavior with empty feedforward_input.""" + feedforward_input = jnp.array([[[]]]) + params_f = (jnp.array([[]]), jnp.array([])) + with pytest.raises(ValueError, match="Model parameters have inconsistent shapes."): + mock_regressor.preprocess_simulate(feedforward_input, params_f) + + +def test_preprocess_simulate_invalid_datatypes(mock_regressor): + """Test behavior with invalid feedforward_input datatype.""" + feedforward_input = "invalid_data_type" + params_f = (jnp.array([[]]),) + with pytest.raises(TypeError, match="Value 'invalid_data_type' with dtype .+ is not a valid JAX array type."): + mock_regressor.preprocess_simulate(feedforward_input, params_f) + + +def test_preprocess_simulate_with_nan(mock_regressor): + """Test behavior with NaN values in feedforward_input.""" + feedforward_input = jnp.array([[[jnp.nan]]]) + params_f = (jnp.array([[1]]), jnp.array([1])) + with pytest.raises(ValueError, match="feedforward_input contains a NaNs or Infs!"): + mock_regressor.preprocess_simulate(feedforward_input, params_f) + + +def test_preprocess_simulate_with_inf(mock_regressor): + """Test behavior with infinite values in feedforward_input.""" + feedforward_input = jnp.array([[[jnp.inf]]]) + params_f = (jnp.array([[1]]), jnp.array([1])) + with pytest.raises(ValueError, match="feedforward_input contains a NaNs or Infs!"): + mock_regressor.preprocess_simulate(feedforward_input, params_f) + + +def test_preprocess_simulate_higher_dimensional_data(mock_regressor): + """Test behavior with improperly dimensional feedforward_input.""" + feedforward_input = jnp.array([[[[1]]]]) + params_f = (jnp.array([[1]]), jnp.array([1])) + with pytest.raises(ValueError, match="X must be three-dimensional"): + mock_regressor.preprocess_simulate(feedforward_input, params_f) + + +def test_preprocess_simulate_invalid_init_y(mock_regressor): + """Test behavior with invalid init_y provided.""" + feedforward_input = jnp.array([[[1]]]) + params_f = (jnp.array([[1]]), jnp.array([1])) + init_y = jnp.array([[[1]]]) + params_r = (jnp.array([[1]]),) + with pytest.raises(ValueError, match="y must be two-dimensional"): + mock_regressor.preprocess_simulate(feedforward_input, params_f, init_y, params_r) diff --git a/tests/test_glm.py b/tests/test_glm.py index db56e1a8..34b9503f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -362,35 +362,6 @@ def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_in y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) - @pytest.mark.parametrize("device_spec, error, match_str", - [ - ("cpu", None, None), - ("tpu", None, None), - ("gpu", None, None), - ("none", ValueError, "Invalid device specification: %s"), - (1, ValueError, "Invalid device specification: %s") - ] - ) - def test_fit_device_spec(self, device_spec, error, match_str, - poissonGLM_model_instantiation): - """ - Test `simulate` across different device specifications. - Validates if unsupported or absent devices raise exception - or warning respectively. - """ - if match_str is not None: - match_str = match_str % str(device_spec) - raise_warning = all(device_spec != device.device_kind.lower() - for device in jax.local_devices()) - raise_warning = raise_warning and (error is not ValueError) - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - if raise_warning: - with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): - model.fit(X, y, init_params=true_params, device=device_spec) - else: - _test_class_method(model, "fit", [X, y], {"init_params": true_params, "device":device_spec}, - error, match_str) - ####################### # Test model.score ####################### @@ -717,9 +688,7 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, error, match_str, "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str @@ -755,8 +724,7 @@ def test_simulate_input_dimensionality(self, delta_dim, error, match_str, "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" + "feedforward_input": feedforward_input }, error, @@ -795,8 +763,6 @@ def test_simulate_y_dimensionality(self, delta_dim, error, match_str, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, "feedforward_input": feedforward_input, - "device": "cpu" - }, error, match_str @@ -828,9 +794,7 @@ def test_simulate_n_neuron_match_y(self, delta_n_neuron, error, match_str, "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str @@ -860,9 +824,7 @@ def test_simulate_is_fit(self, is_fit, error, match_str, "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str @@ -895,9 +857,7 @@ def test_simulate_time_point_match_y(self, delta_tp, error, match_str, "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str @@ -930,9 +890,7 @@ def test_simulate_time_point_match_coupling_basis(self, delta_tp, error, match_s "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str @@ -971,9 +929,7 @@ def test_simulate_feature_consistency_input(self, delta_features, error, match_s "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str @@ -1010,64 +966,12 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, error "random_key": random_key, "init_y": init_spikes, "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": "cpu" - + "feedforward_input": feedforward_input }, error, match_str ) - @pytest.mark.parametrize("device_spec, error, match_str", - [ - ("cpu", None, None), - ("tpu", None, None), - ("gpu", None, None), - ("none", ValueError, "Invalid device specification: %s"), - (1, ValueError, "Invalid device specification: %s") - ] - ) - def test_simulate_device_spec(self, device_spec, error, match_str, - poissonGLM_coupled_model_config_simulate): - """ - Test `simulate` across different device specifications. - Validates if unsupported or absent devices raise exception - or warning respectively. - """ - if match_str is not None: - match_str = match_str % str(device_spec) - - raise_warning = all(device_spec != device.device_kind.lower() - for device in jax.local_devices()) - raise_warning = raise_warning and (error is None) - - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - - if raise_warning: - with pytest.warns(UserWarning, match=f"No {device_spec.upper()} found"): - model.simulate(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device=device_spec) - else: - _test_class_method( - model, - "simulate", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - "device": device_spec - - }, - error, - match_str - ) - ####################################### # Compare with standard implementation ####################################### @@ -1121,8 +1025,7 @@ def test_end_to_end_fit_and_simulate(self, spikes, _ = model.simulate(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") + feedforward_input=feedforward_input) # convolve basis and spikes # (n_trials, n_timepoints - ws + 1, n_neurons, n_coupling_basis) @@ -1154,8 +1057,7 @@ def test_end_to_end_fit_and_simulate(self, model.simulate(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input, - device="cpu") + feedforward_input=feedforward_input) diff --git a/tests/test_solver.py b/tests/test_solver.py index b3dae4ae..86a6614e 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -49,8 +49,8 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [jnp.exp, np.exp, 1, None, {}]) - def test_loss_callable(self, loss): + @pytest.mark.parametrize("loss", [jnp.exp, 1, None, {}]) + def test_loss_is_callable(self, loss): """Test that the loss function is a callable""" raise_exception = not callable(loss) if raise_exception: @@ -59,6 +59,18 @@ def test_loss_callable(self, loss): else: self.cls("GradientDescent").instantiate_solver(loss) + @pytest.mark.parametrize("loss", [jnp.exp, np.exp, nsl.glm.GLM()._score]) + def test_loss_type_jax_or_glm(self, loss): + """Test that the loss function is a callable""" + raise_exception = (not hasattr(loss, "__module__")) or \ + (not (loss.__module__.startswith("jax.") or + loss.__module__.startswith("neurostatslib.glm"))) + if raise_exception: + with pytest.raises(ValueError, match=f"The function {loss.__name__} is not from the jax namespace."): + self.cls("GradientDescent").instantiate_solver(loss) + else: + self.cls("GradientDescent").instantiate_solver(loss) + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) def test_run_solver(self, solver_name, poissonGLM_model_instantiation): """Test that the solver runs.""" @@ -96,7 +108,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): solver = self.cls("GradientDescent", {"tol": 10**-12}) runner_bfgs = solver.instantiate_solver(model._score) weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] - model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=solver.alpha) + model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=solver.regularizer_strength) model_skl.fit(X[:,0], y[:, 0]) match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) @@ -164,7 +176,7 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): family=sm.families.Poisson()) # regularize everything except intercept - alpha_sm = np.ones(X.shape[2] + 1) * solver.alpha + alpha_sm = np.ones(X.shape[2] + 1) * solver.regularizer_strength alpha_sm[0] = 0 # pure lasso = elastic net with L1 weight = 1 From 0c2d173f91e5d8b5531dc9f96f2b08d4eae3b0d7 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 09:18:29 -0400 Subject: [PATCH 073/250] fixed deprecations --- tests/test_basis.py | 2 +- tests/test_convolution_1d.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_basis.py b/tests/test_basis.py index 4d3d023a..733d42fd 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -225,7 +225,7 @@ def test_minimum_number_of_basis_required_is_matched(self, n_basis_funcs): raise_exception = n_basis_funcs < 1 if raise_exception: with pytest.raises(ValueError, match=f"Object class {self.cls.__name__} " - "requires >= 1 basis elements\."): + r"requires >= 1 basis elements\."): self.cls(n_basis_funcs=n_basis_funcs) else: self.cls(n_basis_funcs=n_basis_funcs) diff --git a/tests/test_convolution_1d.py b/tests/test_convolution_1d.py index e0616e11..cdd42a62 100644 --- a/tests/test_convolution_1d.py +++ b/tests/test_convolution_1d.py @@ -11,7 +11,7 @@ def test_basis_matrix_type(self, basis_matrix, trial_count_shape: tuple[int]): vec = np.ones(trial_count_shape) raise_exception = any(k == 0 for k in basis_matrix.shape) if raise_exception: - with pytest.raises(ValueError, match="Empty basis_matrix provided\. " + with pytest.raises(ValueError, match=r"Empty basis_matrix provided\. " r"The shape of basis_matrix is \(0, 0\)!"): utils.convolve_1d_trials(basis_matrix, vec) else: From 788f3d482091685c941fa4ec698582d241935b05 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 11:00:32 -0400 Subject: [PATCH 074/250] added tests for corner cases --- src/neurostatslib/base_class.py | 11 +++--- src/neurostatslib/glm.py | 14 -------- tests/conftest.py | 5 ++- tests/test_base_class.py | 63 +++++++++++++++++++++++++++++++-- tests/test_glm.py | 31 ++++++++++++++-- tests/test_solver.py | 4 +-- 6 files changed, 100 insertions(+), 28 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index b103ef0e..fef2828f 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -13,7 +13,6 @@ from .utils import has_local_device - class _Base: """Base class for neurostatslib estimators. @@ -153,15 +152,13 @@ def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Dev ValueError If the an invalid device name is provided. """ - if device == "cpu": - target_device = jax.devices(device)[0] - elif (device == "gpu") or (device == "tpu"): + if device in ["cpu", "gpu", "tpu"]: if has_local_device(device): - # assume for now 1 gpu/tpu (no further parallelization) target_device = jax.devices(device)[0] else: - warnings.warn(f"No {device.upper()} found! Falling back to CPU") - target_device = jax.devices("cpu")[0] + raise RuntimeError(f"Unknown backend: '{device}' requested, but no " + f"platforms that are instances of {device} are present.") + else: raise ValueError( f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`." diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index b8afd216..b078ef35 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -466,20 +466,6 @@ def simulate( feedforward_input, params_f=(Wf, bs), init_y=init_y, params_r=(Wr, bs) ) - if ( - feedforward_input.shape[2] + coupling_basis_matrix.shape[1] * bs.shape[0] - != self.basis_coeff_.shape[1] - ): - raise ValueError( - "The number of feed forward input features " - "and the number of recurrent features must add up to " - "the overall model features." - f"The total number of feature of the model is {self.basis_coeff_.shape[1]}." - f" {feedforward_input.shape[1]} " - f"feedforward features and {coupling_basis_matrix.shape[1]} recurrent features " - f"provided instead." - ) - self._check_input_and_params_consistency( (Wr, bs), y=init_y, diff --git a/tests/conftest.py b/tests/conftest.py index 4bc0cbe5..d914f6e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -128,11 +128,14 @@ def group_sparse_poisson_glm_model_instantiation(): b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) w_true[0, 1:4] = 0. + mask = np.zeros((2, 5)) + mask[0, 1:4] = 1 + mask[1, [0,4]] = 1 noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) - return X, np.random.poisson(rate), model, (w_true, b_true), rate + return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask @pytest.fixture diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 690c3ab8..53d4a398 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -5,12 +5,16 @@ import pytest from numpy.typing import NDArray -from neurostatslib.base_class import BaseRegressor +from neurostatslib.base_class import BaseRegressor, _Base + +import neurostatslib as nsl + @pytest.fixture def mock_regressor(): return MockBaseRegressor() + # Sample subclass to test instantiation and methods class MockBaseRegressor(BaseRegressor): """ @@ -62,6 +66,12 @@ def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) pass +class BadEstimator(_Base): + def __init__(self, param1, *args): + super().__init__() + pass + + def test_init(): """Test the initialization of the MockBaseRegressor class.""" model = MockBaseRegressor(param1="test", param2=2) @@ -91,7 +101,7 @@ def set_params(): def test_invalid_set_params(): """Test invalid parameter setting using the set_params method.""" model = MockBaseRegressor() - with pytest.raises(ValueError): + with pytest.raises(ValueError, match="Invalid parameter 'invalid_param' for estimator"): model.set_params(invalid_param="invalid") @@ -275,3 +285,52 @@ def test_preprocess_simulate_invalid_init_y(mock_regressor): params_r = (jnp.array([[1]]),) with pytest.raises(ValueError, match="y must be two-dimensional"): mock_regressor.preprocess_simulate(feedforward_input, params_f, init_y, params_r) + +def test_preprocess_simulate_feedforward(mock_regressor): + """Test that the preprocessing works.""" + feedforward_input = jnp.array([[[1]]]) + params_f = (jnp.array([[1]]), jnp.array([1])) + ff, = mock_regressor.preprocess_simulate(feedforward_input, params_f) + assert(jnp.all(ff == feedforward_input)) + +def test_empty_set(mock_regressor): + """Check that an empty set_params returns self. + """ + assert mock_regressor.set_params() is mock_regressor + +@pytest.mark.parametrize("device_name", [1, "none"]) +def test_target_device_invalid_device_name(device_name, mock_regressor): + with pytest.raises(ValueError, match="Invalid device specification"): + mock_regressor.select_target_device(device_name) + +@pytest.mark.parametrize("device_name", ["cpu", "gpu", "tpu"]) +def test_target_device_availability(device_name, mock_regressor): + raise_exception = not nsl.utils.has_local_device(device_name) + if raise_exception: + with pytest.raises(RuntimeError, match=f"Unknown backend: '{device_name}' requested, but no "): + mock_regressor.select_target_device(device_name) + else: + mock_regressor.select_target_device(device_name) + +@pytest.mark.parametrize("device_name", ["cpu", "gpu", "tpu"]) +def test_target_device_put(device_name, mock_regressor): + """Test that put works. + + Put array to device and checks that the device is matched after put, if device is found. + Raise error otherwise. + """ + raise_exception = not nsl.utils.has_local_device(device_name) + x = jnp.array([1]) + if raise_exception: + with pytest.raises(RuntimeError, match=f"Unknown backend: '{device_name}' requested, but no "): + mock_regressor.device_put(x, device=device_name) + else: + x, = mock_regressor.device_put(x, device=device_name) + assert x.device().device_kind == device_name + + +def test_glm_varargs_error(): + """Test that variable number of argument in __init__ is not allowed.""" + bad_estimator = BadEstimator(1) + with pytest.raises(RuntimeError, match="GLM estimators should always specify their parameters"): + bad_estimator._get_param_names() diff --git a/tests/test_glm.py b/tests/test_glm.py index 34b9503f..4e26bae4 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -63,7 +63,6 @@ def test_init_noise_type(self, noise, error, match_str, ridge_solver): """ _test_class_initialization(self.cls, {'solver': ridge_solver, 'noise_model': noise}, error, match_str) - ####################### # Test model.fit ####################### @@ -319,7 +318,6 @@ def test_fit_n_feature_consistency_x(self, delta_n_features, error, match_str, p Test the `fit` method for inconsistencies between data features and model's expectations. Ensure the number of features in X aligns. """ - raise_exception = delta_n_features != 0 X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if delta_n_features == 1: @@ -362,6 +360,12 @@ def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_in y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation): + """Test that the group lasso fit goes through""" + X, y, model, params, rate, mask = group_sparse_poisson_glm_model_instantiation + model.set_params(solver=nsl.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask)) + model.fit(X, y) + ####################### # Test model.score ####################### @@ -972,6 +976,29 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, error match_str ) + def test_simulate_feedforward_GLM_not_fit(self, poissonGLM_model_instantiation): + X, y, model, params, rate = poissonGLM_model_instantiation + with pytest.raises(nsl.exceptions.NotFittedError, + match="This GLM instance is not fitted yet"): + model.simulate(jax.random.PRNGKey(123), X) + + def test_simulate_feedforward_GLM(self, poissonGLM_model_instantiation): + """Test that simulate goes through""" + X, y, model, params, rate = poissonGLM_model_instantiation + model.basis_coeff_ = params[0] + model.baseline_link_fr_ = params[1] + ysim, ratesim = model.simulate(jax.random.PRNGKey(123), X) + # check that the expected dimensionality is returned + assert ysim.ndim == 2 + assert ratesim.ndim == 2 + # check that the rates and spikes has the same shape + assert ratesim.shape[0] == ysim.shape[0] + assert ratesim.shape[1] == ysim.shape[1] + # check the time point number is that expected (same as the input) + assert ysim.shape[0] == X.shape[0] + # check that the number if neurons is respected + assert ysim.shape[1] == y.shape[1] + ####################################### # Compare with standard implementation ####################################### diff --git a/tests/test_solver.py b/tests/test_solver.py index 86a6614e..40a89a3a 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -268,7 +268,7 @@ def test_mask_validity_groups(self, group_sparse_poisson_glm_model_instantiation): """Test that mask assigns at most 1 group to each weight.""" raise_exception = n_groups_assign > 1 - X, y, model, true_params, firing_rate = group_sparse_poisson_glm_model_instantiation + X, y, model, true_params, firing_rate, _ = group_sparse_poisson_glm_model_instantiation # create a valid mask mask = np.zeros((2, X.shape[2])) @@ -364,7 +364,7 @@ def test_mask_n_groups(self, n_groups, poissonGLM_model_instantiation): def test_group_sparsity_enforcement(self, group_sparse_poisson_glm_model_instantiation): """Test that group lasso works on a simple dataset.""" - X, y, model, true_params, firing_rate = group_sparse_poisson_glm_model_instantiation + X, y, model, true_params, firing_rate, _ = group_sparse_poisson_glm_model_instantiation zeros_true = true_params[0].flatten() == 0 mask = np.zeros((2, X.shape[2])) mask[0, zeros_true] = 1 From ee00d13af34a4112edf3a9e8489ad7566415b83e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 11:50:21 -0400 Subject: [PATCH 075/250] set solver_name and solver_kwargs as propertied --- src/neurostatslib/base_class.py | 7 +- src/neurostatslib/solver.py | 124 +++++++++++++++--------- tests/test_solver.py | 167 ++++++++++++++++++++++++++++++++ 3 files changed, 248 insertions(+), 50 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index fef2828f..81468539 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -13,6 +13,7 @@ from .utils import has_local_device + class _Base: """Base class for neurostatslib estimators. @@ -156,8 +157,10 @@ def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Dev if has_local_device(device): target_device = jax.devices(device)[0] else: - raise RuntimeError(f"Unknown backend: '{device}' requested, but no " - f"platforms that are instances of {device} are present.") + raise RuntimeError( + f"Unknown backend: '{device}' requested, but no " + f"platforms that are instances of {device} are present." + ) else: raise ValueError( diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 3bd3a209..4e771ef1 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -55,13 +55,31 @@ def __init__( super().__init__(**kwargs) self._check_solver(solver_name) self.regularizer_strength = regularizer_strength - self.solver_name = solver_name + self._solver_name = solver_name if solver_kwargs is None: - self.solver_kwargs = dict() + self._solver_kwargs = dict() else: - self.solver_kwargs = solver_kwargs + self._solver_kwargs = solver_kwargs self._check_solver_kwargs(self.solver_name, self.solver_kwargs) + @property + def solver_name(self): + return self._solver_name + + @solver_name.setter + def solver_name(self, solver_name: str): + self._check_solver(solver_name) + self._solver_name = solver_name + + @property + def solver_kwargs(self): + return self._solver_kwargs + + @solver_kwargs.setter + def solver_kwargs(self, solver_kwargs: dict): + self._check_solver_kwargs(self.solver_name, solver_kwargs) + return self._solver_kwargs + def _check_solver(self, solver_name: str): """ Ensure the provided solver name is allowed. @@ -331,7 +349,60 @@ def __init__( solver_kwargs=solver_kwargs, regularizer_strength=regularizer_strength, ) - self.mask = mask + self._mask = mask + + @property + def mask(self): + return self._mask + + @mask.setter + def mask(self, mask: jnp.ndarray): + self._check_mask(mask) + self._mask = mask + + @staticmethod + def _check_mask(mask: Optional[jnp.ndarray] = None): + """ + Validate the mask array. + + This method ensures the mask adheres to requirements: + - It should be 2-dimensional. + - Each element must be either 0 or 1. + - Each feature should belong to only one group. + - The mask should not be empty. + - The mask is an array of float type. + + Raises + ------ + ValueError + If any of the above conditions are not met. + """ + if mask.ndim != 2: + raise ValueError( + "`mask` must be 2-dimensional. " + f"{mask.ndim} dimensional mask provided instead!" + ) + + if mask.shape[0] == 0: + raise ValueError(f"Empty mask provided! Mask has shape {mask.shape}.") + + if jnp.any((mask != 1) & (mask != 0)): + raise ValueError("Mask elements be 0s and 1s!") + + if mask.sum() == 0: + raise ValueError("Empty mask provided!") + + if jnp.any(mask.sum(axis=0) > 1): + raise ValueError( + "Incorrect group assignment. Some of the features are assigned " + "to more then one group." + ) + + if not jnp.issubdtype(mask.dtype, jnp.floating): + raise ValueError( + "Mask should be a floating point jnp.ndarray. " + f"Data type {mask.dtype} provided instead!" + ) @abc.abstractmethod def get_prox_operator( @@ -462,50 +533,7 @@ def __init__( regularizer_strength=regularizer_strength, mask=mask, ) - self._check_mask() - - def _check_mask(self): - """ - Validate the mask array. - - This method ensures the mask adheres to requirements: - - It should be 2-dimensional. - - Each element must be either 0 or 1. - - Each feature should belong to only one group. - - The mask should not be empty. - - The mask is an array of float type. - - Raises - ------ - ValueError - If any of the above conditions are not met. - """ - if self.mask.ndim != 2: - raise ValueError( - "`mask` must be 2-dimensional. " - f"{self.mask.ndim} dimensional mask provided instead!" - ) - - if self.mask.shape[0] == 0: - raise ValueError(f"Empty mask provided! Mask has shape {self.mask.shape}.") - - if jnp.any((self.mask != 1) & (self.mask != 0)): - raise ValueError("Mask elements be 0s and 1s!") - - if self.mask.sum() == 0: - raise ValueError("Empty mask provided!") - - if jnp.any(self.mask.sum(axis=0) > 1): - raise ValueError( - "Incorrect group assignment. Some of the features are assigned " - "to more then one group." - ) - - if not jnp.issubdtype(self.mask.dtype, jnp.floating): - raise ValueError( - "Mask should be a floating point jnp.ndarray. " - f"Data type {self.mask.dtype} provided instead!" - ) + self._check_mask(mask) def get_prox_operator( self, diff --git a/tests/test_solver.py b/tests/test_solver.py index 40a89a3a..3a1362f5 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -37,6 +37,26 @@ def test_init_solver_name(self, solver_name): else: self.cls(solver_name) + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_set_solver_name_allowed(self, solver_name): + """Test RidgeSolver acceptable solvers.""" + acceptable_solvers = [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", + "LBFGSB" + ] + solver = self.cls("GradientDescent") + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + solver.set_params(solver_name=solver_name) + else: + solver.set_params(solver_name=solver_name) + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_name, solver_kwargs): @@ -133,6 +153,20 @@ def test_init_solver_name(self, solver_name): else: self.cls(solver_name) + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_set_solver_name_allowed(self, solver_name): + """Test RidgeSolver acceptable solvers.""" + acceptable_solvers = [ + "ProximalGradient" + ] + solver = self.cls("ProximalGradient") + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + solver.set_params(solver_name=solver_name) + else: + solver.set_params(solver_name=solver_name) + @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_kwargs): """Test RidgeSolver acceptable kwargs.""" @@ -214,6 +248,25 @@ def test_init_solver_name(self, solver_name): else: self.cls(solver_name, mask) + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_set_solver_name_allowed(self, solver_name): + """Test RidgeSolver acceptable solvers.""" + acceptable_solvers = [ + "ProximalGradient" + ] + # create a valid mask + mask = np.zeros((2, 10)) + mask[0, :5] = 1 + mask[1, 5:] = 1 + mask = jnp.asarray(mask) + solver = self.cls("ProximalGradient", mask=mask) + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + solver.set_params(solver_name=solver_name) + else: + solver.set_params(solver_name=solver_name) + @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_kwargs): """Test RidgeSolver acceptable kwargs.""" @@ -379,3 +432,117 @@ def test_group_sparsity_enforcement(self, group_sparse_poisson_glm_model_instant raise ValueError("GroupLasso failed to zero-out the parameter group!") + ########### + # Test mask from set_params + ########### + @pytest.mark.parametrize("n_groups_assign", [0, 1, 2]) + def test_mask_validity_groups_set_params(self, + n_groups_assign, + group_sparse_poisson_glm_model_instantiation): + """Test that mask assigns at most 1 group to each weight.""" + raise_exception = n_groups_assign > 1 + X, y, model, true_params, firing_rate, _ = group_sparse_poisson_glm_model_instantiation + + # create a valid mask + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + solver = self.cls("ProximalGradient", mask) + + # change assignment + if n_groups_assign == 0: + mask[:, 3] = 0 + elif n_groups_assign == 2: + mask[:, 3] = 1 + + mask = jnp.asarray(mask) + + if raise_exception: + with pytest.raises(ValueError, match="Incorrect group assignment. " + "Some of the features"): + solver.set_params(mask=mask) + else: + solver.set_params(mask=mask) + + @pytest.mark.parametrize("set_entry", [0, 1, -1, 2, 2.5]) + def test_mask_validity_entries_set_params(self, set_entry, poissonGLM_model_instantiation): + """Test that mask is composed of 0s and 1s.""" + raise_exception = set_entry not in {0, 1} + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + + # create a valid mask + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + solver = self.cls("ProximalGradient", mask) + + # assign an entry + mask[1, 2] = set_entry + mask = jnp.asarray(mask, dtype=jnp.float32) + + if raise_exception: + with pytest.raises(ValueError, match="Mask elements be 0s and 1s"): + solver.set_params(mask=mask) + else: + solver.set_params(mask=mask) + + @pytest.mark.parametrize("n_dim", [0, 1, 2, 3]) + def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): + """Test that mask is composed of 0s and 1s.""" + + raise_exception = n_dim != 2 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + + valid_mask = np.zeros((2, X.shape[2])) + valid_mask[0, :1] = 1 + valid_mask[1, 1:] = 1 + solver = self.cls("ProximalGradient", valid_mask) + + # create a mask + if n_dim == 0: + mask = np.array([]) + elif n_dim == 1: + mask = np.ones((1,)) + elif n_dim == 2: + mask = np.zeros((2, X.shape[2])) + mask[0, :2] = 1 + mask[1, 2:] = 1 + else: + mask = np.zeros((2, X.shape[2]) + (1,) * (n_dim - 2)) + mask[0, :2] = 1 + mask[1, 2:] = 1 + + mask = jnp.asarray(mask, dtype=jnp.float32) + + if raise_exception: + with pytest.raises(ValueError, match="`mask` must be 2-dimensional"): + solver.set_params(mask=mask) + else: + solver.set_params(mask=mask) + + @pytest.mark.parametrize("n_groups", [0, 1, 2]) + def test_mask_n_groups_set_params(self, n_groups, poissonGLM_model_instantiation): + """Test that mask has at least 1 group.""" + raise_exception = n_groups < 1 + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + valid_mask = np.zeros((2, X.shape[2])) + valid_mask[0, :1] = 1 + valid_mask[1, 1:] = 1 + solver = self.cls("ProximalGradient", valid_mask) + + # create a mask + mask = np.zeros((n_groups, X.shape[2])) + if n_groups > 0: + for i in range(n_groups - 1): + mask[i, i: i + 1] = 1 + mask[-1, n_groups - 1:] = 1 + + mask = jnp.asarray(mask, dtype=jnp.float32) + + if raise_exception: + with pytest.raises(ValueError, match=r"Empty mask provided! Mask has "): + solver.set_params(mask=mask) + else: + solver.set_params(mask=mask) + + From 263c530d33941ad02507f8ca518733b9316bded4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 12:08:58 -0400 Subject: [PATCH 076/250] set noise model link func as a property --- src/neurostatslib/noise_model.py | 28 ++++++++++++++++++----- tests/test_noise_model.py | 38 ++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 2d9ecdcf..d08f533d 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -1,5 +1,5 @@ import abc -from typing import Union +from typing import Union, Callable import jax import jax.numpy as jnp @@ -18,13 +18,31 @@ def __dir__(): class NoiseModel(_Base, abc.ABC): FLOAT_EPS = jnp.finfo(jnp.float32).eps - def __init__(self, inverse_link_function, **kwargs): + def __init__(self, inverse_link_function: Callable, **kwargs): super().__init__(**kwargs) - if not callable(inverse_link_function): - raise ValueError("inverse_link_function must be a callable!") - self.inverse_link_function = inverse_link_function + self._check_inverse_link_function(inverse_link_function) + self._inverse_link_function = inverse_link_function self._scale = None + @property + def inverse_link_function(self): + return self._inverse_link_function + + @inverse_link_function.setter + def inverse_link_function(self, inverse_link_function: Callable): + self._check_inverse_link_function(inverse_link_function) + self._inverse_link_function = inverse_link_function + + @staticmethod + def _check_inverse_link_function(inverse_link_function): + if not callable(inverse_link_function): + raise TypeError("The `inverse_link_function` function must be a Callable!") + # check that the callable is in the jax namespace + if not hasattr(inverse_link_function, "__module__"): + raise TypeError("The `inverse_link_function` must be from the `jax` namespace!") + elif not getattr(inverse_link_function, "__module__").startswith("jax."): + raise TypeError("The `inverse_link_function` must be from the `jax` namespace!") + @abc.abstractmethod def negative_log_likelihood(self, firing_rate, y): pass diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index f82a4eed..458c3952 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -12,15 +12,49 @@ class TestPoissonNoiseModel: cls = nsl.noise_model.PoissonNoiseModel @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) - def test_initialization(self, link_function): + def test_initialization_link_is_callable(self, link_function): """Check that the noise model initializes when a callable is passed.""" raise_exception = not callable(link_function) if raise_exception: - with pytest.raises(ValueError, match="inverse_link_function must be a callable"): + with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): self.cls(link_function) else: self.cls(link_function) + @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x:x, sm.families.links.log]) + def test_initialization_link_is_jax(self, link_function): + """Check that the noise model initializes when a callable is passed.""" + raise_exception = (not hasattr(link_function, "__module__")) or \ + (not getattr(link_function, "__module__").startswith("jax")) + if raise_exception: + with pytest.raises(TypeError, match="The `inverse_link_function` must be from the `jax` namespace"): + self.cls(link_function) + else: + self.cls(link_function) + + @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) + def test_initialization_link_is_callable_set_params(self, link_function): + """Check that the noise model initializes when a callable is passed.""" + noise_model = self.cls() + raise_exception = not callable(link_function) + if raise_exception: + with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): + noise_model.set_params(inverse_link_function=link_function) + else: + noise_model.set_params(inverse_link_function=link_function) + + @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log]) + def test_initialization_link_is_jax_set_params(self, link_function): + """Check that the noise model initializes when a callable is passed.""" + raise_exception = (not hasattr(link_function, "__module__")) or \ + (not getattr(link_function, "__module__").startswith("jax")) + noise_model = self.cls() + if raise_exception: + with pytest.raises(TypeError, match="The `inverse_link_function` must be from the `jax` namespace"): + noise_model.set_params(inverse_link_function=link_function) + else: + noise_model.set_params(inverse_link_function=link_function) + def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): """ Compare fitted parameters to statsmodels. From 3cfc0b084eb162ee202698a0458c098488af644e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 12:11:22 -0400 Subject: [PATCH 077/250] linted --- src/neurostatslib/noise_model.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index d08f533d..0d58aa85 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -1,5 +1,5 @@ import abc -from typing import Union, Callable +from typing import Callable, Union import jax import jax.numpy as jnp @@ -39,9 +39,13 @@ def _check_inverse_link_function(inverse_link_function): raise TypeError("The `inverse_link_function` function must be a Callable!") # check that the callable is in the jax namespace if not hasattr(inverse_link_function, "__module__"): - raise TypeError("The `inverse_link_function` must be from the `jax` namespace!") + raise TypeError( + "The `inverse_link_function` must be from the `jax` namespace!" + ) elif not getattr(inverse_link_function, "__module__").startswith("jax."): - raise TypeError("The `inverse_link_function` must be from the `jax` namespace!") + raise TypeError( + "The `inverse_link_function` must be from the `jax` namespace!" + ) @abc.abstractmethod def negative_log_likelihood(self, firing_rate, y): From e430c35a4cb514cc7b60c45375b59696e877d72a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 12:16:32 -0400 Subject: [PATCH 078/250] bugfixed tests (compiled jax funcs start with jaxlib) --- src/neurostatslib/noise_model.py | 2 +- src/neurostatslib/solver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 0d58aa85..8d5486e8 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -42,7 +42,7 @@ def _check_inverse_link_function(inverse_link_function): raise TypeError( "The `inverse_link_function` must be from the `jax` namespace!" ) - elif not getattr(inverse_link_function, "__module__").startswith("jax."): + elif not getattr(inverse_link_function, "__module__").startswith("jax"): raise TypeError( "The `inverse_link_function` must be from the `jax` namespace!" ) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 4e771ef1..dd47175d 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -151,7 +151,7 @@ def _check_is_callable_from_jax(func: Callable): if (not hasattr(func, "__module__")) or ( not ( - func.__module__.startswith("jax.") + func.__module__.startswith("jax") or func.__module__.startswith("neurostatslib.glm") ) ): From 55ec5e3597d0bc7ec6f4eb5ebce2b4b6683d6e6e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 12:45:27 -0400 Subject: [PATCH 079/250] added tests for unreg solver --- src/neurostatslib/glm.py | 4 -- src/neurostatslib/solver.py | 1 + tests/test_glm.py | 16 ----- tests/test_solver.py | 134 ++++++++++++++++++++++++++++++++++-- 4 files changed, 129 insertions(+), 26 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index b078ef35..1a2f441d 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -282,10 +282,6 @@ def fit( # convert to jnp.ndarray & perform checks X, y, init_params = self.preprocess_fit(X, y, init_params) - # Make sure mask is of floating type - if isinstance(self.solver, slv.GroupLassoSolver): - self.solver.mask = jnp.asarray(self.solver.mask, dtype=float) - # Run optimization runner = self.solver.instantiate_solver(self._score) params, state = runner(init_params, X, y) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index dd47175d..1832af47 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -228,6 +228,7 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: + self._check_is_callable_from_jax(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss return self.get_runner(solver_kwargs, {}) diff --git a/tests/test_glm.py b/tests/test_glm.py index 4e26bae4..6fbc27df 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1017,22 +1017,6 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") - def test_compare_fit_estimate_to_statsmodels(self, poissonGLM_model_instantiation): - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - glm_sm = sm.GLM(endog=y[:, 0], - exog=sm.add_constant(X[:, 0]), - family=sm.families.Poisson()) - res_sm = glm_sm.fit() - fit_params_sm = res_sm.params - # use a second order method for precision, match non-linearity - model.set_params(noise_model__inverse_link_function=jnp.exp, - solver__solver_name="BFGS", - solver__solver_kwargs={"tol": 10**-8}) - model.fit(X, y) - fit_params_model = jnp.hstack((model.baseline_link_fr_, - model.basis_coeff_.flatten())) - if not np.allclose(fit_params_sm, fit_params_model): - raise ValueError("Fitted parameters do not match that of statsmodels!") def test_compatibility_with_sklearn_cv(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation diff --git a/tests/test_solver.py b/tests/test_solver.py index 3a1362f5..3517350b 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -15,6 +15,128 @@ def test_abstract_nature_of_solver(self): with pytest.raises(TypeError, match="Can't instantiate abstract class Solver"): self.cls("GradientDescent") +class TestUnRegularizedSolver: + cls = nsl.solver.UnRegularizedSolver + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_init_solver_name(self, solver_name): + """Test UnRegularizedSolver acceptable solvers.""" + acceptable_solvers = [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", + "LBFGSB" + ] + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + self.cls(solver_name) + else: + self.cls(solver_name) + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + def test_set_solver_name_allowed(self, solver_name): + """Test UnRegularizedSolver acceptable solvers.""" + acceptable_solvers = [ + "GradientDescent", + "BFGS", + "LBFGS", + "ScipyMinimize", + "NonlinearCG", + "ScipyBoundedMinimize", + "LBFGSB" + ] + solver = self.cls("GradientDescent") + raise_exception = solver_name not in acceptable_solvers + if raise_exception: + with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + solver.set_params(solver_name=solver_name) + else: + solver.set_params(solver_name=solver_name) + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) + @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) + def test_init_solver_kwargs(self, solver_name, solver_kwargs): + """Test RidgeSolver acceptable kwargs.""" + + raise_exception = "tols" in list(solver_kwargs.keys()) + if raise_exception: + with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + self.cls(solver_name, solver_kwargs=solver_kwargs) + else: + self.cls(solver_name, solver_kwargs=solver_kwargs) + + @pytest.mark.parametrize("loss", [jnp.exp, 1, None, {}]) + def test_loss_is_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + if raise_exception: + with pytest.raises(TypeError, match="The loss function must a Callable"): + self.cls("GradientDescent").instantiate_solver(loss) + else: + self.cls("GradientDescent").instantiate_solver(loss) + + @pytest.mark.parametrize("loss", [jnp.exp, np.exp, nsl.glm.GLM()._score]) + def test_loss_type_jax_or_glm(self, loss): + """Test that the loss function is a callable""" + raise_exception = (not hasattr(loss, "__module__")) or \ + (not (loss.__module__.startswith("jax.") or + loss.__module__.startswith("neurostatslib.glm"))) + if raise_exception: + with pytest.raises(ValueError, match=f"The function {loss.__name__} is not from the jax namespace."): + self.cls("GradientDescent").instantiate_solver(loss) + else: + self.cls("GradientDescent").instantiate_solver(loss) + + @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) + def test_run_solver(self, solver_name, poissonGLM_model_instantiation): + """Test that the solver runs.""" + + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + runner = self.cls("GradientDescent").instantiate_solver(model._score) + runner((true_params[0]*0., true_params[1]), X, y) + + def test_solver_output_match(self, poissonGLM_model_instantiation): + """Test that different solvers converge to the same solution.""" + jax.config.update("jax_enable_x64", True) + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set precision to float64 for accurate matching of the results + model.data_type = jnp.float64 + runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver(model._score) + runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver(model._score) + runner_scipy = self.cls("ScipyMinimize", {"method": "BFGS", "tol": 10**-12}).instantiate_solver(model._score) + weights_gd, intercepts_gd = runner_gd((true_params[0] * 0., true_params[1]), X, y)[0] + weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] + weights_scipy, intercepts_scipy = runner_scipy((true_params[0] * 0., true_params[1]), X, y)[0] + + match_weights = np.allclose(weights_gd, weights_bfgs) and \ + np.allclose(weights_gd, weights_scipy) + match_intercepts = np.allclose(intercepts_gd, intercepts_bfgs) and \ + np.allclose(intercepts_gd, intercepts_scipy) + if (not match_weights) or (not match_intercepts): + raise ValueError("Convex estimators should converge to the same numerical value.") + + def test_solver_match_sklearn(self, poissonGLM_model_instantiation): + """Test that different solvers converge to the same solution.""" + jax.config.update("jax_enable_x64", True) + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + # set precision to float64 for accurate matching of the results + model.data_type = jnp.float64 + solver = self.cls("GradientDescent", {"tol": 10**-12}) + runner_bfgs = solver.instantiate_solver(model._score) + weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] + model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=0.) + model_skl.fit(X[:,0], y[:, 0]) + + match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) + match_intercepts = np.allclose(model_skl.intercept_, intercepts_bfgs.flatten()) + if (not match_weights) or (not match_intercepts): + raise ValueError("Ridge GLM solver estimate does not match sklearn!") + + class TestRidgeSolver: cls = nsl.solver.RidgeSolver @@ -142,7 +264,7 @@ class TestLassoSolver: @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): - """Test RidgeSolver acceptable solvers.""" + """Test LassoSolver acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] @@ -155,7 +277,7 @@ def test_init_solver_name(self, solver_name): @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_set_solver_name_allowed(self, solver_name): - """Test RidgeSolver acceptable solvers.""" + """Test LassoSolver acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] @@ -169,7 +291,7 @@ def test_set_solver_name_allowed(self, solver_name): @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_kwargs): - """Test RidgeSolver acceptable kwargs.""" + """Test LassoSolver acceptable kwargs.""" raise_exception = "tols" in list(solver_kwargs.keys()) if raise_exception: with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): @@ -230,7 +352,7 @@ class TestGroupLassoSolver: @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): - """Test RidgeSolver acceptable solvers.""" + """Test GroupLassoSolver acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] @@ -250,7 +372,7 @@ def test_init_solver_name(self, solver_name): @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_set_solver_name_allowed(self, solver_name): - """Test RidgeSolver acceptable solvers.""" + """Test GroupLassoSolver acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] @@ -269,7 +391,7 @@ def test_set_solver_name_allowed(self, solver_name): @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_kwargs): - """Test RidgeSolver acceptable kwargs.""" + """Test GroupLassoSolver acceptable kwargs.""" raise_exception = "tols" in list(solver_kwargs.keys()) # create a valid mask From 064222d57ccd3883a36f70c6304662f67c1e093e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 12:56:15 -0400 Subject: [PATCH 080/250] added unreg tests and pointed to src for coverage --- tests/test_glm.py | 6 +++--- tests/test_solver.py | 1 + tox.ini | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 6fbc27df..eb55b705 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -139,10 +139,10 @@ def test_fit_weights_dimensionality(self, dim_weights, error, match_str, poisson _test_class_method(model, "fit", [X, y], {"init_params": (init_w, true_params[1])}, error, match_str) @pytest.mark.parametrize("dim_intercepts, error, match_str", [ - (0, ValueError, "params\[1\] must be of shape"), + (0, ValueError, r"params\[1\] must be of shape"), (1, None, None), - (2, ValueError, "params\[1\] must be of shape"), - (3, ValueError, "params\[1\] must be of shape") + (2, ValueError, r"params\[1\] must be of shape"), + (3, ValueError, r"params\[1\] must be of shape") ]) def test_fit_intercepts_dimensionality(self, dim_intercepts, error, match_str, poissonGLM_model_instantiation): """ diff --git a/tests/test_solver.py b/tests/test_solver.py index 3517350b..fcbfa8c9 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -15,6 +15,7 @@ def test_abstract_nature_of_solver(self): with pytest.raises(TypeError, match="Can't instantiate abstract class Solver"): self.cls("GradientDescent") + class TestUnRegularizedSolver: cls = nsl.solver.UnRegularizedSolver diff --git a/tox.ini b/tox.ini index fc61991e..13c25a84 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ commands = black --check src isort --check src flake8 --config={toxinidir}/tox.ini src - pytest --cov + pytest tests/ --cov=src [gh-actions] python = From b37e9b5d3460c3f06f57331f72685ce81b01f097 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 19 Sep 2023 13:00:39 -0400 Subject: [PATCH 081/250] added unreg tests and pointed to src for coverage --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 13c25a84..fc61991e 100644 --- a/tox.ini +++ b/tox.ini @@ -16,7 +16,7 @@ commands = black --check src isort --check src flake8 --config={toxinidir}/tox.ini src - pytest tests/ --cov=src + pytest --cov [gh-actions] python = From 01c86d05875f5a4dddfbfd29815fafaac972fb24 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 20 Sep 2023 12:43:19 -0400 Subject: [PATCH 082/250] improved docs --- src/neurostatslib/base_class.py | 3 +- src/neurostatslib/glm.py | 92 ++++++++++++++++++++++++++++++++ src/neurostatslib/noise_model.py | 92 ++++++++++++++++++++++++++++++++ src/neurostatslib/solver.py | 43 +++++++++++++++ 4 files changed, 229 insertions(+), 1 deletion(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 81468539..51ae44f3 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -1,4 +1,5 @@ -"""Abstract class for models.""" +"""## Abstract class for estimators. +""" import abc import inspect diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 1a2f441d..3c532d78 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -13,6 +13,54 @@ class GLM(BaseRegressor): + """ + Generalized Linear Model (GLM) for neural activity data. + + This GLM implementation allows users to model neural activity based on a combination of exogenous inputs + (like convolved currents or light intensities) and a choice of noise model. It is suitable for scenarios where + the relationship between predictors and the response variable might be non-linear, and the residuals + don't follow a normal distribution. + + Parameters + ---------- + noise_model : NoiseModel + Noise model to use. The model describes the noise distribution of the neural activity. + Default is Poisson noise model. + solver : Solver + Solver to use for model optimization. Defines the optimization algorithm and related parameters. + Default is Ridge regression with gradient descent. + **kwargs : Any + Additional keyword arguments. + + Attributes + ---------- + noise_model : NoiseModel + Noise model being used. + solver : Solver + Solver being used. + baseline_link_fr_ : jnp.ndarray or None + Model baseline link firing rate parameters after fitting. + basis_coeff_ : jnp.ndarray or None + Basis coefficients for the model after fitting. + scale : float + Scale parameter for the noise model. It's 1.0 for Poisson and Gaussian. + solver_state : Any + State of the solver after fitting. May include details like optimization error. + + Raises + ------ + TypeError + If provided `solver` or `noise_model` are not valid or implemented in `neurostatslib.solver` and + `neurostatslib.noise_model` respectively. + + Notes + ----- + The GLM aims to model the relationship between several predictor variables and a response variable. + In this neural context, the predictors might represent external inputs or other neurons' activity, while + the response variable is the neuron's activity being modeled. The noise model captures the statistical properties + of the neural activity, while the solver determines how the model parameters are estimated. + """ + def __init__( self, noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), @@ -361,6 +409,50 @@ def simulate( class GLMRecurrent(GLM): + """ + A Generalized Linear Model (GLM) with recurrent dynamics. + + This class extends the basic GLM to capture recurrent dynamics between neurons, + making it more suitable for simulating the activity of interconnected neural populations. + The recurrent GLM combines both feedforward inputs (like sensory stimuli) and past + neural activity to simulate or predict future neural activity. + + Parameters + ---------- + noise_model : nsm.NoiseModel, default=nsm.PoissonNoiseModel() + The noise model to use for the GLM. This defines how neural activity is generated + based on the underlying firing rate. Common choices include Poisson and Gaussian models. + + solver : slv.Solver, default=slv.RidgeSolver() + The optimization solver to use for fitting the GLM parameters. + + data_type : {jnp.float32, jnp.float64}, optional + The numerical data type for internal calculations. If not provided, it will be inferred + from the data during fitting. + + Attributes + ---------- + - The attributes of `GLMRecurrent` are inherited from the parent `GLM` class, and might include + coefficients, fitted status, and other model-related attributes. + + See Also + -------- + [GLM](../glm/#neurostatslib.glm.GLM) : Base class for the generalized linear model. + + Notes + ----- + The recurrent GLM assumes that neural activity can be influenced by both feedforward + inputs and the past activity of the same and other neurons. This makes it particularly + powerful for capturing the dynamics of neural networks where neurons are interconnected. + + Examples + -------- + >>> # Initialize the recurrent GLM with default parameters + >>> model = GLMRecurrent() + >>> # ... your code for training and simulating using the model ... + + """ + def __init__( self, noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 8d5486e8..92e06945 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -1,3 +1,5 @@ +"""Noise model classes for GLMs.""" + import abc from typing import Callable, Union @@ -16,6 +18,27 @@ def __dir__(): class NoiseModel(_Base, abc.ABC): + """ + Abstract noise model class for neural data processing. + + This is an abstract base class used to implement noise models for neural data. + Specific noise models that inherit from this class should define their versions + of the abstract methods: negative_log_likelihood, emission_probability, and + residual_deviance. + + Attributes + ---------- + FLOAT_EPS : + A small value used to ensure numerical stability. Set to the machine epsilon for float32. + inverse_link_function : + A function that transforms a set of predictors to the domain of the model parameter. + + See Also + -------- + [PoissonNoiseModel](./#neurostatslib.noise_model.PoissonNoiseModel) : A specific implementation of a + noise model using the Poisson distribution. + """ + FLOAT_EPS = jnp.finfo(jnp.float32).eps def __init__(self, inverse_link_function: Callable, **kwargs): @@ -26,10 +49,12 @@ def __init__(self, inverse_link_function: Callable, **kwargs): @property def inverse_link_function(self): + """Getter for the inverse link function for the model.""" return self._inverse_link_function @inverse_link_function.setter def inverse_link_function(self, inverse_link_function: Callable): + """Setter for the inverse link function for the model.""" self._check_inverse_link_function(inverse_link_function) self._inverse_link_function = inverse_link_function @@ -49,16 +74,66 @@ def _check_inverse_link_function(inverse_link_function): @abc.abstractmethod def negative_log_likelihood(self, firing_rate, y): + r"""Compute the noise model negative log-likelihood. + + This computes the negative log-likelihood of the predicted rates + for the observed neural activity up to a constant. + + Parameters + ---------- + predicted_rate : + The predicted rate of the current model. Shape (n_time_bins, n_neurons). + y : + The target activity to compare against. Shape (n_time_bins, n_neurons). + + Returns + ------- + : + The Poisson negative log-likehood. Shape (1,). + """ pass @abc.abstractmethod def emission_probability( self, key: KeyArray, predicted_rate: jnp.ndarray ) -> jnp.ndarray: + """ + Calculate the emission of the noise model. + + This method generates random numbers from the desired distribution based on the given + `predicted_rate`. + + Parameters + ---------- + key : + Random key used for the generation of random numbers in JAX. + predicted_rate : + Expected rate of the distribution. Shape (n_time_bins, n_neurons). + + Returns + ------- + jnp.ndarray + Random numbers generated from the desired distribution based on the `predicted_rate` scale parameter + if needed. + """ pass @abc.abstractmethod def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray): + r"""Compute the residual deviance for a Poisson model. + + Parameters + ---------- + predicted_rate: + The predicted firing rates. Shape (n_time_bins, n_neurons). + spike_counts: + The spike counts. Shape (n_time_bins, n_neurons). + + Returns + ------- + : + The residual deviance of the model. + """ pass def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): @@ -92,6 +167,23 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): class PoissonNoiseModel(NoiseModel): + """ + Poisson Noise Model class for spike count data. + + The PoissonNoiseModel is designed to model the observed spike counts based on a Poisson distribution + with a given rate. It provides methods for computing the negative log-likelihood, emission probability, + and residual deviance for the given spike count data. + + Attributes + ---------- + inverse_link_function : + A function that maps the predicted rate to the domain of the Poisson parameter. Defaults to jnp.exp. + + See Also + -------- + [NoiseModel](./#neurostatslib.noise_model.NoiseModel) : Base class for noise models. + """ + def __init__(self, inverse_link_function=jnp.exp): super().__init__(inverse_link_function=inverse_link_function) self._scale = 1 diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 1832af47..944210bd 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -1,3 +1,10 @@ +""" +## A Module for Optimization with Various Regularizations. + +This module provides a series of classes that facilitate the optimization of models +with different types of regularizations. Each solver class in this module interfaces +with various optimization methods, and they can be applied depending on the model's requirements. +""" import abc import inspect from typing import Callable, Optional, Tuple, Union @@ -205,6 +212,28 @@ def solver_run( class UnRegularizedSolver(Solver): + """ + Solver class for optimizing unregularized models. + + This class provides an interface to various optimization methods for models that + do not involve regularization. The optimization methods that are allowed for this + class are defined in the `allowed_optimizers` attribute. + + Attributes + ---------- + allowed_optimizers : list of str + List of optimizer names that are allowed for this solver class. + + Methods + ------- + instantiate_solver(loss) + Instantiates the optimization algorithm with the given loss function. + + See Also + -------- + [Solver](./#neurostatslib.solver.Solver) : Base solver class from which this class inherits. + """ + allowed_optimizers = [ "GradientDescent", "BFGS", @@ -228,6 +257,20 @@ def instantiate_solver( ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: + """ + Instantiate the optimization algorithm for a given loss function. + + Parameters + ---------- + loss : + The loss function that needs to be minimized. + + Returns + ------- + : + A runner function that uses the specified optimization algorithm + to minimize the given loss function. + """ self._check_is_callable_from_jax(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss From 753a64d01e51f8afb01bfd5d002f13accab8c7fa Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 20 Sep 2023 14:15:48 -0400 Subject: [PATCH 083/250] edited 02-base_class.md note --- docs/developers_notes/02-base_class.md | 70 +++++++++++++++----------- src/neurostatslib/base_class.py | 2 +- src/neurostatslib/glm.py | 27 +++++----- 3 files changed, 55 insertions(+), 44 deletions(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index d5d85934..00ae52cc 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -2,9 +2,9 @@ ## Introduction -The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. Currently, the sole abstract class available is `_BaseRegressor`. +The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. Currently, the sole abstract class available is `BaseRegressor`. -The `_Base` class is envisioned as the foundational component for any model type (e.g., regression, dimensionality reduction, clustering, etc.). In contrast, abstract classes derived from `_Base` define overarching model categories (e.g., `_BaseRegressor` is building block for GLMs, GAMS, etc.). +The `_Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, noise models, solvers etc.). In contrast, abstract classes derived from `_Base` define overarching object categories (e.g., `BaseRegressor` is building block for GLMs, GAMS, etc. while `NoiseModel` is the building block for the Poisson noise, Gamma noise, ... etc.). Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. @@ -12,32 +12,37 @@ Below a scheme of how we envision the architecture of the `neurostatslib` models ``` Class _Base -| -└─ Abstract Subclass _BaseRegressor +│ +├─ Abstract Subclass BaseRegressor │ │ -│ └─ Abstract Subclass _BaseGLM -│ │ -│ ├─ Concrete Subclass PoissonGLM -│ │ │ -│ │ └─ Concrete Subclass RidgePoissonGLM *(not implemented yet) -│ │ │ -│ │ └─ Concrete Subclass LassoPoissonGLM *(not implemented yet) -│ │ │ -│ │ ... +│ └─ Concrete Subclass GLM │ │ -│ ├─ Concrete Subclass GammaGLM *(not implemented yet) -│ │ │ -│ │ ... -│ │ -│ ... +│ └─ Concrete Subclass RecurrentGLM +│ +├─ Abstract Subclass BaseManifold *(not implemented yet) +│ │ +│ ... +│ +├─ Abstract Subclass Sovler +│ │ +│ ├─ Concrete Subclass UnRegularizedSolver +│ │ +│ ├─ Concrete Subclass RidgeSolver +│ ... +│ +├─ Abstract Subclass NoiseModel +│ │ +│ ├─ Concrete Subclass PoissonNoiseModel +│ │ +│ ├─ Concrete Subclass GammaNoiseModel *(not implemented yet) +│ ... │ -├─ Abstract Subclass _BaseManifold *(not implemented yet) ... ``` !!! Example - The current package version includes a concrete class named `neurostatslib.glm.PoissonGLM`. This class inherits from `_BaseGLM` <- `_BaseRegressor` <- `_Base`, since it falls under the " GLM regression" category. - As any `_BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. + The current package version includes a concrete class named `neurostatslib.glm.GLM`. This class inherits from `BaseRegressor` <- `_Base`, since it falls under the " GLM regression" category. + As any `BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. ## The Class `model_base._Base` @@ -53,23 +58,32 @@ For a detailed understanding, consult the [`scikit-learn` API Reference](https:/ - **`get_params`**: The `get_params` method retrieves parameters set during model instance initialization. Opting for a deep inspection allows the method to assess nested object parameters, resulting in a comprehensive parameter dictionary. - **`set_params`**: The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. -- **`select_target_device`**: Selects either "cpu", "gpu" or "tpu" as the device. If not found, rolls back to "cpu". +- **`select_target_device`**: Selects either "cpu", "gpu" or "tpu" as the device. - **`device_put`**: Sends arrays to device, if not on device already. -## The Abstract Class `model_base._BaseRegressor` +## The Abstract Class `model_base.BaseRegressor` -`_BaseRegressor` is an abstract class that inherits from `_Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. +`BaseRegressor` is an abstract class that inherits from `_Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. ### Abstract Methods -For subclasses derived from `_BaseRegressor` to function correctly, they must implement the following: +For subclasses derived from `BaseRegressor` to function correctly, they must implement the following: 1. `fit`: Adapt the model using input data `X` and corresponding observations `y`. 2. `predict`: Provide predictions based on the trained model and input data `X`. 3. `score`: Score the accuracy of model predictions using input data `X` against the actual observations `y`. 4. `simulate`: Simulate data based on the trained regression model. -Moreover, `_BaseRegressor` incorporates auxiliary methods such as `_convert_to_jnp_ndarray`, `_has_invalid_entry` +### Public Methods + +To ensure the consistency and conformity of input data, the `BaseRegressor` introduces two public preprocessing methods: + +1. `preprocess_fit`: Assesses and converts the input for the `fit` method into the desired `jax.ndarray` format. If necessary, this method can initialize model parameters using default values. +2. `preprocess_simulate`: Validates and converts inputs for the `simulate` method. This method confirms the integrity of the feedforward input and, when provided, the initial values for feedback. + +### Auxiliary Methods + +Moreover, `BaseRegressor` incorporates auxiliary methods such as `_convert_to_jnp_ndarray`, `_has_invalid_entry` and a number of other methods for checking input consistency. !!! Tip @@ -80,9 +94,9 @@ and a number of other methods for checking input consistency. ### Implementing Model Subclasses -When devising a new model subclass based on the `_BaseRegressor` abstract class, adhere to the subsequent guidelines: +When devising a new model subclass based on the `BaseRegressor` abstract class, adhere to the subsequent guidelines: -- **Must** inherit the `_BaseRegressor` abstract superclass. +- **Must** inherit the `BaseRegressor` abstract superclass. - **Must** realize the abstract methods: `fit`, `predict`, `score`, and `simulate`. - **Should not** overwrite the `get_params` and `set_params` methods, inherited from `_Base`. - **May** introduce auxiliary methods such as `_convert_to_jnp_ndarray` for added utility. diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 51ae44f3..d94455df 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -263,7 +263,7 @@ def simulate( random_key: jax.random.PRNGKeyArray, feed_forward_input: Union[NDArray, jnp.ndarray], # feed-forward input and/coupling basis - **kwargs, + **kwargs: Any, ): pass diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 3c532d78..a8feb60a 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -344,8 +344,8 @@ def fit( ) # Store parameters - self.basis_coeff_ = params[0] - self.baseline_link_fr_ = params[1] + self.basis_coeff_: jnp.ndarray = params[0] + self.baseline_link_fr_: jnp.ndarray = params[1] # note that this will include an error value, which is not the same as # the output of loss. I believe it's the output of # solver.l2_optimality_error @@ -356,7 +356,7 @@ def simulate( random_key: jax.random.PRNGKeyArray, feedforward_input: Union[NDArray, jnp.ndarray], # feed-forward input and/coupling basis - **kwargs, + **kwargs: Any, ) -> Tuple[jnp.ndarray, jnp.ndarray]: """Simulate neural activity in response to a feed-forward input. @@ -465,8 +465,8 @@ def simulate( self, random_key: jax.random.PRNGKeyArray, feedforward_input: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Union[NDArray, jnp.ndarray] = None, - init_y: Union[NDArray, jnp.ndarray] = None, + coupling_basis_matrix: Optional[Union[NDArray, jnp.ndarray]] = None, + init_y: Union[NDArray, Optional[jnp.ndarray]] = None, ): """ Simulate neural activity using the GLM as a recurrent network. @@ -476,6 +476,9 @@ def simulate( of historical activity and external feedforward inputs like convolved currents, light intensities, etc. + If no `coupling_basis_matrix` is provided, the spikes will be generated in response to + the feedforward input only. + Parameters ---------- random_key : @@ -526,15 +529,7 @@ def simulate( to ensure consistency in the model's input feature dimensionality. """ if coupling_basis_matrix is None: - raise ValueError( - "GLMRecurrent simulate method requires a coupling basis" - " matrix in order to generate neural activity!" - ) - if init_y is None: - raise ValueError( - "GLMRecurrent simulate method requires the initial activity " - "init_y to start-off the simulation!" - ) + return super().simulate(random_key, feedforward_input) # check if the model is fit self._check_is_fit() @@ -545,7 +540,9 @@ def simulate( n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] - # split weights in recurrent and feed-forward + if init_y is None: + init_y = jnp.zeros((coupling_basis_matrix.shape[0], n_neurons)) + Wf = self.basis_coeff_[:, n_basis_coupling * n_neurons :] Wr = self.basis_coeff_[:, : n_basis_coupling * n_neurons] bs = self.baseline_link_fr_ From 29eb4bad1eda45e9290a7b559f15a5263d590813 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 20 Sep 2023 18:08:46 -0400 Subject: [PATCH 084/250] edited note 03-glm.md --- docs/developers_notes/03-glm.md | 101 +++++++++++---------------- docs/developers_notes/GLM_scheme.jpg | Bin 0 -> 137103 bytes mkdocs.yml | 3 + src/neurostatslib/glm.py | 24 ++----- src/neurostatslib/noise_model.py | 26 ++++++- 5 files changed, 74 insertions(+), 80 deletions(-) create mode 100644 docs/developers_notes/GLM_scheme.jpg diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index 3d325276..f814f02e 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -2,95 +2,74 @@ ## Introduction -The `neurostatslib.glm` module implements variations of Generalized Linear Models (GLMs) classes. -At this stage, the module consists of two primary classes: -1. **`_BaseGLM`:** An abstract class serving as the backbone for building GLMs. -2. **`PoissonGLM`:** A concrete implementation of the GLM for Poisson-distributed data. +Generalized Linear Models (GLM) provide a flexible framework for modeling a variety of data types while establishing a relationship between multiple predictors and a response variable. A GLM extends the traditional linear regression by allowing for response variables that have error distribution models other than a normal distribution, such as binomial or Poisson distributions. -Our design aligns with the `scikit-learn` API. This ensures that our GLM classes integrate seamlessly with the robust `scikit-learn` pipeline and its cross-validation capabilities. +The `neurostatslib.glm` module currently offers implementations of two GLM classes: -## The class `_BaseGLM` +1. **`GLM`:** A direct implementation of a feedforward GLM. +2. **`RecurrentGLM`:** An implementation of a recurrent GLM. This class inherits from `GLM` and redefines the `simulate` method to generate spikes akin to a recurrent neural network. -Designed with `scikit-learn` compatibility in mind, `_BaseGLM` provides the common computations and functionalities needed by the diverse `GLM` subclasses. +Our design is harmonized with the `scikit-learn` API, facilitating seamless integration of our GLM classes with the well-established `scikit-learn` pipeline and its cross-validation tools. -### Inheritance - -The `_BaseGLM` inherits attributes and methods from the `_BaseRegressor`, as detailed in the [`base_class` module](02-base_class.md). This grants `_BaseGLM` a toolkit for managing and verifying model inputs. Leveraging the inherited abstraction, all GLM subclasses must explicitly define the `fit`, `predict`, `score`, and `simulate` methods, ensuring alignment with the `scikit-learn` framework. - -### Attributes - -- **`solver`**: The optimization solver from jaxopt. -- **`solver_state`**: Represents the current state of the solver. -- **`basis_coeff_`**: Holds the solution for spike basis coefficients after the model has been fitted. Initialized to `None` at class instantiation. -- **`baseline_link_fr`**: Contains the bias terms' solutions after fitting. Initialized to `None` at class instantiation. -- **`kwargs`**: Other keyword arguments, like regularization hyperparameters. - - -### Private Methods +The classes provided here are modular by design offering a standard foundation for any GLM variant. -- **`_check_is_fit`**: Ensures the instance has been fitted. This check is implemented here and not in `_BaseRegressor` because the model parameters are likely to be GLM specific. -- **`_predict`**: Forecasts firing rates based on predictors and parameters. -- **`_pseudo_r2`**: Computes the Pseudo-$R^2$ for a GLM, giving insight into the model's fit relative to a null model. -- **`_safe_predict`**: Validates the model's fit status and input consistency before calculating mean rates using the `_predict` method. -- **`_safe_score`**: Scores the predicted firing rates against target spike counts. Can compute either the GLM mean log-likelihood or the pseudo-$R^2$. -- **`_safe_fit`**: Fit the GLM to the neural activity. Verifies input conformity, then leverages the `jaxopt` optimizer on the designated loss function (provided by the concrete GLM subclass). -- **`_safe_simulate`**: Simulates spike trains using the GLM as a recurrent network. It projects neural activity into the future using the fitted parameters of the GLM. The function can simulate activity based on both historical spike activity and external feedforward inputs, such as convolved currents, light intensities, etc. +Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. +![Title](GLM_scheme.jpg){ width="512" } +
+
Schematic of the module interactions.
+
-!!! note - The introduction of `_safe_predict`, `_safe_fit`, `_safe_score` and `_safe_simulate` offers the following benefits: - 1. It eliminates the need for subclasses to redo checks in their `fit`, `score` and `simulate` methods, leading to concise code. - 2. The methods `predict`, `score`, `fit`, and `simulate` must be defined by subclasses due to their abstract nature in `_BaseRegressor`. This ensures subclass-specific docstrings for public methods. - While `predict` is common to any GLM, we explicitly omit its implementation in `_BaseGLM` so that the method will be documented in the `Code References` under each concrete class. +## The Concrete Class `GLM` -### Abstract Methods -Besides the methods acquired from `_BaseRegressor`, `_BaseGLM` introduces: +The `GLM` class provides a direct implementation of the GLM model and is designed with `scikit-learn` compatibility in mind. -- **`residual_deviance`**: Computes a GLM's residual deviance. The deviance, on par with the likelihood, is model specific. +### Inheritance -!!! note - The residual deviance can be formulated as a function of log-likelihood. Although a concrete `_BaseGLM` implementation is feasible, subclass-specific implementations might offer increased robustness or efficiency. +`GLM` inherits from `BaseRegressor`. This inheritance mandates the direct implementation of methods like `predict`, `fit`, `score`, and `simulate`. -## The Concrete Class `PoissonGLM` +### Attributes -The class `PoissonGLM` is a concrete implementation of the un-regularized Poisson GLM model. +- **`solver`**: Refers to the optimization solver - an object of the `neurostatslib.solver.Solver` type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. +- **`noise_model`**: Represents the GLM noise model, which is an object of the `neurostatlib.noise_model.NoiseModel` type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. +- **`basis_coeff_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. +- **`baseline_link_fr_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. +- **`solver_state`**: Indicates the solver's state. For specific solver states, refer to the [`jaxopt` documentation](https://jaxopt.github.io/stable/index.html#). -### Inheritance +### Public Methods -`PoissonGLM` inherits from `_BaseGLM`, which provides methods for predicting firing rates and "safe" methods to score and simulate spike trains. Inheritance enforces the concrete implementation of `fit`, `score`, `simulate`, and `residual_deviance`. +- **`predict`**: Validates input and computes the mean rates of the `GLM` by invoking the inverse-link function of the `noise_model` attribute. +- **`score`**: Validates input and assesses the Poisson GLM using either log-likelihood or pseudo-$R^2$. This method uses the `noise_model` to determine log-likelihood or pseudo-$R^2$. +- **`fit`**: Validates input and aligns the Poisson GLM with spike train data. It leverages the `noise_model` and `solver` to define the model's loss function and instantiate the solver. +- **`simulate`**: Simulates spike trains using the GLM as a feedforward network, invoking the `noise_model.emission_probability` method for emission probability. -### Attributes +### Private Methods -- **`solver`**: The optimization solver from jaxopt. -- **`solver_state`**: Represents the current state of the solver. -- **`basis_coeff_`**: Holds the solution for spike basis coefficients after the model has been fitted. Initialized to `None` at class instantiation. -- **`baseline_link_fr`**: Contains the bias terms' solutions after fitting. Initialized to `None` at class instantiation. +- **`_predict`**: Forecasts rates based on current model parameters and the inverse-link function of the `noise_model`. +- **`_score`**: Determines the Poisson negative log-likelihood, excluding normalization constants. +- **`_check_is_fit`**: Validates whether the model has been appropriately fit by ensuring model parameters are set. If not, a `NotFittedError` is raised. -### Public Methods +## The Concrete Class `RecurrentGLM` -- **`predict`**: Calculates mean rates by invoking the `_safe_predict` method of `_BaseGLM`. -- **`score`**: Scores the Poisson GLM using either log-likelihood or pseudo-$R^2$. It invokes the parent `_safe_score` method to validate input and parameters. -- **`fit`**: Fits the Poisson GLM to align with spike train data by invoking `_safe_fit` and setting Poisson negative log-likelihood as the loss function. -- **`residual_deviance`**: Computes the residual deviance for each Poisson model observation, given predicted rates and spike counts. -- **`simulate`**: Simulates spike trains using the GLM as a recurrent network, invoking `_safe_simulate` and setting `jax.random.poisson` as the emission probability mechanism. +The `RecurrentGLM` class is an extension of the `GLM`, designed to simulate models with recurrent connections. It inherits the `predict`, `fit`, and `score` methods from `GLM`, but provides its own implementation for the `simulate` method. -### Private Methods +### Overridden Methods -- **`_score`**: Computes the Poisson negative log-likelihood up to a normalization constant. This method is used to define the optimization loss function for the model. +- **`simulate`**: This method simulates spike trains, treating the GLM as a recurrent neural network. It utilizes the `noise_model.emission_probability` method to determine the emission probability. ## Contributor Guidelines ### Implementing Model Subclasses -To write a usable (i.e. concrete) GLM class you +When crafting a functional (i.e., concrete) GLM class: -- **Must** inherit `_BaseGLM` or any of its subclasses. -- **Must** implement the `fit`, `score`, `simulate`, and `residual_deviance` methods, either directly or through inheritance. -- **Should** invoke `_safe_fit`, `_safe_score`, and `_safe_simulate` within the `fit`, `score`, and `simulate` methods, respectively. -- **Should not** override `_safe_fit`, `_safe_score`, or `_safe_simulate`. -- **May** integrate supplementary parameter and input checks if mandated by the GLM subclass. \ No newline at end of file +- **Must** inherit from `BaseRegressor` or one of its derivatives. +- **Must** realize the `predict`, `fit`, `score`, and `simulate` methods, either directly or through inheritance. +- **Should** incorporate a `noise_model` attribute of type `neurostatslib.noise_model.NoiseModel` to specify the link-function, emission probability, and likelihood. +- **Should** include a `solver` attribute of type `neurostatslib.solver.Solver` to establish the solver based on penalization type. +- **May** embed additional parameter and input checks if required by the specific GLM subclass. diff --git a/docs/developers_notes/GLM_scheme.jpg b/docs/developers_notes/GLM_scheme.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9a7f630092c23ae0bacebcc4f81ac4e0cf61c15c GIT binary patch literal 137103 zcmce;1z1#D_c%O5cS$QDCEa5n-KoS3Lr8b2AfTW$NH;?%oijrVC?lZ=NGLskU{O-y zl~fS$e+I?tz3=Vc_k5f4%-MVGwb#zGS3Z25`1%$^qphx`4g!P0pl;v~^mPiP0>a0` z!#zME03JdTLP7!pLNa1vA`%KR3JP*Ea&k&4IvPqUS}JmK8b%u0WAqFR3>4H%%#8HR zbo31LI3i$tpbY^bDIp;#Jta9M{l89M+d;G>1f7JH_+Ty&9xWK37W}mX#0CO`@PX0d z2K+q{5t9%S;FIFvs#yUr9{!Ii90ma)7!RN5>m-N*faB5N(||x=Ea&&OfAInjoZB&m zR6j!tVP^#!^m{9zl4uwO3abJXx@#+2C5=&)*Fu{9g2LZd^_gYRWeW}-B7$?g;r!?i zm#)|TeY-!$AQGC)te6Z2sc9JzPLx)|wu6D`#%z}g0kHp`@EKNhjXe}WMC1rR?~`P~ zUJka9wM_O0rv&G@%~~7kf+Z|RJ8$a$_YOcH+ZJTBUOiYjV^qE-CAq3{lzXdkRNyO! z@HICqV3~UzbfSpUL-+rd9EJOr1*arH@nJMzSPjY4Z=5G znN={PoqCWnIByc<^r_cdIUD4(9n7-KQR$$rh}8uJ#z+w!X#OAiq9@dmwY7~>7ly~Elyj>0Sk=`Ii z-=Az+ndawF1&4w_0+xIc%+<=Keo*Xk>$h|JANnCy7>`%4Uhh2|XTFFT3d^eqkilnt zV{9pN+K>VAA86v5(1S^vPr|cVvpb07t`*b;0=aR_TH$M$~T}T64IJ z3E?s-0`NKuh$*#6+H~;5GDwUxweOZ)nC|s zaK)94(8?hXW+lXf8dKCp+?rSwG8N#3uj46Jf3ZhcO&nc*UB~6y_*$E>|6#8kErb%{ zA>trfQz&#`7cID409?t(#VWQ+>xTTts87ly-9j7XFZ3rs1z{c>{K7(`nrG{(lX)ZH zyR!K*t!>D`#Ahhf14NAwfPm5)bd zhMl`I$IAUGFC7FQfxvO61BA9j4=0kRdyG z^jzceZp=4E1p*QCFPD=Ppt7K@r7*gOaE{*RfxSgA$#SeN+M3A-;KSb&g@Du9BidiO!a>%>zP-^r4U z1&uyckci>;YsEn`F?M3u{Lju0p&lRJo-wN~*ZwB})$>V*GP9g=i#7-l{CcWX` zYzPZ#>ltS88^uH6^K?F;tHwdizMzo-S_qPzdLPU3L}S{5%p6LrTypwi1*r3;5yPtD zw-z7}?&yhIsiqy3|H0ed!Y8HS`{De4Ve_NMyA2y(+c#)4t=f*=o?JKZ+-NX-ZeLh4 zoE1Fv%c%++9=zd*I39=LVui1};!*kU-zSaCoa3EQDC~DsK!i3NCG^!H8L+$ zcW*4>h!BzXSc8Crw*dNI%*)XNlB@^e{7!;qC%EIdYxcmqW#dKx+-U>Pp<-2NAVY#% z!QW9p0N+#_H~Gc(sQmYDirA_;gGxmZe)LG9hoXt`s1#OcdFq1CtjdKl;{j|2rgS^7 zEU0R&3aqtcutX^)NDxZt)n)%Ae@CXui0UarV^MeoaFc?3>_^sXK7J5v)D_MX9FmCE zcYVk>mo#p^Bj?QFywPdIm4WA6>2LBT+GY5f4r_Ef=~hM49icwz@Z{qQlbXmNw6=~7 zw4Yn|7r~{Qbb@8$c$kg*ZyK8pP}TBAWc@c!WAtTzsd^5#N( z5S5it-$*>iV9K+7jp$(IigUC^uaf=L0vhmr_lo_xeCt~+SP;Xt^DR$%i&nZsoG%N8 zRiEq{;XuNDDO#<}Zk5X$?+3b?{7{~<7jF$h$0C|bP*frQwHT4bx@Q$<1WOW#`l#8C-+YnlTMS3c8e7cGoO zG_LRFn-fWX!;05ZcO7oRw+jAN{M%nav@z-&90qd@1_jYSiwZm#sPf_1lLqT6WGwP1oBg4C7Xq9H7Iq`-zbyz0lZAaH)c?`nb_gH{tzS2DczA1t z7spU5_}3Cvz>xsq)2&tj#wB`ej3&+J9cNJ42M`#>h=TBvp|o{D0_$(RFa>1mj35co$zImx zqmlmc`x{FKj`F&s2%5T54*|#Sg0km!i1{DzG==txkPS0%3YPa!Fb*AL2;eAfT= zCJ=;JWr!KaG+lUuidarJKhsSr@+ogi|NG*Az6&DICbXVH%=K*4IBR3Ixz=VKM>oFNHJ-j$+nay7kzuBKLp5wqo|v&RC*sjz=M-y`TZ z6JMy1;0_ClqpG!3Kj+QfdPMcMKGMcaq1(|)w0a>6?<=StM0@EgC|_y^75sf{aHuh6 zhaRe+=^F@r7bJ<9^HV3sod(t{7=#n#=bqw7MLN~GXl45Y}O$wcr7ne4;wtU8A2P`e*LBPe%`ab_?T7UeefY-UA9%0|& zYQgIQ2O=De3FRO=$~<^#Yn9r#A`bpNq2D(vnqlM>I6)vH&Z*5E*WL9h)KMkqL{*jA zIdl2y`X7)#%CM`9541BPeUuCIiwuX%(98CfN>+oV1Gcpd%vWYEmkJ#>i0K-|l`35J z5Rlr0{Q}mMU=WdO%*yUws&8#|+GL#Z=^}>s@=iNf36sAr6e|dJlVDe5GI1kupiXE& z#p0vm2uDTP z$ZAC13~>;mu#_NB5r8tcziq5B$QPx6?2_OvZ17yiR0>8CcE1!=IBD17?0wYocW;JK z`M!>ll@9bLzm?xr{kA9%#R{4=1Z=q?_JeUXU<>$1O0Vy{L1n#aa^bG2F{GK?`M1rE zO5p4&QX(rz`BC`~-eg}vXPK~I*EebqpKsNP-IB<4>>85)TM=K}Wi7kDQbF$y?ZDz6 z5CA}u#tkvKNFuY*{6DS-ptO6V&X=aK!=?U12j8!9;#A)LGKaLICMxgn=~k%5)J_|s z8WfL!zj;f%ZVFbSB>N5Qr^33Ek)q=PQJPd$cG3};!9e-_uHe>D5d`Y9@Ziv65@K6P z9s3pdYYu0+;Ivo{eETct=NvuKYp?-!ppTJ%^aD)cR?c6f_p{~lq^X<7wSH&@vz6Mf zpXU#J5a?EG5$pR3tVe48Prx79>OMSFfd?K6Q)TtjzAa?A>CNx|<~&J`l>mz82n<}y ze$nJd^ItP3DjIOQkipKcAW%FV3@Y+h4{SxaJkWpZE>bjNITuHkSdhewtlSLs=TB3+ zPnMpQHB1@%hmU%3J}>}0c3lSj2H4?aW3Z;^radmgQLwC1|5ud1cZwb}y%3~`o5jLo z9RV0Q?6;3Ggqk6+q#XnOPX1dbzyF0-)ZIM@#=|Vy8_0~d;00UF{MECtxEAxpY`H|l zs!IIDTM(7sKzFl6l%qM5att`=zddmX(CfSF47SrPn!vIWx@|qzkiTBGO9*P~{I?Hy zZb*e;%V*!xgpJ}1;Isoo^3V5?$`cq_ee%73{jlrNOIV>DjEh$aBCZ=moi>)4I$Uhr zf`eGndovx4EI>roBcIa-8-F^Hht=TTu7^PYj|5gI-d9u&SgPM7BA=AWCi)kOVE7~p z{p!-^(H~#?ceH#N#;>3Si2&CM`m|eaTO!n_t|(+PFl@0K``7AZ ztR!sptSFZfV+m7o;6mMM?iI19giwwtnTBy=c{3_8S;V@!Q~2+Gbt3XJ;wAFmIh$~J&L3P&6TYT(;mCspj=$0 zM3%$`z2&=7@fC!;aBKXmWtj{07rHW`W#xx{3MOn+qwpEs69)o<8jD4m#)$B}oSthD z2i_+SFwU4_`pSIY7475(T_L!nEN?f~*!^>>e9b z=?Uw3dve>Db4p`OXe~4y8j5Prco(@8^sY8&8Bo%0!?31`k#8(9vWV^GX5~ky3bG`` z(;cIt*sUFt;ta_ba3po*ja;L)sdazgnrhnsTPxQaU*vhI(_RfoPTvjq-(^r#j#==? zPH68|7D{PwosC4e70Uq6==L8Yut&5PeHx-h6*mkyYfmfKVjA3#epBeJ+h)>L4)=CD zXjz-ITek+_NDJdW_cPItKb3|Ae5>pw4qLsG3IHh@pdY% z285dKR}iYM3Izz6_J%eCLk1hXn>@{h1CbLoc*Udj6{G0wF`?1hZlBjYih4L&*LTJ| zWy3=|t166Qd%oOKRo<;ixH&3Kq*fzVh^cD5w9zoQ=oKAK_qH^;(x0*{g1sJas_O=X51j>e%jItibQFKJkS~W=T1A?s=ewd|!BPzmc9D z-oT2doP*2CmfwZZP#sM+%b@fDc`R=M=ef<%Win?=_;{3}OdIpic1Gu|@KCqy&#C0b zC5@5q?k`<@SL@@pcczKJ7<$f1bTDSizYw#ir_QaQkB9A#Y_sm_s6rKGe_v0t)LXF$dYB%^MKk_vHN5yT`x` z+TjYTf+wn= zhQnvYX-)Ri&RJ@KB7DE7Al6q2T(tp}aozuM%$Bh?AhiHC$Sgf+V0#d~2x5hNp&cuz zn%%a3yn4kU;of=eaG2nUub}D*zmvwGg*69d_DqtmpmZp=g=H{MRLP^RAx;euq%nmn z!q+bk&TNP5gzQ3MV)3@JXYjA1(9omXikeU7Y&I%?=q*UVyxfVBBj)|a@5Mz1s6*r_{Yp+}tnZ{k$(6X=8dk=eXK z7FY87T|yp8F;n`sjibz!4McxZ2val52P8j5t63WV?VlVxzw!3`!vf8P2L6RjU?A6a z@deu(KmY13IwD{-Z^$vV?y+%HK6ETm?)!{^LTfIrxPEYn-+VFr3aVz! z)kE|Gy4hg@Uu>_ha*!U*d+=YVeIucq^d*$x&^LHkX8uK^w>G2^kedD_NwU2f7IYfm zR$@gW)KLxw-+WBJFR1lt4}A(jzrgvQeyS@uc_jzeAB|c0(0ddF8YB}{HPGIIf29ua zOran~g7MJrZmREvdxl$HnEZb@M5-Va8q$b2)(qp9e|1D1S;BsA1^Z#is_wEDp!f=U zcmU`~c9dkMiXf^$H7hkGACv#Foc(A65UtTkvn5AuJ$3AW8UJ2z`jhd3t%ljqinWTr zsXkHR(^w0L19%+b1==fCBcFb)1so*DUuI@_{0jNY91C30X&b( z&`vYg9`80op_k20paT1Z(5c5g6c%jRvDuO z4Id5lLVX!+x@6Ye;PT}P+wlO+3(+V{Ool%yo@wx31zz=H@D&#EVDL!}Ziw@x0@thq z_%DoBD0t4r47UPSnL4vj?Mw9)sqG%up}PeK0{{_qhdSj`gkd{Q|#@tqF`*ylVLejla&w zo4ENXCsc&tE#pnzYR&%%8O#d!iES>e0EF9*%f(T?F1c)UM3tC+)v3I?Bh&fUUVm!& z2U+|)<=Zmf{juMw7s6MpB7$+Y5;7*%@XYURuFVzHiu|s<0uTc!MFRp8fbsDN34j<* zAQ}?TobU;Vh(UOaOyJWroZ^bs^bGn}kDZ}|@bYQU@^GC}2ZAF>fWS#GA>Iynq>{TR ze$ru1Xc1-q4#KOxvYbKW^gn-glX7F>4eh)iS3Py2p<4~UVRH%wZGkV%BG3i_@^xqovd0D*~OoXB!Whbq<85W<)R%GyqPpx z#`DLQ1;jp`sw=r!wnd-uCAHUAR8j|VgHe)uqa07d*Jm`Vqf~BjxMgHFMrXh09s0Gr zZD3E5=F5f<*USBF#5Bi;<%}|d3gw6{;{itXs+G_9my-==mhAc=57v%oKG*)Fm$Vb+ zoJMdbYEPRvF5nAUzkaXq;~OAkjbnmUkI>}nN4DkGb+4ZA{b0Mufpz< zXU(x~gJ7oZ&%#GOBUum^m?s-z+y=c`~^!EK~!MOW~sHmRY zM~Q(+Fbfi%GI(Rh<05Eg!#2|)i3yXz%2An;>;PAeqSY5?Ec~q{^duAmyAH>A^SCib zWo#6*Qo#AjP}wtH4oVXOGu?`y;@Tt~rR+q%u@uY{-eR}$&gp<;!#f0Zc=M*## zlF;O7`urno$)(3%0X+X_MoQ(J{6w5v!$7WC>S3TDRkH!? zIevNx(356|oJ#c@>xYXT`dnN(BkNTno9#-P9kV!EMI-=$eI;+?;#TR-mymL6d{d$@ z8!rlpkGeFFVmdkM6pwu+rd-4INJFp9!V5K|Y)|c}RZ}_r3^P^S^1q1F4c=IOIZO^e(Y<2X1t6zxBQudB@!K3cN81+F__f_v{c8N3=eQEG2x_;eG} zYhht#>$y*xB(LSjaeL+!5^yd7ECxIa*st{Bnz%rPkv!*^P3n6UNs27x(KSO_{eDe;Dd#mKu_ zRUxW0GQN=HHw6rEWm1*o*V_u|hwFz?LJFMX?~)j~&Zs-M#FK5dzGJ5><#Q@NH&>X# zNb7NDH(YAnH^}t#o$@evOvo6Q1z9ZfbyLmvpYrq#gz}VU-ubgdqs>!dyq(x`)HZLI z#MIj_zqWmQDFRnHCi8*0tBZnKr|h2b^U_JR8YjmyIu;H#=8}9wCKrdD@u-Oe2h*I| zmp|l5*C`b066Gej0bZu`+7AVVTbk?zg$Jg%l{~M@=OLb%Vn(C2sMLY*qPf{+;bneTF zZJrp1n<7$xVd(U(qAq4*t1(aC1wI~8y;^o9c;^c z*S^8BardBdB?%(ekN`aya%F7C3mo3SBy5f;uh4iW`U1HF98f>qVD@i!5MgBSz3 z0M7J_1D9>{bWT*OEizDlI&;VmUcItz5DYgNn@QYTZ~qFan>bkh#tQBoZc*%({l)^q zUm@u3O*FcpFrf06Q_&*ic|27S!#mixi>p^BuWG0pRQr5mg7d}XT2UpMHw!e!`>WQ2 zX2#^D-ap#MVkd{>$3E4}H>xFWAgT8YUwyiM*!SyK_^`{10(wN@x2Azh2BDpP&#>+K zH&XALHH>Oe`U4wOJ01I{Q!NKetMXaAra3 zWU5^HRq)Avar}|Vos3o6oq==T|M1NfduFs3EyS+)+&-`w!rQ;l)q2IjRdYUK^_0}@ zH6q(tFLK_`{`n-MzZHOfl7)`4p|-lR6qVUHB6sgNJ!{gP5Mr$oc0r z?%&N-4!_|i{_UdvcgM4!xyu^IAJjDt8I4}1Ga0uZ&J0;ZC^Ld zd&hk&aWO7rn{k3Yf*blcuchq3G#~P5 zwJ*_WQFRTeW3SEJfru5|sb3$z z{os6we6*5X5+xy*2CLYX-Ypl)rBAfsyQ&_ESBUUwIZKY-p8=EFK_4RkZxya1~_NLGcDeruBlI0qNXn;@Np)7X~rd!9y!_x zNsip>{dlulg=7O}61<(#eGo0qx(lrkKRjHfIL?{szde*G?V8HiI*sG|NZ1%7d_(A( zN@(3L9&%NN2>ZaxmTi~hVhDOkg$le|PF)zZ4Vltp4xrZR7NJYlEYeK+#pje!vHHYL zvg2g$tH6RXQIoVy(j0do!wsnS^l*8%w>XMcCXeh+Kf{^3f>+K$UtdKwr;Mv|6-7Yj zDH5iQZoI4TJw+ERV-Z0A7$FTQj&Z%DCN5H1T1jIYiI{76a{b`mk_7lemI(HVta!l3 zvXIwqoaftrvD*usDUshgu@fgp{a`Bh+J&uJW(a{*`Oxc{Ay4itO>7(s?2O@+PbuLi zxZ(`P;WL~6nC8G-kdHJ z(+KM6LJ%1t9RrEuT4-94=CAZryX%|n#}rl5iIcYYQj5?$dvjs>pMU^FciquQAX6+F zsqpda+TeC&i7Fd^+~an)vuSP4)6?Nu@h|P6j-114`iA<`BO7F>CMG&It}dS>4EwIb z!LZapV!N}{hc#_|++m7m?^C~j;N@2QB1HU+cBCchVT|%sE;+ae-&fF$X@V%gy26B? z>%*}*P2lZ;+zsnUy!jS|G0^5qFN;8MVkx~OS;cBu2fpjOK)D?u?PzAJI=3U zjDZx7^Lzy@PPeTp9OOD2^s5}yTzAInzVhYVnJ)uKqn+34y)?$!iN+te6&Cm0-t1x% zH%Oh_42A2R$rrp6M;pdzu?LjI`8|cTlnT8(TzKBq^zQSH@vX^}Hgx)}w9&xkl#lf-{>LCGTIt0-gJ|TGXF$qO;cQKyjd9S9TubOn+mZ_^U<+D!p zP8p6dJ*m}Ks&qSrW+mS?$V*Z>`MTMYD&5oJy!|Z5c7xPA!_qU_lJV2pJB#qsy9wZH zGZLQ6C^N=EGoHLPRkBTN(dd`8iZ4qQ6q;dAEn9}46s|1_y`r85H035X!PnfDl4I>m z?|odCinOx0!eyS$?h`F-Rvju!Dca}t#|udp`3GJ;W8KbY@#$h}%`mcU#&BO&#C&xjnBWY?HPRS`J6Pu{<(jnF>Y_y**jq z$Nd2TC|pphem1~Lng5bdCK=i?vgwQM zSzk77yzkf|b!vzy|E$l2vs!E<&fDK4i6LxRX$zm7YvL%CUcmweleDWG&&IP;-N%N} ziZzoONz#;|9A-U@r1baR94dQZaX4$;Q6ufotU8sqIWdU!qhgxl}WKz|*uhxrjtGrj7%e(4kp@m=5YU;ertNe87WUC)R7`jDqmW{=2 zd;Dm}OJzwwBs1^LsvA&EQQBU>aB|&o}ZXPe2 z`nH9(%7R1hT!$}6=D%>eey$9z=N}M18aYhwymab$pe9a<-p@F}fzA$ff)Mn(lHMIX zcCMUHcNOf#|59?0i7kFZU-w;p)^jDP>`+_ySzZP|p$jJEiqX*;!>8;tA{KsXbnREn zpJ@T$nu?Rdj|xNvXO(}V_B)*4bi#BF&&G@fXeilE7(^oGfPg5L^b^1UWY&*oBJdL)8 zBBa9J#7!dl(?yShConfJ@YM%lx`|TT&}#2p+UgG%@fnleUVO~v!o|jw;v8SXp4X6i z?qlXewzicCu__YhAni5Gm)T$Ykk;rPSSus{1J8r1iF=lnkhL4JIBY zoAFb{nGMOuuDZ9f)Tt5&uxG}k&fUHsFFMwhBc2*cwEy72MM>~(QQ}lu_;5=lzBG};D65WX~jGsl)7YlSPExqaM7*4Say@vAa%J*gLJZj!lG7ng5EBaM#j=xQ5@ zx}A&n_)K6+MEUGmJI?r$4Vd$%eH-9fpS>`QF&Ema=6uxe%_bIEz09Tg{9&O2wS=PJ z-UR~oJ+!wQ7SbgRC1S>B2_zG{Unldr_vWX#c-2voRq|wY&}1gmb?nEqd&~dkD0P2emWRHOw3MeT6WSPjI8^KAHReiyV-qaMlK7>K%8E4xNt=iiJYtRAG3!& zhh@v#-J(idC8DIFIIPoThB6afPxs&Ve&|ve-V?qv-CrFwHC)b8~5=5T(@984VI2GPE&W}BA)4z9WpOMT9A|TIf@3U!%DY zhu*eI_(v2jFFRb&24xcqk7S6y8f{u*(?Izu_8wv#bA0*d+&rOZ6x7c$Usxnr#`&H zNaPoLUUXWv;~v_i$c6tw_`r=DfU{;%y{$=BM9ccsNzPN5>0sx_kUQF{C8mpf){Rv(n@QVsCp zc7K4qDWHA~nQiv|JjucRtj>lg? zn^hMH0Sk#Xwd=jpfO2%(nH_s3SHz67lfqpN4Y`$aw15l%l5Qv!Yjt^t4s9T1WP@Xs z3;VdT7HxLLKhGqEtfXAmlxG%HsshxQ!Is5Ynk45-!wq_Sthz_3ByFl zONw>aNE|~}jr`__mf#s09q;GhU|F1)5D-yir#OL-6kD>(HMdE5%!h}wd%YDd+$T_3 zlJK93+7zx{AhebCPJP$9h?K^sLcS2bY_oP=QUIk*Rh;dtaj^>b!D$*`Q^gOh#*HW7 z6~&Zl5{p=?KwOReb&FVj5F7vJE6UGeTvNi3j^$jDnEQ@$Y(kc5{++>lN=rmVh5-tR zR8QfYj4My(F7V>L*Y0VrKDqsAIRDVpta9%p?JGLdEYme+S+-`Syxi`}gXyG)F2*Lp zBluc3dkAVSISA;!j0j$C3h{u-3DMPEMRy0ZY?f094)$5O>yLntdLkv3^HSodzSX2W?P*VF#HeDJxu)m zn9e_~i0M4FQY@P6gEZOGz2qQfsAC?LJiB{i0_*$psQ!Z54HuSTAC$q)?$Yn4@~x4Z zUe>K_CJKQ*T#C+iXgT`y7cbe{?9@fgS6gw4H zPy|BC*Vr6NKZAyE}eHx zeOJ-lhT_pJSSWoqZ5;XW7SR)j%01CL-sq_K`(yIcIXY6l(?L-<&m<3A%5j1Vb4J3G zBRV#&WGR(?u+PG?xcHcWp&>NI8OtXUa4t=xxswMtE=JmGX(jy69u$)l5wG@2?QPy` zD12XBIzgNu&2Zjzh=~7|9AINiOMlHS#iC|!Y?S$)Ue#_aFL#L}b|iV9qWvbRG%UO_ zPmPD+oaW};TLsk%ADn*~t-YNy!j*7KM$IjXJgPQjvZ@I7AZnqe_QY1kg=~mctd;G4 zm{;7K_)NpfqQ&iA!;FQPIK}Serjg9K>gg17PStB{jA>@9vki*DtsOhzMbpRvHp}Mi zRI|3vk}NJk8`T?Q6F#aeAR7VDmGJH|Wp3sJ2i}?nbrkKa1kP+&+e8^)yH>yk3faDA zHeZb9+6Ya${~jjM5+?b>9`>MD}VF&(2c@os-pazO++RoCo=QGHiBsk-4#($r7TE`o-k29zJ9{<3yN> zIATIXkt=ul3(-7Fw@s(p_#kpa%iQYuL~sK~j8O_DHQp_fEaQ;uZc#Qj6kQE3pVtY{ z=Uw&}yfPEpP7kwTlY_?8ZqB^ujVu0()gsivGDo{9O5lI>K;ak@i_X*wI*XZfCE+= zZ4BQj#}3kKymdtQ)-!}nPLjIrHQ`+GCM`a!=?9i-GPKN3Hv~i?P!Rn@mS}(nv`iCY zh-xZKWm5Wrh|g6W@7+>!Ctf3tx5(e2?lZT+(%6H;i@|LIoay9o#TR#3eH)GaTjpGDwP9Cdx(zEFn!n>V95h?hv^^ z$;Tbr{>3t<2ykvo{hsE(9EF>``fJhdFhear-jUn11c)RZVPb9p!ma<4v-_rI!dS9Q z&8ClLOV>2S5U#xB*Pr9UcEZ2!3?HDma;^~|75C}>Kk}>pGv_?%F!@qfX$eF7+M)A3 zyx)76J<`xCR&In$!OBARzdH5*S?AvZ27I*0M?(%*rKIA$;o+s)j}wvLo`R*d2%9Ck zI3(@l|1YazhtjkxCdEb;q0}ow!5P;3P3nH)DJ2ZV$6{J)$!n}%nk#rpSF>mvhV|vP z_wD4hT^7ksh&HQx@C+7ikT+w6i%IC^n|UZehA0uCe0R66UX?)E_QI)7X7S4+dDCU$ zzU$jpv!tTHzsN;McnXM-SF@<4?8)|%4O@pxYJb4z>{%(3VtHE-Ma1@@>TGKS%QzjS z7js+s%6TM@(2cQ|34y}p9TW*6GS{eb{Yv^C&~fEDHKBO&w23!LXitkE)5fD0whQ`vqx>#?~jp0%SXHwcLk zeu7%(=ty$NPbJeD#$4RFLX3g5#fvYC%6(3usHd|g)4u>ob2XDePd~(Sd`}t;ST>4> zOKH_T0NWzE%vnNFcn%s>mV6YXpq|I>5QLADQ1M?c@;n&?BD?xn59U7loMi0ut)sn? zamzHS+3h6bP7vk9u`|}XI30e$vV$m#gS+Q#qokF9WF2G{^l=TqLp9~Q9b4+O)-RiUPZ{?+J?(qcarfUxio)z$2 zh}glgE+NORWLR37>8XQ5Ax=^6B-gP;!xXj}^a4F=qmh&Q-Sv;%; zWAG*6cFuXF7z)SW9vPe$rO=0CYz*bKJt;(&iaO%dDQB zsgRpEcH+*7=RD*R-sAouyJY}Xom(M`Q|-TQ;;U!J^VhEt(KLJANk-+iEK=_s>`d(3kDgci*H@gTII6l_S#%Xm|#hrf}UxSJ^5svYqmPXvha zp`JLy-=9s_QF`IFcjz;gEG27?QZTzL=N+F+mF(cl*k_Np&xFQHo=ykWLb>;}bSDmg z`)raBB;JeAG}DI5Nvq!R=y&M*K61<%pTX(Gh$d)m;O}&=L9or&6X?Co7rXgWNBb{Wz6H6Nu}!4wi&*F-*pt=33)K5dPIpzquF#E{Ee) zGb&0FIc;8n0RoZqa_Ktjl{4wg2L3|b9rk7o4btfmGZL!xC$9{&)xYPTNSK65o}1x%g!ZIV-5|M;~HV1bJ24+wlAk9z#Ww;-ma}X>P!3-@AN!Nsu=>7qwYbM zyui4LE=qSDKPeMZl6ZD(h9W)U{R$1aNm^C6|InymyB?Tdlj~w%f)w4=ct5fV_3k1{ z*5b}8OFzIX#B;BqeBk&1j||_!M1dI>*)cN}6P@~AeHEphhCuF1ju@?qNwI<_n3v6h zctA90CS2L8IZJEa0B>7Z%OHfi;cVn|(X>Vf8x%8zJQweF#n$A|u60Rb&Vs=Hx;~jR z&8g##Y!=7H5#DlX1#cyg8kYrX(nZ`;lGYsXnW8m?o-)iQj%ioGyiaY*RWn>BhlRR% z8rL$nxJu z!Lv#WuZM!2VSpV~KySee$}vzktu{M*d@E6qwLYFHHZ`#i=0jS|{ZVnCK%YIE?i}N7 zX_511DeDcN1M2H=a0wE0Z7#8W&gm`)ZA0T3V6)~8QFRZoog!>{AIeI2Ayk^gA2y^+ zC$N<19Wvd=fzer6x%A>R`9T>%8q0SYo9p+8q?wCKtU{g{H8{melpeiV_&PzFOuhH& z+tGFyod~D4dKQnX;KN#3-kcHj8?Ut&m(kYo4p^)ouQ9XZG$(e>&{pijRvKdm!`M0U zbV{3ZeEWrC{HJI(6HJyK5Q=zuk}XmxUJa&jgW#=^aPvgJxQcg&(ZZF(H9!c~7^3ft zGhdpl=5#nF17yUs@)~MdJdBdt0hYTsugf=d2|5YrRyiC#qww=%*dhuVUHs=D*91 z&Ws_Tk~?W15pVraMXkWKZE4KHp<9J%#HM#r+lJ=)YPT9s|KQT)DWi*p%H;Q_gXafG zj&T&uQkJVuXL%(T_(nN_TySQ*in2KQo-3IodP0nXt5Gz@{GpsnirS0gR_9eL3T*L4 z0|lUbA7{U$sf52IZ&E84-nq}?CR1rarnDy}3q+%Si#>d~kNJfeh*!PaX+hG-kM;I8 z9=3i<(XJ##vCRhj!wCXGu4dS?MFs=Nmpi7Kk&wAw1z{#W?N^+mX(`LTkrUh({NHEN z$El~Rk=|IlritW1_jEqG<%f8;{^&HC7x(sctq6~2txAa5H#;$FkI{-x zmA>n2Fl$=Q6s3Cn{wWvLub_QDh5K<3uG8%dh370_u8W4$PWjEY4j}wt1tv0==T<_z zHqJ8Zx~o*_zr+KK4R?Gw09h-CM~`UE>vB<4Z-*@H~XqavDPcW+8cXLfWuTt{R zg%Y|cQ^pHri9=NSG)zLJrb&Cj@3y&7I`B2f+A0>mHFuj_Gs`P_R0 zhzJDCl{av4&8y$CC4MJhGr>>8hS*LqOEyOqvnbgTx=e9%auUe!fcy}zjMwhR#?cQ- z85!6Z3X?N3usy{E3*ut`J~wvCQmRF0*4R6n_}D6Q$1k}-;Mb@Bu_m7h zXHo51YeVVZgk`OY=i-Idk)!~aJS*KZ7uml@K*HPCywl5+Sln2ddH`pR)Ki!6!=V*T zrfBhZdoy?s5qCo>XWhqd@kZ+o(s;RVXX*lUioz1+GFiJJjpbYo4LmM6TAL z9X#-~jiM-SH-Ol3O9ry;NfS$H;q9ba8-tgVHWFyn|Hs=|0L9g1`yL4H4nab2m*5)Q z8h6)V4KxI&(Livw#vK}WcY*~?a0njU69ObSc}-^Sn{TGB%&mI$pePQf`|RGFvt{l7 z`mMc43pN2)YN;^P6Qy521qR^vJrS4B;o+!fH`xF-b6AfU3qM)~iyiF?1lI+AKB><+ zPfzRt8b5W)mC@91Gm91z-b|MKL`+RrC>tifd&hB@@T*93*@7!Z;*|+C4GnEnI^1iC zclvc@-&mhxcT~aNp7dt9a1wS;q?cnAvJv6ERABn|v!@0Cz(k@;c`iwS3(n{-=)!%SGxC`^F<20X%k1nR3S=Sn&UzP;d{e~MHRk}n##(`Vp20Zyr z6zGa%@^LyEcTMP#w*Gh?jsBpIimwiS=56gYG!;sge>EH-yfEt1BY7DP$Qm0Apjk@x z;MT;TT)~@UE9TgWU%;HZg0!;SPCHr8w;CtExi ztL$^lQ9X{70WSPkr0;)G$bZ8I|JzsaO3`0Xy7;sigUwP1Sf+Rku*$A)AqpODys#rlo$}$b(`gKSi9mRK)VZu6A@F z5!~DlqU8}WZrHNgf*=St5ie`;!II4U{1pEORoThX$0j~K+<ef5g?lC3yjJi82M^$f$6XOb zbe0(${4Go-i{C4sR=5Y_mu}DQ6G`b?@F36hq>uzc_qk8;;E8q=2V-Heq{3*$5$^f} z9bwkf^ePn!dj}9cqMp*(sKJbJ?ncZ*3}1%KB&G z$akCtrzu}(AK8`Is6EL;u zE%5MyO}wY}o%OF~mRAbKw;s!rA(Zq72~9J1(UpnFOx}cnzgoB|pM!&7Tc*Hf=#+KX z8qM*w1=F>CooPGsi9h}$u;JGwm_*~4S2>~7h(5R$Zt~;Ea&*0ZiU>`;{?LYkqxp0z z>*_m=kvT|lnLEtA|72k1vAnt^#wV76J1?RtI20gnA!#OUws${PIM_XvV>e85PaN?u z5Hv^Ysu8e>CD z;2Wo!h00EXKQzns)03YDVQZvE5Bd%RFfIFG_mI;Jq$x3`Aw z*RVjE^Q=jjuP@c!aN+$7J+HmFy3reDeQBdoYt{Y+)em{7FNmqNniad~w%J`)d|{kJ z*ZHxMy#A^W9sMdpy2gM39qhg^lIi2ZmD8K_6;RKM>mk-r%73AXqcdzed}GHeJpDG& zz@@+k3$-Nr(lRBrnTlhIZRu#L_)8X0=J5{=?*{eYE8-5wZL>O8-|bqKHLvJ3qR}8p z9Vb=kucfTAtS=DW%6*DV`rFNUqAf<1#VCF-q?0h*jWd6Kt4MT@T`8P zLa?D>2jTR=k$dS%ltN{q=3WPd!qE(WqE#GW0A)EIVWf(W84PG}h@V53=X{yct;Bib zp+fiS#xdQvjo^CC**Jva5oZ(D#KxRqYoivH)Xi@S`ciJFE>1j^mD6N--p_T4Y@O3+m@qsJH-ng9Ul-w4oeuNI z!2!?(VJcsCqVYX(CTfaYlD*Ay#2kh9s?R4_hdY5e0|gSV>?$LE2IANM48mh(sIHLv z7$TyAtu;=OJtV?W{2kSdFdzz zJz-+kMgYIm6qW-o{j*-)1fz;&bJSy4z=oTH3NNOGhO7@Z$$7c4Msbs?&km7=_d_=B zvLej7?L{yJuwiG!c2+N>V;R;!@AoBn6R};FC3Q*Smbb7hpJGkypMVEI&FhZK50xGx z!Z&Z8tiV@FINlwAV@Z=Q&H@%)Yhf3jb#M(O%$f{d+`d=K1@yjHsmzA6`Q`I2IL++nq$%XOY5a1CI5fNZXc3^?z zurF9L9Yj1heCk(71dlaLv=J--k0T8@t07bM7(M^tP&D{C7MMup7`n2=xf6e-O4&mVI-<+#) zn4=yxf4{wZb*GvwGt>Q13nbTiW0_gQQq?D5(l-^O!qWE~(n3n%6Y*trWjDeb%DZ#| zO7=aB5QOqcN6dZ6ksYcLtsiQuvwN@6-p1dn*8A} zK)iK|gfv!kdAD+=fLjJ{NpezI!3<4SOv zm{xmJhtp;tk`wt--OmnRs3-d>tXPKplH(i$f+Zi!8 z*X?QH-0L{LNZ0%;zPCI!dv6TH$u;x>nMS4j((pn6YRFl;kX`WlG8|Z$2k_B{ z{5V_@ZnrF{&m>wHu$Wvj$ywS_jE0cMY%j_;?)f#uwH>+4)NID+%d^y=JqB)klz3|d zQCZdqQRZ*J2ETFMSkv?jK^sctAN2#p8ClkH6eTgq*nU>q{#AMW!)0LlX!Y*s+wJpk zJS%gTwT*)?F+56JvI-t&_&IXM(PnE!X+8`u1EThr`QhbePXp}^-8b)e&dW-o=u#SE zH$;coqQGxrRss6BU*EX&d?~kuFSN@B=kL3mteh9PIJS~9BBceqyRUfIz#ym-{0*1) zeOEVdM@~&cPQRy~^_Y}3&N9iaK*@DNblvy;gq9LzgR();!V>9*y>9fe&9VNBUTsTB zn;r%ob`b^4EY4dy`X%FwsQw|rC*CLr??|>t}Fxx@d7H_Ot#TIld~+HddYttdO_7;Zx_QJj{>LvRWHL%VC_+RLVxF0 zFk7|j`D?-hji%Q3L`pfVGS%rbo!$6~qn8!q?!4l@bHM3@LuVv;a)o8xoP{rT(cKH} zw;mZbCIkU|Z0u+oeRwi6Es#=(g(-zN!4*>tIu?7!tWni_W0XyqH+ex8K_!AjY<(o5 zIGlNFL{Zt*F56kaum|9&K9*wJ8Lg_a<*T~84#1BqESJKp)366wz5-fpdG$i{6F=Un z5!aI3-5nLq-lIP)-Ways32aV!hJuZXiHM8<+wEfQ43@K`u#wCJngOXBEuWoMn);0KjQeko57M*nenU=O&|^jcnYI^?a*^MwZ()=0;}I(jgx_IR^~){Q(T&dX8Fa|PY|b)y=~=9BWB8`wTY zt?a-U4K5U%L|-|I4U2%uX3Y!kC;?LkG5nuGD&CG0p#1w8z?V87q-)pC28)8o;suvb@-exh{tc=~M(NU)nCfz4a*|5514XEf_ z-1cVj0+iQ1O|h)s3mWE)~;d@xT8yX1o0yG+j{r={6ZK)BZvdXWS6eDNKv46u~)V8%(Pg(qBD} zZE;A%CHre29L_Fht8uaFcrSLEKjm9kHXA$eHyrmn^11oEPb%J--Aw^@%Rffd+Iv%P zPwqsv1!5Y498}5jyRz-UUG?qk7_3|ENYyMz=)JyTN2yq|5ezAFG=BQC_E}Au%NvFW z0-w5!`lyA^pP4AUM$OGRDz{H;vQY{R{oE!sgo3>*snO*swAo7T-x5{gMQ*WV;L7Pd zVvxTW`3%QTrQTV1>oiSoVb3&OT{8%t8I7+%T{xb>1#~!DkS(*dVqWR%mk<(Kc(#E# zKkoS=z2&qt%~>USJYYKtvr-36g*%@%8C_;RVJl^)!)Ufy>&w*#Tv36Z&t{2yTlT-E zZ;Bj->0j?X8ZSmxuE?l03l~E~*Ph!y5H`=L2N~NrB~xk}g7_;xaIdJ^_W;7hINvK| z97R{4E!ct(HfJKcvOY{1f2%+ywa+L2(n^)-A}ou1$#(1^Ar$@yw&>LTpykS=s-?*3 zm#W!wDx^cF*Y|FKY^aE3I|@uJ9n&+_3D(29Oj$H1J=d~PF<6KhcTMJw=t!edm!goX zpe%w{^>507Bu;l*k8yKO#5xmMy&B?0(XB-p$^60RYj;20yq%Qi7RZd%E+)qG1s#yc zbo$||FeFU;=GBUBc(%M03Dn8b^OvDlxmTa-iJwk8`E}3kC*%s>pNqoV@VjV{EA0Z@ zbs8p3aC&*DSw_J0u3O?|MX$P0dPRXJZ^#Q!wCaLIYv`i-YQH8iKjk-}bYjO&wADFk zi#`)}H8sf%uD6w+U#OIG%Lj3-xNVwjJk^RoxH+*SOBUqLp2rCW>yE{Im17OcQ(_mVxBzg)592R43*E{&pahiDg?vrS~6pP5a`- zmr2BdK&oVnZtio4=S>MJm5l*bdH~R*u4EOo{|%_5Am&n`<$uHI$Md;k$U;k(do|sf zBxNN=$m0^-VXX93_vA_JB#>+67|`d!!*-DK43i5gnzN@>vC>*GKdn!Eo2K&p5WnL86rbIQs0UrLKS7%ROXk0Rv%JMEDB&b*-ntlbQ_YHyRP7$R}yBJnFL;buKH=twRAx+yHm`ZIx72B zfpwG?+fe4T>ezkeXPCA_s>2S{>G3cU!zF)HsnuA_^>i!tC~<^i?O_|$r5XBhfs-D{ zhX}fH01eWAT;M22Cqi@#r!zAQ&Ayr5F!<2ojWNeHl4Epj5^387i|OM(wnYydxVqsV zoFAfF{&}31EQ?$RT`O!q->NvQP;vk3_^Rj7pm8a4EQl+HUSV zrx^uRBS-tqePa1)+DzHJ@cIZ{gwLKUVJ@*qe@=K0iYxMpIic5Y@yDOS-j*NhYA|@36GBVtr<>$tg(E+9rCY?%EY+uO zSN6*Ho&gc#IoeZev zA@3AtwavKDou&G2k6&1fwxb!8!gUZ|FdLLiSQ?Bfq8aF6E^F$JGfP5;HbV?l zg{&3Ywsy%hQ_J1)Rr=!8yN4M$nFB-f?P#vT@_7H+QGdhjh^d)cuv5~;@;$1~(|L8u z)go&P=rG1gya zn5=v4ZnPkN4OZHemJl)VE9!o&3~Xrnmy#P+U4Wn_j4{JlIC4wVvHBH_WUD0Nig(pD ztmb6R+v5s0lF0$zSUpcMqJ;9U%5ZWaKL+nGqR9uBexZD>ZpBR_*Y7B1(#C6e-sSM%$>SsU> zyryZE`-cA%-&IjUpjPYtuAj^6{;iKc`2B+}#{beKFL(c4J98*=mvhUOZ|D*4 zVy=2kX|*Lp@eqME5E;EeUF@)0EzM3m1F3CmX2p&qrI`$lf7nQAq#=Vgq{7Z{@pi0~ zGeMerqGX#bpFd=w=^6axb2AFHy0-c*l{|BQL=l#-p^{;Ec`O>=v4{CP77Jp+MH_3x z@eZ8*gMP@kTo@7s{)l8`k6_eH?Q?>8HVUA+KIVhy1XEh*B86O!r8(Tdc>kqtQ-_mnsW2z~t=`UG8#(^gvp9p#<`jnTGYvrR zfP}pfd#-SDL)ff)uA8hbw=v<^s-+7CtbjfcNt$+6&FLY&k3V*dR1zx!v(cn=ZfA(@ zmF_qxx-+;=(?CgDt1W``I&CJXd9cfe4ZhhhsC?9_^byx#DP4>@P!K{>S?9)XW9bFW;^hpJ8erfj$mAf=X}9F-c;OjljxT4-%Y&# zpO;ALq}egCysuV?np>nicO6K=V>^W9o~t-m_Nw8kk7HiS@t~+g$27GB)nO!kv)v?@ zeS4+1!(sN+v<|&e1yNRE*u)%X`RLb@RY~1FMW+x7z*KGT3sbcTtBLcPLwt^gP%l-A zFu(l-(#)e^*NZr|)Fq?zB;V#Op-R)c9a=Y&wOP7?%7J&t_IqXLA6h|f9rSYzCCdig zB|0veH>{Or?|J!`@nM}!Gb9R*-=A+uu_fugv+!uD`t8I{)@LV&pQ%~T->C>RE`KP zQ;ScE9E1iNg3G03;mBX-3D|ZxK`%A0eB4mdrv^X4Vk<*m6V9v<@_KY+s|7zA1xU|K zGTd>#Y-w76pghB9WLQ;9%wPtrF?SH=ZjNP_sS39l%w7=jh*oyUL{}A<%!X6v3GE;= zbmV7uZAa6$=S+?m_hT)S8E~8NYF#9X@zV(o2Q8Da_BPSg-t?^3 zAJROinVA%k&&YJLIlbYjx-3lr`X<&Ph>U3|=h=_4ja$yX$n3peV0*uBZi}mbmlK?v z6Pb>si&^1O%-5K%fbxwM>4}f~7#vG&PgJ!u9T@Y{{=G|cF<6z5XINdSWH(Qp2<~1A znGvY9^roy{psF|K2`fdA5>H~0g*^|d1Y=G#z)~)~+adyJQ^@bVZ~~7*+4%K50G-}P zDqvMSk$J++p5kgr^s$SIAzN(e#*UpE{>jMh6VjI7aBm)a!}JDcPiqiU28%ex;62z0 zzs4|kFNKZZFG!_akXO~I z_D&NUIWINT(7Q%x980yPEIYGwsvrw(qFE$Bm3o%nZwm8nUgwI`L>%wM%#msUCT{APH zP(jJeymaMGBD5}-@-J|opQqYP^<0c&6$q0cDSq(cT1kzWv7XuNh(V9s!(GkJ{*`Gx zwk8*8*#v`GUy9hNw6Z1lp>MK&;1Pw$lsP3^8SAYa#-(j$JBtZ^}7*irnAu-Ey6N8S{p zANO3*y>VW;@9V)-7NcYoz_tQ#X&@ zXy;$z$Jf?=DzBF}KQ15D+DVs91fl~Ahvn;qu2f*y z-&pXaZQ1!?1^JOmI|}q5ML2RH-sn4X7?qc(KEL}B;Tj zU^!t&XyG>;_$$a9rFVLIW^S_bz?Ow$C^O*es%pif#z%IO^)#XaDg3pewGJI$|4kA< zJgcALHZ8lAEQ{C8Bh{B!tSj5SbXxCHHkEq;^(Ug)@RyV3X-5FXp*^JEaaVwM141ra}lmEJibUvNJ~X@y1Z zz$%|uv%zVt)5ET&C{nx%)8N->>bYTnNps|=Ug$7t0f3N6U}?(%@+CVj3q3PCXicz_N5d^`6Qt3yjlD_1Em_LQf};mbUvjn*W#^|D1y&xI^7`|F<YI53tXsfei|4zbJ3(e9@F(VS4fy4GiE}3VTK@|A zee2jgGYLbm5DwG$1$rxmp%Dn-Jheyy`AcTA2{wyCXhzDqEH^KZA%h~Gec-%0kHGiK zE70TcLm*~jQZm~oetP_*7F}Xie{YLFCe*<9k+H7T&TrJLuBWHy%t~YPNs3S~^)P6s z^%cX+H{;G?DI=zJXSG8Fu!BMa$i^sr)peN%tWc6dJ&#}PDih6LX^4S8mE4s9Zb{M# zRIZFR@gOXAwG@42QCePJvcVx5nwnL4U=u}t^giN45^JcCrOmXoj{&&3y3bF8lUnXP z*+hn_&7Ny5aqhlrmZvdXwu7?JT|X-4^i;2YKHC6P&prN)+f=CxWFd;~X-5Zjc z5m@oO+*84HkHwK&rzWyH)ZSx};wz1=MJHdRm1pDKd{CUpkHt_`5oJToSpuwv%a74A zX;_>yXZgDum^7bZqjc&KK5x04YW^INg>t#t^iBx>TJ}Zi%+O#R!i<@O-TcqsQ2*`?hj{oU`l$QiT9h^4VI-J0^RS4 zH%uk! z#-+$gfMmYKC~)S6WIBGtNJ_$QsKIluPSt#`k}l!oWROwP*M)nIFyy(7vFhr2o_1}B z9L(tr#BdpBksT&>X0}h0q&pq^m+M@eq;!EFI6}L|bb*bet_B%Gp4Qpbzl`hw#}l!G-sOegbbxTyqe?5f_fD(k{%T>64&mTlx{n-kTunLPUF?Um8Sd( z!NPzp58D@3XSo3__JunL5&(^l)H#gQeg)N~lSQ@_*O?xeCh_ugjCdtNDYnW)laK{< z3C+jwQ*s(ul!VNJS$)5l)>Co()9Tm5WsJ_RRrH4r{Hi89XU4z~sB~^ZX^G>KP&lOD zWdR@h!bj?gBD)LvNF0$*5d10^kMI`}0??Dk2IU*PAcD_}%|y*_a%xZ7Yc2ZwWrT&Lwh zSlC2aIMMOS;yL&5HAfuFT~AW#s>zEkbJXWhNdroGsavK}S0p-8=+xmGcg5Jwog2QJOq-!{~;P)YypcFWIm@0 zuGnPqU(=mb&&8v+_gFejy00l6K)+q?;k+t{JVv$_CaFzJAm*qz>jUyFzU$9~+`P|f zq9jS`XZ7k4g8ytsGuvXO6|LFv!39u#?loT1aVA>2N7h2m$-6q*rMpfh5Y~lEGzoH+ zcEhR`fx0Px>g0N0F}cs*|~=K$w>{pSKX#SY6nvp%r6f$-aj% z+uM$6j~auASAShVx3EPwUYO4U3Kq2D#G#+Ug!)gjtesd?lqyoHh`2nlBIkc$%lu_= z<2Nx8RdYzl$=&S5ff;8!oRFS*6ySQ{^fK+abULnYlO_02_3i>`VJ_|DMoLf-kYwmA zK7(#>&bpT}G0r~0T|?kha<0ZJc$zFyP>XD8r6Y88Gm{-YK(!gOx0h=AgS(Mr%4;d? zIO4$!)00(tdZ2(ZYU=XsfxKmt7=jo1~TVp%`9N(yD_V-+*fi73h!%Egtx#{APXAYL>m*6Z_4)FqrObXJou14HC7L-=cc$rx<*xWSstm4K2cOtfBQz) z=LhL&WES%f!)5slUqrc7<3PgkeeVP!Bt6tQ&lK_ja>YD?SC@Q`NBa^%>PaZ23+|e8 z*{z+iDan!M!B=4MISPXRI4eVovr6~E3{mw1C#lX97@JFi^<}9KvKU znL|H_iD4TSD~C{uYv2aTX)#RWgH9VL|(LURA6j)bZM*?<(A zEUGOlmV;e+PoACHqx1gfE{dF^l*pE?%w4?mVN{jZ4+5gQGpXdP9Z-`l4~DK%)FyUW zLxTpcdn06YYP_Y||O(i$ zq8}zKxBVQ%@Z(!qNhIOmw%5=2?F)RM>(So&5Q(>fD&uvHY-Y$Bi$^_IGvW-?_rwn_ z=+kBSU&J_Wzyh%2pkrW+Xq?wov$fY-22$X;l`D6znf2AK!d07Uc;)p7d4AJ+Y#M&7 zUhHnUc!ivJ^5|vzU(5KXVJ}*0FoOly?_S+hM<5$R%)iJcPWSkF*>eo~tw2~_?l)Y0 z^^m!cSLvrTX5@LbK#uJ>9v6Z9))yNAdngUum!g1tcI$yO&n{;e72Y)@7Rk4iL zd3&ue-w4Bl3^81!@(Y>kTKQh+=Y$-(>v8#;jFF}ba4E2BU{#ZjZHPvEKkBtPvDDPtKV<~9TOcZ&J>!(MK$b;b|zp)jT`CwYJY4bblz4}T2GC+ri3rau1(gcUH5%^El3%mi1e5yTHe>Wa} zf~NOAX#D!ZW^}a%vMUnMZbDf4;~~*jG4+xalyZpY(_}CdIH)$QJaIDMIuUm`klTS( zidir?TK`J4Y38WB2xPoZwfSFrAgRm6qg&~R-?v^a{=`#7UBPz^Bm@eGFxfvILXQg} zUx)Dn`5qDn^F9SyxiDUsEm0`mFaDB$I+>87%swu(?XK~otNcqr8Hg7)yv}SpkR&x_ z2J=7A0A{7UCOBm_0U{79#V@Fjf{pvy+Qu#&g4%EuAAibYs|UVd>tt6GG$13U1o#Fw zZv!@spyIaEb{xHS9m3<};>^Z*fvH(7z%7 z-r9p0<`D-9725}$7YF~di}Ig+jQ`DLQ_4x}joJAK;R-PqFSmbvkZP?K@`g5NoqxCQ z-u&PwB==WG#KQ|8i4chB5^9$MJh*ssT%n1rnJ^TA!HAGgY^?ral5=~gK-fle%X<7X zH7vhjzDLvUNI^W~^ou-iv(8|$ZmF#}>4zg^swQ?F(aS=W{hVxZ)iC}Hb7!$a*ZJ(4pqr2zw#4gTTYSj#NP=P#Nj}MZvrE4F^o#@p z4ecz5Dnby($6WPZ&gq?MUPizEOI5<6c3qjAck$cNuRUaChsldVCD5y!wcqZd{8)Yq z)Sit`X^3v#c%v!q&O*PcsG&U`C){zw*B;I-KTY-rR4Gs%6VxX6Z{{t`K9HsH(Pyn{ zXD`+SW&qy3n7$Rg3_@c8H{f|EAY3R8q$b)K*fXOeDz`A%`$FE_<(dt{4BuV%TIQ9X zV+T<2_>*XUgPm1yhRuqhgCTAL&fh;G`Gg zz=%##CcVnKA6rdV%K9wkuYtJHF{i_nnUkIb0pAzbA2*vEbrhI65JjQNrqgk^L8O~; z&+l=RyDDQEg16=YQ<%yuhLcBL?xHC`bT2zqQY_#u?%2A6TURkcORZ^}N7V5Rfhj{R zh)MyZfK6h&x&zvHX#^ya;1{mFb!VILN66SaR#^?(^XbffX0`VUz&k_j&5 zb>o;0Qx&Qd)}r}_}QI@^WkSRdl8TTe+;zu4r<($9GDB! zsrTrgB&ENa}|EqUW*sozB@aRO=zSUb;VPx3gJY(ts+1Zq-o}9^&Y%dj}7= zO%1?I+u}LqAN0sC;}1qsY|s)cZMikqKk4ZZ1rD7DTgv$nLPWo?cA&z8ncj?>4I89g zaVxog=D5~C&g4Dl@AWy{*}G*m6=J>;ILB>bIM>gRjw9P)DtRLESu`8jJNoWU`v) zaf%f6^Us{9Ir!UgA=Fa&Q9L1Hwx8M`i5ynHm1Uw+sPDNye;+4N!R}OUCe>solBxP2 z5V;t}%iEbs{!``&Hj*Fx!|>e77SJUWZpY5oB_ZhwjFO%Q5j4oOL6yY(OuvSdSqaS) zOY_{DWwHRnNl_oSVvC1Go;ULeiXetF{X7y2%TpRXp%ocmA{hfBHL7 zJEJuB>O>vNBSOv=c;V}3KV}wrhN#vJh)RaSh?lZXC>$ssQ&@PM1{*CGi zxf~yB8)xER*RF;e9K0M+GjKAJgX+;h5yX-&v$$6w#}2$mKxal=`y|!I&F?gZpt}5( zQ|Mf%trY9^KG$+^REaUDSWBb&9-L%3St(4^?my&gr=vs0dIkKXRyek-2&4{QnPvy7 z-2``W*lS&aY5t{E=J$*lck&1=;39EQL<{h7)Tfc5EDlc}YIO z94<}NFR2RuRV)cVDZgAx^*AMHHZy-q-r5M|zeXPUtI;*t`454f+{Rcm12El0;f`ei zw*7vNzKc1|ofXsYZkfPv3KM0?9>ADhb`c=- z<(DCG0;DD3i>{=@+mhx7?VHze3_Ihl3+eeRX7JND+6(*_-joe{yG;{K-ItXgqZ}Ur zEfm=GjlzBUxlSt5A#+>r0aR1FW&IQ&pi6V~sG*7^c>qXF873bxZwemO6uS*XTK|TN zvC&F){Zh(uc3v9XgNlFBQawUZVW?(l*|AUyCv-k*=PQnF7@uu}mtKAQS3?5zlVUADn z&zvyFr|r$qC2b3gJ`(JKc|P;@Nv2|#g!mZLb;$vSnqw(}UI~L;Cr!J#B`~4Y=csN+ zWc8!Qif&-GnG}2z1N2)JZV(5QNH2#LwJl>wZnj*Fh@nRU2(Oskm;yl$rZmAhlaiUZ z%u>EmvOk{_Dtmp6k(c2!rOxOrJcC-&@#a^*L8^7Tr&b@RcuisNMztx`h@HQ)ZJ{K) z_jL<}OueD+RE7W9Km~L+pJ}%5jJQUroY5~ETW2wb_b!OqzS?OwZ@}iWSxMwBpKHcC zQSF(3y{#$<@3>3KA&DnMzb|kQXB$#;@mUZ$R%2l|eBNa4%~5$m#dY`TY=&!}H+dS_Q;EZds3Jgl^i_e4F9E&uyN9|Qrd<@u6CfO6q(8vL`VJub2fWXk&|cGS5YFV@uVdCbfB!jH;Q3# zIvP_G_Ui3^4hqCq?4q+O1+x-c@Wa+Zo7^*}D0#G_qiRrRKcq zaip%z6Pk&IP6nkEdWdvXe#|`F2l9*i#=4 za(zgX`nGNO@QB04?mo@jL#8;M%S-Ib^!5dIGM|6lQJ%BZFL;H$Nd8Rt(HwLxAx5mX zVIblxDZ&K%g)-;Ih`R};fYzn}a#h1PX4#}B`WpO{oOAgU)SM*xAr^z-2d8c}q7$zNdLCdD#TNqD=rGwZ_-+GKM0bwkPij)2gSA^MUw9JdR2%@q5>0f{i%h@Xa z_S&k~>{fC>9F|pzL4poR_={*82cx*w?O;t@Yi&&E+r?ip8xPhGXXPz+6?8y#9xBK$ z|3r?teG_X|q-LhA>9doa@U-HNdM>->D_%hfmzyx+I*vs)(S$KdoqhawgcS>=Uo#%e z?4^JXI|b)#u7scBQm0 zZ4G{SkJRu(r*Yl6OctO(lU_Yp)jRdU+3r4CYD6TTcP}w@rrS$phuN$GTO2Zez5O?z zq?98OkTM+$uOd=Kz9qn7&%sL}IP;{6y`&liwiCCzy^3Gk$9>Q<`I_#s_xh3NM3Sw!~L0KF~Gcg)^~sBUmE3 zzrMH}d|#WG_I}e9U>wT!-PrWALXW%SV*A_eXNvqOwfX=S>Q>F|tAzXlO+Gij(j}!d z8(ZOixDu0fpPJ3ve%y;lg#h=%bE0;YuQgq-9_$)VWkNy`th4#}#Ae<;{Dxywm<&!T z%|h~$gyW>g1UsOCTcs!gPHVdIq)KW9vegQ1c$L8xmvpHc>pz>l1OB@)$RuNq^ zky4GUT*?I9QSZ01uhEQ^7poHOhcOr?Q5%v8E~!H~DLBEj9__5{q&>JQZ^2I z9eAKFi^S*7H*VtKa-V4@p>6}!5otR@DcLEZ*UvaPIZI~VLi{kAUiRm%8X6X!@|NQ= zOq2+lz&^lp(UJaisIZV@7Lbg^!?kR^m%iTOtc~~hc%YNkwy1Mv3<$;N_rLu)kd==l zeCt%@Rb*{0JkDSV=_oEP!y7{hd*Lz~&>kcN{|QSNA0pyTuM%o4DT$V>&ZeY$=+Y;P zcycD?&h7OaNJi(;7xA1F{{qvl3CwaWP@#B3>|Y}w0sKV%nVIZcRwW=%8&jM$?Z?Mi zEeA~^8HRG+57jT@>6F@%3d_?dJUr$c+_0_U3!Q{$w3E9vXV=bAmFJdyzQ0}i23F&o z7^~2=)WMGtkO+@Nz7g3DR?W|&a^tlGY)E2JqIotCXr zHH*igN)dT*P875v@epeETt+lxy)7wt z41@-Za6E7C3b2`g4CPTjaZo=0g{<3Y8xJniF9=TKJgAh#xsIH(zrr4u zIYs8CFb*{BEIB8vokyV>+r59=unPyHaTX4R%L?Is;Y?{CSD=CflitX3EJp)}*aNjl zr`^yuYj1IK{sJ_P^>=t48SU=G_Hhv+q{%F{>rR#${Ayc+FXeR>EOzK{s%Cyr=en;k z8^$gbe+axgx<$wZYb%^5*GZ8|Iptg8a#NJ>g0_kVOq;3x3VMzk-|6&-(72VYkY`$q zO)v(lx#tDWIxdZ0PK%b*Xo>VUKjO5o9ro{KIC1CDe4D8x<-88McUE#DLJTMO@@>Wp zjc*ng_y0L-=}{r_G5M}K-p|;A#38e}a^T|pA2Qz!EQw}PSY&zUqhW8cdZu!|^K5I1 zKlg(DLW@m7>xi^j?)FEkJbLI(j+6e$9Nl+cTmB=jPL zUQ~)oLa)+ms8R$(5X2pQ*L6Pp)>`L#*Iw5;d!K)k*~S=ijycEk-1qN^+mwS0wNU<0 zj9^W*C0o$IU~x*`(JdF62;Z=E7BDq_cpZ)zfU+XI}>3f9l7q+?t@TF08| z`$p>pPi`-Cw<$U;g&2k4bMXhKJ0T(!06<)4^}@% zoR33&sa;P^_Da#WgcL&vqkc_6JD#LO$VbR`jX?4d)wwgv zA`{7AkA}@J%s&;Bp+k^>j~Cc!Hh~l{^Q#B3>_8O<6!petne7YN5%xXH#FFKl_9nl292cb+1^vi&YHWmlNkOWxNxod zT)z%JF7i`gr2S#%s?iQHJ@bs_T$}w#5K;3|YOY`s90e6__}nx!Nv~t7yB&;<0fKN6 zdR$Xq`*x9v&$dosb-}tj_dCbq_&;g%lvrySn*aks3g?I%z39#7B`Q|odq0@YHBUi* zkp|5&lxM)4uT=W5ZE!qTQT+n#+oB5(Gy}ADx1KBsadpD<2FtYtg=MbGIWhs8tHT!{ zHWqpJJeFXECyFP<5i@Td`YF^yL07!==1(5q@7AJ0ux_N}?8K{Zb8A|R6nTKJeEqI) zE=2uAqFeesVQ0qGi#Ex|7by83%HDNed217tBEvI7sKurw6Ds+G25h?+(%JA)?Xycb zr-WYZ@+l1clfVfNm=JY*Jbbp%W&nN;ROie#Xv6^%-_0(x`~3L)K>yWoSw88$$B~J&nj*MSJ;4OjXp$&2h{e;1&9gdj~hARLU9Vtgs6-Gv430)aiHA z{?+B2KmG@o$r~_MnSZg1XAh!4T(=?W$+|GtE$~JJlJzwk?=t#}SCs#x0q`m??CG4w z-#U=j-#a%Oz%dY!q*5!D!IMg&9%nLa->4rS379aDB@9YlIpzWoYU$@);$Zw_f;09a zFutSNlCkCo-^r8>WKUAURyuca6HP%ZmAkbhZ~h_F-k{7+opv+XB;#)lccznmBOIm?^M5#u28fw&atqEv>NQX_nnTq?s{jAaIgumC=e{~InQZa5q zxV@;H`Ui=H?4c;e)#JoU5pb!Pyui_6V&l>uJzAkXAUp9-DQ zAYUDd=Q!dOjqMK%LEnN5x^h});a*Oa50EV8n=|Vgf9FSX6)u((gP#PFg~?C=ceYUU{6Z``8L6KPo(WPy)LiGsZ`hRZJ5OGbS-9{JKtu#;(RDriTNn2C zoABST8x_kHD^X~4Q9u`5($7WZM!NdU5fa;QTBA+om8xs$49@JZ ztnLtmm*3ab)Zw;?hR>!NT)Xk`QGxE*zC6_86S-HGtGDb=qVCTd6 zF7Gh^U@f$k@%cZ|TPA(z8?d)jht4G7tY*=MTVa@< zi=T|bJNcFVW}8#v$3Zq)vy_WusDz+I1yM;^sYQr0U4_H@-P~?I&0U_)@0%v!aHxPb zS}2cfbbr&`23;=>s+WTA4A!)ZVi?(=&}&Kj&0?6Opam2K-WVC%5ZoAboS)mlj#?|0 zd$RY=-_PCI=h4{vuW%O?*OYgCUhj#~@Me4x68Ac%B)jnP*{ep>gSbM5dl!NyzQL$Z z!`3{MkjTaoyGZYO4whty_f|KR6uM!uYq^{|H};Ltez_mUa~RL3>Q{hr&J4wugzXxM zcHI1N_jIPr*e`pNEMRft%j~oC(U(o!D=4eMnh-6qd70?tFw=L+RaCYkhypEJt2{qa zP$eQAI^n-!ejEEt(3qJ@w!=fS4Jq+D5LtGAkw3wo)YK6@<; zc&(NE74PiP+2}X9K>6y4feUKmUJ@Js?R^&s6eQ|~u?p8%X}^{L+NBxOoS}E?{o0S4 z=_%Ej;0jDEM?(gEsetUoH`pjB-OLcDbs%&*OE#-9^43l5v}C0+jNIl;l%!MT)woai zY$H8gJU-h<$NDSV$g@~@+7{X0h@t3pjo+!mQRM$EWLG;x; ziRX$>E+ygE!H)aY8)+$y3kLJl(NAl{ai=7*JRurQ*DXxfE$YMh8NzbO2AhRgd91)C zSKLnfEx`~l&r1@ybS#GX!Ssv;Uy?Ydkm0YN(%9L#pazTwm;VVoZg`3 z(-2{-)Q^J8w&p3F18I+K1h|r=_+R>E^YvTnMf&v%YP#2hX)=-lVZ)!t8J@TsfP1lY zGvHKkRq14{XW(PUe`o;rGJdB9<2f&`y~=!3UC1;5%*)^kB+7JEnX1aBq!doDC)_lxp9Zgqq* zvVbHy)i=FOT%c9Xd4QR{rz#$-&n_={X|vm;R?rvv5~IFKQhxbxPxOfKw)8{wL>lU7 zYXiA(Jw?&58Xm{VxKXG|)@7oI&GNhagoKUFVq?jkG4HNuBQIup8F5kEU9Q$Uu0RXD zoZqr@M^riuKpI7*jQI_iEu2->nFj-V+uHBqwJk%YX|v}LXPN`OUAY!@0FLn zVv+79^&aHI^DrNb_kQgldI*3VN?sl>>!`_0^|lPC_0bB^m;4Aja<2=^lLX|eiHd#Y z!5$Uu#OeO%3;xS4kzTVZ;T_k~%I-LQi_6TDY_#kp*=kie)ZCO_CQ}*ShBAcILuO4w zpWQtD(=$Ll6_w?XJdzK6A2j)-s71cS$dLD3(!|YARcx<948!E*f^@zZpQbG~T%n^- zK>-6cA#b==O{F};*wj;wRNh6Be}1d#Upg#zosrM$19h&kWl@IdXY4f~LloX9zi;Ji zb+bw9-Yv1#CzmVwWRDXqs=gMQ8?4QS5s z72I>T-IF~Y?h2|dm87U}Es6JV{XKkk;&lsljY5xy4R>8d zAj%Ud$xYnZeSwRG)>4urghe-Cu{BIB>cr#V?j(#8f#IqZ)aGrmn9cs~Df84U%S{j> z38bepB~lqyJ$xnbr<;TjpZDjVkQIA!)hd~I_!X(R;XYZaZJuuCXL?%cgItxxp$MNp6UfX6=QXpYxO9~6v844Wm@VVZR z674ia11qbu^rZ7Bw#a4CLVe8iuOSwx3Pa7ee07Z`@$RsaRDY!ETZ(92GCK_XlIC@k?z&=`&1B_h$Jd%Ask?ikHtnxR<}f!-_Qpi;3gN+7b+U^ir-l zdI8ijWvDj>S}OQ?3_*H=d};4sC7XLmf-};_p#ynnZ?*Ijy_1q~t)B$XbJE=plWAbH zgERD!#Vm~us%Cr%=`DNu40R@n$%S*hW(g@=nV<*0Y<1eOOA

32?)=}d2f9&M(U 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 E1DybE2LJ#7 literal 0 HcmV?d00001 diff --git a/mkdocs.yml b/mkdocs.yml index 196dc4bd..87c55bdb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,9 @@ theme: primary: 'light blue' # The primary color palette for the theme features: - navigation.tabs # Enable navigation tabs feature for the theme + markdown_extensions: + - md_in_html + - admonition plugins: - search diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index a8feb60a..56190db4 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,5 +1,5 @@ """GLM core module.""" -from typing import Any, Literal, Optional, Tuple, Type, Union +from typing import Any, Literal, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -29,8 +29,6 @@ class GLM(BaseRegressor): solver : Solver Solver to use for model optimization. Defines the optimization algorithm and related parameters. Default is Ridge regression with gradient descent. - **kwargs : Any - Additional keyword arguments. Attributes ---------- @@ -42,8 +40,6 @@ class GLM(BaseRegressor): Model baseline link firing rate parameters after fitting. basis_coeff_ : jnp.ndarray or None Basis coefficients for the model after fitting. - scale : float - Scale parameter for the noise model. It's 1.0 for Poisson and Gaussian. solver_state : Any State of the solver after fitting. May include details like optimization error. @@ -65,7 +61,6 @@ def __init__( self, noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), solver: slv.Solver = slv.RidgeSolver("GradientDescent"), - **kwargs: Any, ): super().__init__() @@ -84,14 +79,12 @@ def __init__( self.noise_model = noise_model self.solver = solver + # initialize to None fit output self.baseline_link_fr_ = None self.basis_coeff_ = None - # scale parameter (=1 for poisson and Gaussian, needs to be estimated for Gamma) - # the estimate of scale does not affect the ML estimate of the other parameter - self.scale = 1.0 self.solver_state = None - def _check_is_fit(self): # check scale. + def _check_is_fit(self): """Ensure the instance has been fitted.""" if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): raise NotFittedError( @@ -430,21 +423,19 @@ class GLMRecurrent(GLM): The numerical data type for internal calculations. If not provided, it will be inferred from the data during fitting. - Attributes - ---------- - - The attributes of `GLMRecurrent` are inherited from the parent `GLM` class, and might include - coefficients, fitted status, and other model-related attributes. - See Also -------- [GLM](../glm/#neurostatslib.glm.GLM) : Base class for the generalized linear model. Notes ----- - The recurrent GLM assumes that neural activity can be influenced by both feedforward + - The recurrent GLM assumes that neural activity can be influenced by both feedforward inputs and the past activity of the same and other neurons. This makes it particularly powerful for capturing the dynamics of neural networks where neurons are interconnected. + - The attributes of `GLMRecurrent` are inherited from the parent `GLM` class, and might include + coefficients, fitted status, and other model-related attributes. + Examples -------- >>> # Initialize the recurrent GLM with default parameters @@ -457,7 +448,6 @@ def __init__( self, noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), solver: slv.Solver = slv.RidgeSolver(), - data_type: Optional[Union[Type[jnp.float32], Type[jnp.float64]]] = None, ): super().__init__(noise_model=noise_model, solver=solver) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 92e06945..e16093ae 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -45,7 +45,7 @@ def __init__(self, inverse_link_function: Callable, **kwargs): super().__init__(**kwargs) self._check_inverse_link_function(inverse_link_function) self._inverse_link_function = inverse_link_function - self._scale = None + self._scale = 1. @property def inverse_link_function(self): @@ -58,6 +58,18 @@ def inverse_link_function(self, inverse_link_function: Callable): self._check_inverse_link_function(inverse_link_function) self._inverse_link_function = inverse_link_function + @property + def scale(self): + """Getter for the scale parameter of the model.""" + return self._scale + + @scale.setter + def scale(self, value: Union[int, float]): + """Setter for the scale parameter of the model.""" + if not isinstance(value, (int, float)): + raise ValueError("The `scale` parameter must be of numeric type.") + self._scale = value + @staticmethod def _check_inverse_link_function(inverse_link_function): if not callable(inverse_link_function): @@ -73,7 +85,7 @@ def _check_inverse_link_function(inverse_link_function): ) @abc.abstractmethod - def negative_log_likelihood(self, firing_rate, y): + def negative_log_likelihood(self, predicted_rate, y): r"""Compute the noise model negative log-likelihood. This computes the negative log-likelihood of the predicted rates @@ -136,6 +148,11 @@ def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarr """ pass + @abc.abstractmethod + def estimate_scale(self, predicted_rate: jnp.ndarray) -> float: + """Estimate the scale parameter for the model.""" + pass + def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): r"""Pseudo-R^2 calculation for a GLM. @@ -297,3 +314,8 @@ def residual_deviance( spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) ) return resid_dev + + def estimate_scale(self, predicted_rate: jnp.ndarray): + """Assign 1 to the scale parameter of the Poisson model.""" + self.scale = 1. + From 23e0557b359df295761cbe20f02c91687a3427bc Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 21 Sep 2023 09:27:58 -0400 Subject: [PATCH 085/250] edited note 03-glm.md --- docs/developers_notes/03-glm.md | 3 ++- docs/developers_notes/GLM_scheme.jpg | Bin 137103 -> 137389 bytes 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index f814f02e..2290eabb 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -15,7 +15,8 @@ Our design is harmonized with the `scikit-learn` API, facilitating seamless inte The classes provided here are modular by design offering a standard foundation for any GLM variant. -Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. +Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the `neurostatslib.noise_model.NoiseModel` and `neurostatslib.solver.Solver` objects, respectively. + ![Title](GLM_scheme.jpg){ width="512" }
diff --git a/docs/developers_notes/GLM_scheme.jpg b/docs/developers_notes/GLM_scheme.jpg index 9a7f630092c23ae0bacebcc4f81ac4e0cf61c15c..7fea537473e6adf6e5653c1f737b30095ab27eae 100644 GIT binary patch delta 114146 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 delta 113823 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�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�qeQq89i1Qi_P)Nt6=SD*?=`|5tFviGxQgU;em$uhg$700xMR9w%SJM0v zM59DUjdT=#93c9&3j6YxXiV{2&THJ(ou-rCv6jWq4JK9!@_~bXXp`=QRFh6|fyBU~ zA)_1$+aBtQ4Gn65*c4tO;smKf7d_mIOq*`?3TF-smTV@3hYjwd#$btI4@s#{+?>Gb zdJ5afLIs633ZcI0%a2O0s=Hq&Z=I<$<35biVEwh5n^ZGcK71|&UY6TcEVtg2RoRk3 z$Nbf9QChrKZk_*FvQ-avp8-OoOf|>g5_Gaxzb&w6w_dmgYs8;X<-f)8RW-+q$~I$1 zpO(D{(Lteki)@aj=r%Vp+)v950j_1F2zF{(;pNEy9Dku>BTb~1qhYb}jJzUO9rYDp zZ8?19Kp>enLkd%BR0AuMS#fST0DQtt$*R7ml$KbD_toZ%meR!q@6YFGVx{k zz+QktiBpi<)3GJdZ(BS0DgD_o8?KOpa)Ok{UOFd+l;y|!p}VkSE*u=J!>+mRlQ6E`wC(LLS6w*yjH`;wUCYg)LLQ;I}tzii1Iy{Huk zBN;vE2Hzaoig;c)3}!g`)ZVUEnjm`JovdVJr{uhrRS@YHvbD}@ zYST$>B2lM#YfQtwRHmEwiprx1S)eL}SqaI>?_&dc9K*Z;W*#Vi)&_!bO)K@nrYrud zHFu!?(B#6*9Fr)r1ogP-!G!b7=D(1J%73I(EO;wB_Z`s`@yS(|cmhAhp#2ZKCfQaR z3Z$`$D9FFKln29dxvz+}2L*$sYg)lH%di@$A66Q$=gzuUq%TUAv1Q%w)PML(a+UMT z#s&g*Imv>Ohl`A-Y@_qPBB9z5#xm>zU@BILKD?YOIQ#keC%3tdu+8? Date: Thu, 21 Sep 2023 13:48:39 -0400 Subject: [PATCH 086/250] bugfixed plots --- docs/developers_notes/03-glm.md | 2 +- docs/examples/plot_glm_demo.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index 2290eabb..0f7cb3a9 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -18,8 +18,8 @@ The classes provided here are modular by design offering a standard foundation f Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the `neurostatslib.noise_model.NoiseModel` and `neurostatslib.solver.Solver` objects, respectively. -![Title](GLM_scheme.jpg){ width="512" }

+
Schematic of the module interactions.
diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 237d5328..e5a1463d 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -94,7 +94,7 @@ # Observation noise solver = nsl.solver.RidgeSolver( solver_name="LBFGS", - alpha=0.1, + regularizer_strength=0.1, solver_kwargs={"tol":10**-10} ) @@ -102,7 +102,6 @@ model = nsl.glm.GLM( noise_model=noise_model, solver=solver, - data_type=jax.numpy.float64 ) print("Solver type: ", type(model.solver)) @@ -137,7 +136,7 @@ # Fit a ridge regression Poisson GLM model = nsl.glm.GLM() -model.set_params(solver__alpha=0.1) +model.set_params(solver__regularizer_strength=0.1) model.fit(X, spikes) print("Ridge results") @@ -154,7 +153,7 @@ # Here is an example of how we can perform 5-fold cross-validation via `scikit-learn`. # **Ridge** -parameter_grid = {"solver__alpha": np.logspace(-1.5, 1.5, 6)} +parameter_grid = {"solver__regularizer_strength": np.logspace(-1.5, 1.5, 6)} cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) From 5f8b78a2b3ec455330b89a1e4717698767d1f9b7 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 21 Sep 2023 17:33:45 -0400 Subject: [PATCH 087/250] first pass to the noise model dev note --- docs/developers_notes/04-noise_model.md | 40 +++++++++++++++++++++++++ mkdocs.yml | 6 ++++ 2 files changed, 46 insertions(+) create mode 100644 docs/developers_notes/04-noise_model.md diff --git a/docs/developers_notes/04-noise_model.md b/docs/developers_notes/04-noise_model.md new file mode 100644 index 00000000..b129c614 --- /dev/null +++ b/docs/developers_notes/04-noise_model.md @@ -0,0 +1,40 @@ +# The `noise_model` Module + +## Introduction + +The `noise_model` module provides objects representing the observation noise of GLM-like models. + +The abstract class `NoiseModel` defines the structure of the subclasses which specify observation noise type, e.g. Poisson, Gamma, etc. + +These objects are attributes of the `neurostatslib.glm.GLM` class, equipping the GLM with a negative log-likelihood, used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. + + +## The Abstract class `NoiseModel` + +The abstract class `NoiseModel` is the backbone of any noise model. Any class inherting `NoiseModel` must reimplement the `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `get_scale` methods. + +### Abstract Methods + +For subclasses derived from `NoiseModel` to function correctly, they must implement the following: + +- **negative_log_likelihood**: The negative-loglikelihood of the model. This is usually part of the objective function used to learn GLM parameters. +- **emission_probability**: The random emission probability function. Usually calls `jax.random` emission probability with some provided some sufficient statistics. For distributions in the exponential family, sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is fully specified by the weights of the model, while the scale is either fixed (i.e. Poisson) or needs to be estimated (i.e. Gamma). +- **residual_deviance**: Compute the residual deviance given the current model estimated rates and observations. +- **estimate_scale**: Method for estimating the scale parameter of the model. This is required when generating simulated activity in the proper scale. + +### Public Methods + +- **pseudo_r2**: Method for computing the pseudo-$R^2$ of the model based on the residual deviance. There is no consensus definition for the pseudo-$R^2$, what we used here is the definition by Choen at al. 2003[^1]. + + +### Auxiliary Methods + +- **_check_inverse_link_function**: Check that the provided link function is a `Callable` of the `jax` namespace. + +## Contributor Guidelines + + +[^1]: + Jacob Cohen, Patricia Cohen, Steven G. West, Leona S. Aiken. + *Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences*. + 3rd edition. Routledge, 2002. p.502. ISBN 978-0-8058-2223-6. (May 2012) diff --git a/mkdocs.yml b/mkdocs.yml index 87c55bdb..c18782ea 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,6 +11,12 @@ theme: - md_in_html - admonition +# if footnotes is degined in theme doesn't work +# If md_in_html is defined outside theme, it also results in +# an error when building the docs. +markdown_extensions: + - footnotes + plugins: - search - gallery: From b30b48f99046264cafc3fc80f1c2bf73c5c7407f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 21 Sep 2023 17:43:26 -0400 Subject: [PATCH 088/250] completed note 04-noise_model.md --- docs/developers_notes/04-noise_model.md | 44 ++++++++++++++++++------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/developers_notes/04-noise_model.md b/docs/developers_notes/04-noise_model.md index b129c614..bb88d200 100644 --- a/docs/developers_notes/04-noise_model.md +++ b/docs/developers_notes/04-noise_model.md @@ -2,25 +2,25 @@ ## Introduction -The `noise_model` module provides objects representing the observation noise of GLM-like models. - -The abstract class `NoiseModel` defines the structure of the subclasses which specify observation noise type, e.g. Poisson, Gamma, etc. - -These objects are attributes of the `neurostatslib.glm.GLM` class, equipping the GLM with a negative log-likelihood, used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. +The `noise_model` module provides objects representing the observation noise of GLM-like models. +The abstract class `NoiseModel` defines the structure of the subclasses which specify observation noise types, such as Poisson, Gamma, etc. These objects serve as attributes of the `neurostatslib.glm.GLM` class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. ## The Abstract class `NoiseModel` -The abstract class `NoiseModel` is the backbone of any noise model. Any class inherting `NoiseModel` must reimplement the `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `get_scale` methods. +The abstract class `NoiseModel` is the backbone of any noise model. Any class inheriting `NoiseModel` must reimplement the `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `estimate_scale` methods. ### Abstract Methods For subclasses derived from `NoiseModel` to function correctly, they must implement the following: -- **negative_log_likelihood**: The negative-loglikelihood of the model. This is usually part of the objective function used to learn GLM parameters. -- **emission_probability**: The random emission probability function. Usually calls `jax.random` emission probability with some provided some sufficient statistics. For distributions in the exponential family, sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is fully specified by the weights of the model, while the scale is either fixed (i.e. Poisson) or needs to be estimated (i.e. Gamma). -- **residual_deviance**: Compute the residual deviance given the current model estimated rates and observations. -- **estimate_scale**: Method for estimating the scale parameter of the model. This is required when generating simulated activity in the proper scale. +- **negative_log_likelihood**: Computes the negative-log likelihood of the model up to a normalization constant. This method is usually part of the objective function used to learn GLM parameters. + +- **emission_probability**: Returns the random emission probability function. This typically invokes `jax.random` emission probability, provided some sufficient statistics. For distributions in the exponential family, the sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is entirely specified by the model's weights, while the scale is either fixed (i.e., Poisson) or needs to be estimated (i.e., Gamma). + +- **residual_deviance**: Computes the residual deviance based on the model's estimated rates and observations. + +- **estimate_scale**: A method for estimating the scale parameter of the model. ### Public Methods @@ -31,7 +31,29 @@ For subclasses derived from `NoiseModel` to function correctly, they must implem - **_check_inverse_link_function**: Check that the provided link function is a `Callable` of the `jax` namespace. -## Contributor Guidelines +## Concrete `PoissonNoiseModel` class + +The `PoissonNoiseModel` class extends the abstract `NoiseModel` class to provide functionalities specific to the Poisson noise model. It is designed for modeling observed spike counts based on a Poisson distribution with a given rate. + +### Overridden Methods + +- **negative_log_likelihood**: This method computes the Poisson negative log-likelihood of the predicted rates for the observed spike counts. + +- **emission_probability**: Generates random numbers from a Poisson distribution based on the given `predicted_rate`. + +- **residual_deviance**: Calculates the residual deviance for a Poisson model. + +- **estimate_scale**: Assigns a fixed value of 1 to the scale parameter of the Poisson model since Poisson distribution has a fixed scale. + +## Contributor Guidelines + +To implement a noise model class you + +- **Must** inherit from `NoiseModel` + +- **Must** provide a concrete implementation of `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `estimate_scale`. + +- **Should not** reimplement the `pseudo_r2` method as well as the `_check_inverse_link_function` [^1]: From ad99760fd69ad6ec668bfa59c58e2a68de62b4fc Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 10:32:37 -0400 Subject: [PATCH 089/250] started new note on solver --- docs/developers_notes/04-noise_model.md | 2 +- docs/developers_notes/05-solver.md | 38 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 docs/developers_notes/05-solver.md diff --git a/docs/developers_notes/04-noise_model.md b/docs/developers_notes/04-noise_model.md index bb88d200..933835fa 100644 --- a/docs/developers_notes/04-noise_model.md +++ b/docs/developers_notes/04-noise_model.md @@ -53,7 +53,7 @@ To implement a noise model class you - **Must** provide a concrete implementation of `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `estimate_scale`. -- **Should not** reimplement the `pseudo_r2` method as well as the `_check_inverse_link_function` +- **Should not** reimplement the `pseudo_r2` method as well as the `_check_inverse_link_function` auxiliary method. [^1]: diff --git a/docs/developers_notes/05-solver.md b/docs/developers_notes/05-solver.md new file mode 100644 index 00000000..c6253f83 --- /dev/null +++ b/docs/developers_notes/05-solver.md @@ -0,0 +1,38 @@ +# The `sovler` Module + +## Indtroduction + +The `solver` module introduces an archetype class `Sover` which provides the structural components of each concrete sub-classes. + +Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. + +Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e. have a `run` and `update` method with the appropriate input/output types). + +Each solver object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). + +!!! note + If we will need advanced adaptive optimizers (e.g. Adam, LAMB etc.) in the future, we should consider adding [`Optax`](https://optax.readthedocs.io/en/latest/) as a dependency, which is compatible with `jaxopt`, see [here](`https://jaxopt.github.io/stable/_autosummary/jaxopt.OptaxSolver.html#jaxopt.OptaxSolver`). + +## The Abstract Class `Solver` + +The abstract class `Solver` enforces the implementation of the `instantiate_solver` method on any concrete realization of a `Solver` object. `Solver` object are equipped with a method for instantiating a solver runner, i.e. a function that receives as input the initial parameters, the endogenous and the exogenous variable and outputs the optimization results. + +Additionally, the class provides auxiliary methods for checking that the solver and loss function specifications are valid. + +### Abstract Methods + +`Solver` objects defines the following abstract method: + +- **`instantiate_solver`**: Instantiate a solver runner for a provided a loss function. The loss function must be a `Callable` from either the `jax` or the `neurostatslib` namespace. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. + +### Public Methods + +- **`get_runner`**: Configure and return a `solver_run` callable. The method accepts two dictionary arguments, `solver_kwargs` and `run_kwargs`, which are meant to hold additional keyword arguments for the instantiation and execution of the solver, respectively. These keyword arguments are prepared by the concrete implementation of `instantiate_solver`, which is solver-type specific. + +### Auxiliary methods + +- **_check_solver**: + +- **_check_solver_kwargs**: + +- **_check_is_callable_from_jax**: \ No newline at end of file From 3211e8ae4a26839dbebf07fb199653ad348f737d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 10:39:11 -0400 Subject: [PATCH 090/250] minor edits and grammar checks --- docs/developers_notes/05-solver.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/developers_notes/05-solver.md b/docs/developers_notes/05-solver.md index c6253f83..0713533f 100644 --- a/docs/developers_notes/05-solver.md +++ b/docs/developers_notes/05-solver.md @@ -1,38 +1,38 @@ -# The `sovler` Module +# The `solver` Module -## Indtroduction +## Introduction -The `solver` module introduces an archetype class `Sover` which provides the structural components of each concrete sub-classes. +The `solver` module introduces an archetype class `Solver` which provides the structural components for each concrete sub-class. -Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. +Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. -Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e. have a `run` and `update` method with the appropriate input/output types). +Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). Each solver object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). !!! note - If we will need advanced adaptive optimizers (e.g. Adam, LAMB etc.) in the future, we should consider adding [`Optax`](https://optax.readthedocs.io/en/latest/) as a dependency, which is compatible with `jaxopt`, see [here](`https://jaxopt.github.io/stable/_autosummary/jaxopt.OptaxSolver.html#jaxopt.OptaxSolver`). + If we need advanced adaptive optimizers (e.g., Adam, LAMB etc.) in the future, we should consider adding [`Optax`](https://optax.readthedocs.io/en/latest/) as a dependency, which is compatible with `jaxopt`, see [here](https://jaxopt.github.io/stable/_autosummary/jaxopt.OptaxSolver.html#jaxopt.OptaxSolver). ## The Abstract Class `Solver` -The abstract class `Solver` enforces the implementation of the `instantiate_solver` method on any concrete realization of a `Solver` object. `Solver` object are equipped with a method for instantiating a solver runner, i.e. a function that receives as input the initial parameters, the endogenous and the exogenous variable and outputs the optimization results. +The abstract class `Solver` enforces the implementation of the `instantiate_solver` method on any concrete realization of a `Solver` object. `Solver` objects are equipped with a method for instantiating a solver runner, i.e., a function that receives as input the initial parameters, the endogenous and the exogenous variables, and outputs the optimization results. Additionally, the class provides auxiliary methods for checking that the solver and loss function specifications are valid. ### Abstract Methods -`Solver` objects defines the following abstract method: +`Solver` objects define the following abstract method: -- **`instantiate_solver`**: Instantiate a solver runner for a provided a loss function. The loss function must be a `Callable` from either the `jax` or the `neurostatslib` namespace. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. +- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function. The loss function must be a `Callable` from either the `jax` or the `neurostatslib` namespace. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. ### Public Methods -- **`get_runner`**: Configure and return a `solver_run` callable. The method accepts two dictionary arguments, `solver_kwargs` and `run_kwargs`, which are meant to hold additional keyword arguments for the instantiation and execution of the solver, respectively. These keyword arguments are prepared by the concrete implementation of `instantiate_solver`, which is solver-type specific. +- **`get_runner`**: Configure and return a `solver_run` callable. The method accepts two dictionary arguments, `solver_kwargs` and `run_kwargs`, which are meant to hold additional keyword arguments for the instantiation and execution of the solver, respectively. These keyword arguments are prepared by the concrete implementation of `instantiate_solver`, which is solver-type specific. -### Auxiliary methods +### Auxiliary Methods -- **_check_solver**: +- **`_check_solver`**: This method ensures that the provided solver name is in the list of allowed optimizers for the specific `Solver` object. This is crucial for maintaining consistency and correctness in the solver's operation. -- **_check_solver_kwargs**: +- **`_check_solver_kwargs`**: This method checks if the provided keyword arguments are valid for the specified solver. This helps in catching and preventing potential errors in solver configuration. -- **_check_is_callable_from_jax**: \ No newline at end of file +- **`_check_is_callable_from_jax`**: This method checks if the provided function is callable and whether it belongs to the `jax` or `neurostatslib` namespace, ensuring compatibility and safety when using jax-based operations. From 8e39d5e68bf1230b6e01af07c09ebaf911c3cd4d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 11:20:37 -0400 Subject: [PATCH 091/250] added a glossary --- docs/developers_notes/05-solver.md | 10 ++++++++++ mkdocs.yml | 1 + 2 files changed, 11 insertions(+) diff --git a/docs/developers_notes/05-solver.md b/docs/developers_notes/05-solver.md index 0713533f..a8e14220 100644 --- a/docs/developers_notes/05-solver.md +++ b/docs/developers_notes/05-solver.md @@ -7,6 +7,7 @@ The `solver` module introduces an archetype class `Solver` which provides the st Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). +We choose `jaxopt` as dependency because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX. Each solver object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). @@ -36,3 +37,12 @@ Additionally, the class provides auxiliary methods for checking that the solver - **`_check_solver_kwargs`**: This method checks if the provided keyword arguments are valid for the specified solver. This helps in catching and preventing potential errors in solver configuration. - **`_check_is_callable_from_jax`**: This method checks if the provided function is callable and whether it belongs to the `jax` or `neurostatslib` namespace, ensuring compatibility and safety when using jax-based operations. + +## Glossary + +| Term | Description | +|--------------------| ----------- | +| **Regularization** | Regularization is a technique used to prevent overfitting by adding a penalty to the loss function, which discourages complex models. Common regularization techniques include L1 (Lasso) and L2 (Ridge) regularization. | +| **Optimization** | Optimization refers to the process of minimizing (or maximizing) a function by systematically choosing the values of the variables within an allowable set. In machine learning, optimization aims to minimize the loss function to train models. | +| **Solver** | A solver is an algorithm or a set of algorithms used for solving optimization problems. In the given module, solvers are used to find the parameters that minimize the loss function, potentially subject to some constraints. | +| **Runner** | A runner in this context refers to a callable function configured to execute the solver with the specified parameters and data. It acts as an interface to the solver, simplifying the process of running optimization tasks. | diff --git a/mkdocs.yml b/mkdocs.yml index c18782ea..41c9c208 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,7 @@ theme: markdown_extensions: - md_in_html - admonition + - tables # if footnotes is degined in theme doesn't work # If md_in_html is defined outside theme, it also results in From 2b2962d0ba18b7417380542efeff46224c295487 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 11:33:27 -0400 Subject: [PATCH 092/250] edited note --- docs/developers_notes/05-solver.md | 100 +++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/docs/developers_notes/05-solver.md b/docs/developers_notes/05-solver.md index a8e14220..3d7bb910 100644 --- a/docs/developers_notes/05-solver.md +++ b/docs/developers_notes/05-solver.md @@ -38,6 +38,106 @@ Additionally, the class provides auxiliary methods for checking that the solver - **`_check_is_callable_from_jax`**: This method checks if the provided function is callable and whether it belongs to the `jax` or `neurostatslib` namespace, ensuring compatibility and safety when using jax-based operations. +## The `UnRegularizedSolver` Class + +The `UnRegularizedSolver` class extends the base `Solver` class and is designed specifically for optimizing unregularized models. This means that this solver class does not add any regularization penalty to the loss function during the optimization process. + +### Attributes + +- **`allowed_optimizers`**: A list of string identifiers for the optimization algorithms that can be used with this solver class. The optimization methods listed here are specifically suitable for unregularized optimization problems. + +### Methods + +- **`__init__`**: The constructor method for this class which initializes a new `UnRegularizedSolver` object. It accepts the name of the solver algorithm to use (`solver_name`) and an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver. + +- **`instantiate_solver`**: A method which prepares and returns a runner function for the specified loss function. This method ensures that the loss function is callable and prepares the necessary keyword arguments for calling the `get_runner` method from the base `Solver` class. + +### Example Usage + +```python +unreg_solver = UnRegularizedSolver(solver_name="GradientDescent") +runner = unreg_solver.instantiate_solver(loss_function) +optim_results = runner(init_params, exog_vars, endog_vars) +``` + +## The `RidgeSolver` Class + +The `RidgeSolver` class extends the `Solver` class to handle optimization problems with Ridge regularization. Ridge regularization adds a penalty to the loss function, proportional to the sum of squares of the model parameters, to prevent overfitting and stabilize the optimization. + +### Attributes + +- **`allowed_optimizers`**: A list containing string identifiers of optimization algorithms compatible with Ridge regularization. + +- **`regularizer_strength`**: A floating-point value determining the strength of the Ridge regularization. Higher values correspond to stronger regularization which tends to drive the model parameters towards zero. + +### Methods + +- **`__init__`**: The constructor method for the `RidgeSolver` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, and the regularization strength (`regularizer_strength`). + +- **`penalization`**: A method to compute the Ridge regularization penalty for a given set of model parameters. + +- **`instantiate_solver`**: A method that prepares and returns a runner function with a penalized loss function for Ridge regularization. This method modifies the original loss function to include the Ridge penalty, ensures the loss function is callable, and prepares the necessary keyword arguments for calling the `get_runner` method from the base `Solver` class. + +### Example Usage + +```python +ridge_solver = RidgeSolver(solver_name="LBFGS", regularizer_strength=1.0) +runner = ridge_solver.instantiate_solver(loss_function) +optim_results = runner(init_params, exog_vars, endog_vars) +``` + +## `ProxGradientSolver` Class + +`ProxGradientSolver` class extends the `Solver` class to utilize the Proximal Gradient method for optimization. It leverages the `jaxopt` library's Proximal Gradient optimizer, introducing the functionality of a proximal operator. + +### Attributes: +- **`allowed_optimizers`**: A list containing string identifiers of optimization algorithms compatible with this solver, specifically the "ProximalGradient". + +- **`mask`**: An optional mask array for element-wise operations with shape (n_groups, n_features). + +### Methods: +- **`__init__`**: The constructor method for the `ProxGradientSolver` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, the regularization strength (`regularizer_strength`), and an optional mask array (`mask`). + +- **`mask`**: Property method to get or set the mask array. + +- **`_check_mask`**: Static method to validate the mask array adhering to specific requirements. + +- **`get_prox_operator`**: Abstract method to retrieve the proximal operator for this solver. + +- **`instantiate_solver`**: Method to prepare and return a runner function for optimization with a provided loss function and proximal operator. + +## `LassoSolver` Class + +`LassoSolver` class extends `ProxGradientSolver` to specialize in optimization using the Lasso (L1 regularization) method with Proximal Gradient. + +### Methods: +- **`__init__`**: Constructor method similar to `ProxGradientSolver` but defaults `solver_name` to "ProximalGradient". + +- **`get_prox_operator`**: Method to retrieve the proximal operator for Lasso regularization (L1 penalty). + +## `GroupLassoSolver` Class + +`GroupLassoSolver` class extends `ProxGradientSolver` to specialize in optimization using the Group Lasso regularization method with Proximal Gradient. It induces sparsity on groups of features rather than individual features. + +### Attributes: +- **`mask`**: A mask array indicating groups of features for regularization. + +### Methods: +- **`__init__`**: Constructor method similar to `ProxGradientSolver`, but additionally requires a `mask` array to identify groups of features. + +- **`get_prox_operator`**: Method to retrieve the proximal operator for Group Lasso regularization. + +### Example Usage +```python +lasso_solver = LassoSolver(regularizer_strength=1.0) +runner = lasso_solver.instantiate_solver(loss_function) +optim_results = runner(init_params, exog_vars, endog_vars) + +group_lasso_solver = GroupLassoSolver(solver_name="ProximalGradient", mask=group_mask, regularizer_strength=1.0) +runner = group_lasso_solver.instantiate_solver(loss_function) +optim_results = runner(init_params, exog_vars, endog_vars) +``` + ## Glossary | Term | Description | From 0aec19d8693e06331c577b4905fb25d0f7c7db17 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 12:34:13 -0400 Subject: [PATCH 093/250] edited notes, added hyeperlinks --- docs/developers_notes/02-base_class.md | 6 ++--- docs/developers_notes/03-glm.md | 12 ++++----- docs/developers_notes/04-noise_model.md | 12 +++++---- docs/developers_notes/05-solver.md | 34 +++++++++++++++++++++++-- 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 00ae52cc..bf4ab7c9 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -2,9 +2,9 @@ ## Introduction -The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. Currently, the sole abstract class available is `BaseRegressor`. +The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. -The `_Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, noise models, solvers etc.). In contrast, abstract classes derived from `_Base` define overarching object categories (e.g., `BaseRegressor` is building block for GLMs, GAMS, etc. while `NoiseModel` is the building block for the Poisson noise, Gamma noise, ... etc.). +The `_Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, noise models, solvers etc.). In contrast, abstract classes derived from `_Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `noise_model.NoiseModel` is the building block for the Poisson noise, Gamma noise, ... etc.). Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. @@ -47,7 +47,7 @@ Class _Base ## The Class `model_base._Base` -The `_Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. +The `_Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. dditionally, the class provides auxiliary helper methods to identify available computational devices (such as GPUs and TPUs) and to facilitate data transfer to these devices. For a detailed understanding, consult the [`scikit-learn` API Reference](https://scikit-learn.org/stable/modules/classes.html) and [`BaseEstimator`](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html). diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/03-glm.md index 0f7cb3a9..1711cb0a 100644 --- a/docs/developers_notes/03-glm.md +++ b/docs/developers_notes/03-glm.md @@ -11,11 +11,11 @@ The `neurostatslib.glm` module currently offers implementations of two GLM clas 1. **`GLM`:** A direct implementation of a feedforward GLM. 2. **`RecurrentGLM`:** An implementation of a recurrent GLM. This class inherits from `GLM` and redefines the `simulate` method to generate spikes akin to a recurrent neural network. -Our design is harmonized with the `scikit-learn` API, facilitating seamless integration of our GLM classes with the well-established `scikit-learn` pipeline and its cross-validation tools. +Our design aligns with the `scikit-learn` API, facilitating seamless integration of our GLM classes with the well-established `scikit-learn` pipeline and its cross-validation tools. The classes provided here are modular by design offering a standard foundation for any GLM variant. -Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the `neurostatslib.noise_model.NoiseModel` and `neurostatslib.solver.Solver` objects, respectively. +Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`neurostatslib.noise_model.NoiseModel`](../04-noise_model/#the-abstract-class-noisemodel) and [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) objects, respectively.
@@ -31,12 +31,12 @@ The `GLM` class provides a direct implementation of the GLM model and is designe ### Inheritance -`GLM` inherits from `BaseRegressor`. This inheritance mandates the direct implementation of methods like `predict`, `fit`, `score`, and `simulate`. +`GLM` inherits from [`BaseRegressor`](../02-base_class/#the-abstract-class-baseregressor). This inheritance mandates the direct implementation of methods like `predict`, `fit`, `score`, and `simulate`. ### Attributes -- **`solver`**: Refers to the optimization solver - an object of the `neurostatslib.solver.Solver` type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. -- **`noise_model`**: Represents the GLM noise model, which is an object of the `neurostatlib.noise_model.NoiseModel` type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. +- **`solver`**: Refers to the optimization solver - an object of the [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. +- **`noise_model`**: Represents the GLM noise model, which is an object of the [`neurostatslib.noise_model.NoiseModel`](../04-noise_model/#the-abstract-class-noisemodel) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. - **`basis_coeff_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`baseline_link_fr_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`solver_state`**: Indicates the solver's state. For specific solver states, refer to the [`jaxopt` documentation](https://jaxopt.github.io/stable/index.html#). @@ -65,7 +65,7 @@ The `RecurrentGLM` class is an extension of the `GLM`, designed to simulate mode ## Contributor Guidelines -### Implementing Model Subclasses +### Implementing GLM Subclasses When crafting a functional (i.e., concrete) GLM class: diff --git a/docs/developers_notes/04-noise_model.md b/docs/developers_notes/04-noise_model.md index 933835fa..aa6b6eb8 100644 --- a/docs/developers_notes/04-noise_model.md +++ b/docs/developers_notes/04-noise_model.md @@ -4,7 +4,7 @@ The `noise_model` module provides objects representing the observation noise of GLM-like models. -The abstract class `NoiseModel` defines the structure of the subclasses which specify observation noise types, such as Poisson, Gamma, etc. These objects serve as attributes of the `neurostatslib.glm.GLM` class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. +The abstract class `NoiseModel` defines the structure of the subclasses which specify observation noise types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`neurostatslib.glm.GLM`](../03-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. ## The Abstract class `NoiseModel` @@ -16,7 +16,7 @@ For subclasses derived from `NoiseModel` to function correctly, they must implem - **negative_log_likelihood**: Computes the negative-log likelihood of the model up to a normalization constant. This method is usually part of the objective function used to learn GLM parameters. -- **emission_probability**: Returns the random emission probability function. This typically invokes `jax.random` emission probability, provided some sufficient statistics. For distributions in the exponential family, the sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is entirely specified by the model's weights, while the scale is either fixed (i.e., Poisson) or needs to be estimated (i.e., Gamma). +- **emission_probability**: Returns the random emission probability function. This typically invokes `jax.random` emission probability, provided some sufficient statistics[^1]. For distributions in the exponential family, the sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is entirely specified by the model's weights, while the scale is either fixed (i.e., Poisson) or needs to be estimated (i.e., Gamma). - **residual_deviance**: Computes the residual deviance based on the model's estimated rates and observations. @@ -24,7 +24,7 @@ For subclasses derived from `NoiseModel` to function correctly, they must implem ### Public Methods -- **pseudo_r2**: Method for computing the pseudo-$R^2$ of the model based on the residual deviance. There is no consensus definition for the pseudo-$R^2$, what we used here is the definition by Choen at al. 2003[^1]. +- **pseudo_r2**: Method for computing the pseudo-$R^2$ of the model based on the residual deviance. There is no consensus definition for the pseudo-$R^2$, what we used here is the definition by Choen at al. 2003[^2]. ### Auxiliary Methods @@ -55,8 +55,10 @@ To implement a noise model class you - **Should not** reimplement the `pseudo_r2` method as well as the `_check_inverse_link_function` auxiliary method. - -[^1]: +[^1]: + In statistics, a statistic is sufficient with respect to a statistical model and its associated unknown parameters if "no other statistic that can be calculated from the same sample provides any additional information as to the value of the parameters", adapted from Fisher R. A. + 1922. On the mathematical foundations of theoretical statistics. *Philosophical Transactions of the Royal Society of London. Series A, Containing Papers of a Mathematical or Physical Character* 222:309–368. http://doi.org/10.1098/rsta.1922.0009. +[^2]: Jacob Cohen, Patricia Cohen, Steven G. West, Leona S. Aiken. *Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences*. 3rd edition. Routledge, 2002. p.502. ISBN 978-0-8058-2223-6. (May 2012) diff --git a/docs/developers_notes/05-solver.md b/docs/developers_notes/05-solver.md index 3d7bb910..240ad1ea 100644 --- a/docs/developers_notes/05-solver.md +++ b/docs/developers_notes/05-solver.md @@ -4,13 +4,27 @@ The `solver` module introduces an archetype class `Solver` which provides the structural components for each concrete sub-class. -Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. +Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`neurostatslib.glm.GLM`](../03-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). -We choose `jaxopt` as dependency because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX. +We choose to rely on `jaxopt` because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX, that are highly customizable. Each solver object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). +``` +Abstract Class Solver +| +├─ Concrete Class UnRegularizedSolver +| +├─ Concrete Class RidgeSolver +| +└─ Abstract Class ProximalGradientSolver + | + ├─ Concrete Class LassoSolver + | + └─ Concrete Class GroupLassoSolver +``` + !!! note If we need advanced adaptive optimizers (e.g., Adam, LAMB etc.) in the future, we should consider adding [`Optax`](https://optax.readthedocs.io/en/latest/) as a dependency, which is compatible with `jaxopt`, see [here](https://jaxopt.github.io/stable/_autosummary/jaxopt.OptaxSolver.html#jaxopt.OptaxSolver). @@ -138,6 +152,22 @@ runner = group_lasso_solver.instantiate_solver(loss_function) optim_results = runner(init_params, exog_vars, endog_vars) ``` +## Contributor Guidelines + +### Implementing Solver Subclasses + +When developing a functional (i.e., concrete) Solver class: + +- **Must** inherit from `Solver` or one of its derivatives. +- **Must** implement the `instantiate_solver` method to tailor the solver instantiation based on the provided loss function. +- For any Proximal Gradient method, **must** include a `get_prox_operator` method to define the proximal operator. +- **Must** possess an `allowed_optimizers` attribute to list the optimizer names that are permissible to be used with this solver. +- **May** embed additional attributes and methods such as `mask` and `_check_mask` if required by the specific Solver subclass for handling special optimization scenarios. +- **May** include a `regularizer_strength` attribute to control the strength of the regularization in scenarios where regularization is applicable. +- **May** rely on a custom solver implementation for specific optimization problems, but the implementation **must** adhere to the `jaxopt` API. + +These guidelines ensure that each Solver subclass adheres to a consistent structure and behavior, facilitating ease of extension and maintenance. + ## Glossary | Term | Description | From 4e3f51afe356af47548d8513c80ad1d31728b518 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 12:36:46 -0400 Subject: [PATCH 094/250] linted --- src/neurostatslib/noise_model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index e16093ae..ccc5535e 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -45,7 +45,7 @@ def __init__(self, inverse_link_function: Callable, **kwargs): super().__init__(**kwargs) self._check_inverse_link_function(inverse_link_function) self._inverse_link_function = inverse_link_function - self._scale = 1. + self._scale = 1.0 @property def inverse_link_function(self): @@ -317,5 +317,4 @@ def residual_deviance( def estimate_scale(self, predicted_rate: jnp.ndarray): """Assign 1 to the scale parameter of the Poisson model.""" - self.scale = 1. - + self.scale = 1.0 From 3767617529dd3804109f2261e728394b1ff23ba8 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 13:29:12 -0400 Subject: [PATCH 095/250] added one line docstrings for abstract methods --- src/neurostatslib/base_class.py | 4 ++++ src/neurostatslib/proximal_operator.py | 2 ++ tests/conftest.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index d94455df..094510bd 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -241,10 +241,12 @@ class BaseRegressor(_Base, abc.ABC): @abc.abstractmethod def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): + """Fit the model to neural activity.""" pass @abc.abstractmethod def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + """Predict rates based on fit parameters.""" pass @abc.abstractmethod @@ -255,6 +257,7 @@ def score( # may include score_type or other additional model dependent kwargs **kwargs, ) -> jnp.ndarray: + """Score the predicted firing rates (based on fit) to the target neural activity.""" pass @abc.abstractmethod @@ -265,6 +268,7 @@ def simulate( # feed-forward input and/coupling basis **kwargs: Any, ): + """Simulate neural activity in response to a feed-forward input and recurrent activity.""" pass @staticmethod diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index e7fb8bd4..a20b8d10 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -1,3 +1,4 @@ +"""Collection of proximal operators.""" from typing import Tuple import jax @@ -19,6 +20,7 @@ def _norm2_masked(weight_neuron: jnp.ndarray, mask: jnp.ndarray) -> jnp.ndarray: ------- : The norm of the weight vector corresponding to the feature in mask. + Notes ----- The proximal gradient operator is described in article [1], Proposition 1. diff --git a/tests/conftest.py b/tests/conftest.py index d914f6e6..eef76609 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -57,7 +57,7 @@ def poissonGLM_model_instantiation(): w_true = np.random.normal(size=(1, 5)) noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) - model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") + model = nsl.glm.GLM(noise_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -133,7 +133,7 @@ def group_sparse_poisson_glm_model_instantiation(): mask[1, [0,4]] = 1 noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) - model = nsl.glm.GLM(noise_model, solver, score_type="log-likelihood") + model = nsl.glm.GLM(noise_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask From 34e257094a375e51cccbbd5666892332063b2687 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 13:30:01 -0400 Subject: [PATCH 096/250] linted docstrings --- src/neurostatslib/base_class.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 094510bd..9b9271b7 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -1,5 +1,4 @@ -"""## Abstract class for estimators. -""" +"""## Abstract class for estimators.""" import abc import inspect From c9df746e829bc44447d975b464ce4c5dd9d86773 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 14:52:47 -0400 Subject: [PATCH 097/250] linted docstrings with pydocstyle --- src/neurostatslib/base_class.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 9b9271b7..75c442bb 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -236,6 +236,27 @@ def _get_param_names(cls): class BaseRegressor(_Base, abc.ABC): + """Abstract base class for GLM regression models. + + This class encapsulates the common functionality for Generalized Linear Models (GLM) + regression models. It provides an abstraction for fitting the model, making predictions, + scoring the model, simulating responses, and preprocessing data. Concrete classes + are expected to provide specific implementations of the abstract methods defined here. + + Attributes + ---------- + FLOAT_EPS : float + A small float representing machine epsilon for float32, used to handle numerical + stability issues. + + See Also + -------- + Concrete models: + + - [`GLM`](../glm/#neurostatslib.glm.GLM): A feed-forward GLM implementation. + - [`GLMRecurrent`](../glm/#neurostatslib.glm.GLMRecurrent): A recurrent GLM implementation. + """ + FLOAT_EPS = jnp.finfo(jnp.float32).eps @abc.abstractmethod From fc962a5496f36b7958e00b167fbe3ab4e957dff4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 16:20:10 -0400 Subject: [PATCH 098/250] mypy fixes --- src/neurostatslib/base_class.py | 43 +++++++++++++++++++++------------ src/neurostatslib/basis.py | 2 +- src/neurostatslib/glm.py | 16 +++--------- src/neurostatslib/solver.py | 31 +++++++++--------------- src/neurostatslib/utils.py | 9 +++++++ tests/test_glm.py | 28 ++++++++++----------- 6 files changed, 66 insertions(+), 63 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 75c442bb..c3577838 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -11,7 +11,7 @@ from jax._src.lib import xla_client from numpy.typing import ArrayLike, NDArray -from .utils import has_local_device +from .utils import has_local_device, is_sequence class _Base: @@ -93,7 +93,7 @@ def set_params(self, **params: Any): # Simple optimization to gain speed (inspect is slow) return self valid_params = self.get_params(deep=True) - nested_params = defaultdict(dict) # grouped by prefix + nested_params: defaultdict = defaultdict(dict) # grouped by prefix for key, value in params.items(): key, delim, sub_key = key.partition("__") if key not in valid_params: @@ -188,9 +188,11 @@ def device_put( : The arrays on the desired device. """ - device = self.select_target_device(device) + device_obj = self.select_target_device(device) return tuple( - jax.device_put(arg, device) if arg.device_buffer.device() != device else arg + jax.device_put(arg, device_obj) + if arg.device_buffer.device() != device_obj + else arg for arg in args ) @@ -285,8 +287,6 @@ def simulate( self, random_key: jax.random.PRNGKeyArray, feed_forward_input: Union[NDArray, jnp.ndarray], - # feed-forward input and/coupling basis - **kwargs: Any, ): """Simulate neural activity in response to a feed-forward input and recurrent activity.""" pass @@ -330,8 +330,8 @@ def _has_invalid_entry(array: jnp.ndarray) -> bool: @staticmethod def _check_and_convert_params( - params: ArrayLike, data_type: Optional[jnp.dtype] = None - ) -> Tuple[jnp.ndarray, ...]: + params: Tuple[ArrayLike, ArrayLike], data_type: Optional[jnp.dtype] = None + ) -> Tuple[jnp.ndarray, jnp.ndarray]: """ Validate the dimensions and consistency of parameters and data. @@ -340,19 +340,22 @@ def _check_and_convert_params( It ensures that the parameters and data are compatible for the model. """ - if not hasattr(params, "__getitem__"): + if not is_sequence(params): raise TypeError("Initial parameters must be array-like!") + + if len(params) != 2: + raise ValueError("Params needs to be array-like of length two.") + try: - params = tuple(jnp.asarray(par, dtype=data_type) for par in params) + params = jnp.asarray(params[0], dtype=data_type), jnp.asarray( + params[1], dtype=data_type + ) except (ValueError, TypeError): raise TypeError( "Initial parameters must be array-like of array-like objects" "with numeric data-type!" ) - if len(params) != 2: - raise ValueError("Params needs to be array-like of length two.") - if params[0].ndim != 2: raise ValueError( "params[0] must be of shape (n_neurons, n_features), but" @@ -509,7 +512,7 @@ def preprocess_fit( init_params = self._check_and_convert_params(init_params) # check that the inputs and the parameters has consistent sizes - self._check_input_and_params_consistency(init_params, X, y) + self._check_input_and_params_consistency(init_params, X=X, y=y) return X, y, init_params @@ -558,8 +561,16 @@ def preprocess_simulate( if self._has_invalid_entry(feedforward_input): raise ValueError("feedforward_input contains a NaNs or Infs!") - if init_y is not None: - (init_y,) = self._convert_to_jnp_ndarray(init_y) + # Ensure that both or neither of `init_y` and `params_r` are provided + if (init_y is None) != (params_r is None): + raise ValueError( + "Both `init_y` and `params_r` should be provided, or neither should be provided." + ) + # If both are provided, perform checks and conversions + elif init_y is not None and params_r is not None: + init_y = self._convert_to_jnp_ndarray(init_y)[ + 0 + ] # Assume this method returns a tuple self._check_input_dimensionality(y=init_y) self._check_input_and_params_consistency(params_r, y=init_y) return feedforward_input, init_y diff --git a/src/neurostatslib/basis.py b/src/neurostatslib/basis.py index 527ae1f4..103d0bf5 100644 --- a/src/neurostatslib/basis.py +++ b/src/neurostatslib/basis.py @@ -60,7 +60,7 @@ def _evaluate(self, *xi: NDArray) -> NDArray: pass @staticmethod - def _get_samples(*n_samples: int) -> Generator[NDArray, ...]: + def _get_samples(*n_samples: int) -> Generator[NDArray, None, None]: """Get equi-spaced samples for all the input dimensions. This will be used to evaluate the basis on a grid of diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 56190db4..134adae3 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,5 +1,5 @@ """GLM core module.""" -from typing import Any, Literal, Optional, Tuple, Union +from typing import Literal, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -348,8 +348,6 @@ def simulate( self, random_key: jax.random.PRNGKeyArray, feedforward_input: Union[NDArray, jnp.ndarray], - # feed-forward input and/coupling basis - **kwargs: Any, ) -> Tuple[jnp.ndarray, jnp.ndarray]: """Simulate neural activity in response to a feed-forward input. @@ -451,12 +449,12 @@ def __init__( ): super().__init__(noise_model=noise_model, solver=solver) - def simulate( + def simulate_recurrent( self, random_key: jax.random.PRNGKeyArray, feedforward_input: Union[NDArray, jnp.ndarray], - coupling_basis_matrix: Optional[Union[NDArray, jnp.ndarray]] = None, - init_y: Union[NDArray, Optional[jnp.ndarray]] = None, + coupling_basis_matrix: Union[NDArray, jnp.ndarray], + init_y: Union[NDArray, jnp.ndarray], ): """ Simulate neural activity using the GLM as a recurrent network. @@ -466,9 +464,6 @@ def simulate( of historical activity and external feedforward inputs like convolved currents, light intensities, etc. - If no `coupling_basis_matrix` is provided, the spikes will be generated in response to - the feedforward input only. - Parameters ---------- random_key : @@ -518,9 +513,6 @@ def simulate( The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` to ensure consistency in the model's input feature dimensionality. """ - if coupling_basis_matrix is None: - return super().simulate(random_key, feedforward_input) - # check if the model is fit self._check_is_fit() diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 944210bd..6681e22e 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -7,7 +7,7 @@ """ import abc import inspect -from typing import Callable, Optional, Tuple, Union +from typing import Callable, List, Optional, Tuple, Union import jax.numpy as jnp import jaxopt @@ -35,8 +35,6 @@ class Solver(_Base, abc.ABC): ---------- allowed_optimizers : List of optimizer names that are allowed for use with this solver. - regularizer_strength : - Strength of the regularization to be applied. solver_name : Name of the solver being used. solver_kwargs : @@ -50,18 +48,16 @@ class Solver(_Base, abc.ABC): Get the solver runner with provided arguments. """ - allowed_optimizers = [] + allowed_optimizers: List[str] = [] def __init__( self, solver_name: str, solver_kwargs: Optional[dict] = None, - regularizer_strength: Optional[float] = None, **kwargs, ): super().__init__(**kwargs) self._check_solver(solver_name) - self.regularizer_strength = regularizer_strength self._solver_name = solver_name if solver_kwargs is None: self._solver_kwargs = dict() @@ -306,11 +302,8 @@ def __init__( solver_kwargs: Optional[dict] = None, regularizer_strength: float = 1.0, ): - super().__init__( - solver_name, - solver_kwargs=solver_kwargs, - regularizer_strength=regularizer_strength, - ) + super().__init__(solver_name, solver_kwargs=solver_kwargs) + self.regularizer_strength = regularizer_strength def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: """ @@ -388,12 +381,9 @@ def __init__( regularizer_strength: float = 1.0, mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): - super().__init__( - solver_name, - solver_kwargs=solver_kwargs, - regularizer_strength=regularizer_strength, - ) + super().__init__(solver_name, solver_kwargs=solver_kwargs) self._mask = mask + self.regularizer_strength = regularizer_strength @property def mask(self): @@ -405,7 +395,7 @@ def mask(self, mask: jnp.ndarray): self._mask = mask @staticmethod - def _check_mask(mask: Optional[jnp.ndarray] = None): + def _check_mask(mask: jnp.ndarray): """ Validate the mask array. @@ -514,9 +504,9 @@ def __init__( super().__init__( solver_name, solver_kwargs=solver_kwargs, - regularizer_strength=regularizer_strength, mask=mask, ) + self.regularizer_strength = regularizer_strength def get_prox_operator( self, @@ -567,16 +557,17 @@ class GroupLassoSolver(ProxGradientSolver): def __init__( self, solver_name: str, - mask: Union[jnp.ndarray, NDArray], + mask: Union[NDArray, jnp.ndarray], solver_kwargs: Optional[dict] = None, regularizer_strength: float = 1.0, ): super().__init__( solver_name, solver_kwargs=solver_kwargs, - regularizer_strength=regularizer_strength, mask=mask, ) + self.regularizer_strength = regularizer_strength + mask = jnp.asarray(mask) self._check_mask(mask) def get_prox_operator( diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index bee9a07c..1c8ade07 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -405,3 +405,12 @@ def has_local_device(device_type: str) -> bool: return any( device_type in device.device_kind.lower() for device in jax.local_devices() ) + + +def is_sequence(obj) -> bool: + """Check if an object is a sequence.""" + return ( + hasattr(obj, "__iter__") + and hasattr(obj, "__getitem__") + and not isinstance(obj, (str, bytes, dict)) + ) diff --git a/tests/test_glm.py b/tests/test_glm.py index eb55b705..b9f50c83 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -116,10 +116,10 @@ def test_fit_param_values(self, add_entry, add_to, error, match_str, poissonGLM_ error, match_str) @pytest.mark.parametrize("dim_weights, error, match_str", [ - (0, ValueError, "params\[0\] must be of shape \(n_neurons, n_features\)"), - (1, ValueError, "params\[0\] must be of shape \(n_neurons, n_features\)"), + (0, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)"), + (1, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)"), (2, None, None), - (3, ValueError, "params\[0\] must be of shape \(n_neurons, n_features\)") + (3, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)") ]) def test_fit_weights_dimensionality(self, dim_weights, error, match_str, poissonGLM_model_instantiation): """ @@ -686,7 +686,7 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, error, match_str, feedforward_input = np.zeros((n_time_points, n_neurons+delta_n_neuron, n_basis_input)) _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -722,7 +722,7 @@ def test_simulate_input_dimensionality(self, delta_dim, error, match_str, _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -760,7 +760,7 @@ def test_simulate_y_dimensionality(self, delta_dim, error, match_str, _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -792,7 +792,7 @@ def test_simulate_n_neuron_match_y(self, delta_n_neuron, error, match_str, init_spikes = jnp.zeros((init_spikes.shape[0], n_neurons + delta_n_neuron)) _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -822,7 +822,7 @@ def test_simulate_is_fit(self, is_fit, error, match_str, model.baseline_link_fr_ = None _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -855,7 +855,7 @@ def test_simulate_time_point_match_y(self, delta_tp, error, match_str, init_spikes.shape[1])) _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -888,7 +888,7 @@ def test_simulate_time_point_match_coupling_basis(self, delta_tp, error, match_s coupling_basis.shape[1:]) _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -927,7 +927,7 @@ def test_simulate_feature_consistency_input(self, delta_features, error, match_s feedforward_input.shape[2] + delta_features)) _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -964,7 +964,7 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, error coupling_basis.shape[1] + delta_features)) _test_class_method( model, - "simulate", + "simulate_recurrent", [], { "random_key": random_key, @@ -1033,7 +1033,7 @@ def test_end_to_end_fit_and_simulate(self, n_timepoints = feedforward_input.shape[0] # generate spike trains - spikes, _ = model.simulate(random_key=random_key, + spikes, _ = model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input) @@ -1065,7 +1065,7 @@ def test_end_to_end_fit_and_simulate(self, model.fit(X, spikes[:-window_size]) # simulate - model.simulate(random_key=random_key, + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, feedforward_input=feedforward_input) From 1cf9d07e986bc1c422f7958d9c63d87e4d1a7be2 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 16:31:20 -0400 Subject: [PATCH 099/250] changed notes order --- docs/developers_notes/{04-noise_model.md => 03-noise_model.md} | 0 docs/developers_notes/{05-solver.md => 04-solver.md} | 0 docs/developers_notes/{03-glm.md => 05-glm.md} | 0 docs/developers_notes/README.md | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename docs/developers_notes/{04-noise_model.md => 03-noise_model.md} (100%) rename docs/developers_notes/{05-solver.md => 04-solver.md} (100%) rename docs/developers_notes/{03-glm.md => 05-glm.md} (100%) diff --git a/docs/developers_notes/04-noise_model.md b/docs/developers_notes/03-noise_model.md similarity index 100% rename from docs/developers_notes/04-noise_model.md rename to docs/developers_notes/03-noise_model.md diff --git a/docs/developers_notes/05-solver.md b/docs/developers_notes/04-solver.md similarity index 100% rename from docs/developers_notes/05-solver.md rename to docs/developers_notes/04-solver.md diff --git a/docs/developers_notes/03-glm.md b/docs/developers_notes/05-glm.md similarity index 100% rename from docs/developers_notes/03-glm.md rename to docs/developers_notes/05-glm.md diff --git a/docs/developers_notes/README.md b/docs/developers_notes/README.md index 4576ea60..53dfc83a 100644 --- a/docs/developers_notes/README.md +++ b/docs/developers_notes/README.md @@ -6,7 +6,7 @@ Welcome to the Developer Notes of the `neurostatslib` project. These notes aim t #### - [The `basis` Module](01-basis_module.md) #### - [The `base_class` Module](02-base_class.md) -#### - [The `glm` Module](03-glm.md) +#### - [The `glm` Module](05-glm.md) ## Intended Audience From b0a9618eaf01c261826c04a8f97ec3e0eb6a2d4f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 16:38:43 -0400 Subject: [PATCH 100/250] fixed examples --- .../{plot_a1D_basis_function.py => plot_1D_basis_function.py} | 0 docs/examples/plot_glm_demo.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/examples/{plot_a1D_basis_function.py => plot_1D_basis_function.py} (100%) diff --git a/docs/examples/plot_a1D_basis_function.py b/docs/examples/plot_1D_basis_function.py similarity index 100% rename from docs/examples/plot_a1D_basis_function.py rename to docs/examples/plot_1D_basis_function.py diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index e5a1463d..6b809259 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -259,7 +259,7 @@ # %% -# We can now simulate spikes by calling the `simulate` method. +# We can now simulate spikes by calling the `simulate_recurrent` method. model = nsl.glm.GLMRecurrent() model.basis_coeff_ = jax.numpy.asarray(basis_coeff) @@ -268,7 +268,7 @@ # call simulate, with both the recurrent coupling # and the input -spikes, rates = model.simulate( +spikes, rates = model.simulate_recurrent( random_key, feedforward_input=feedforward_input, coupling_basis_matrix=coupling_basis, From 48e9e81d22b4735e38083bd4abd06b0f17aa28ca Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 16:53:30 -0400 Subject: [PATCH 101/250] restart the ci --- mkdocs.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 41c9c208..c09f701d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -52,4 +52,3 @@ nav: - Tutorials: generated/gallery # Link to the generated gallery as Tutorials - For Developers: developers_notes/ # Link to the developers notes - Code References: reference/ # Link to the reference/ directory - From 00b24019d6e67cf7c5ef91875ad1a1df11f63d5a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 22 Sep 2023 17:15:06 -0400 Subject: [PATCH 102/250] show inherited methods in docs --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index c09f701d..b66ffdd4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -36,6 +36,7 @@ plugins: docstring_style: numpy show_source: true members_order: source + inherited_members: true filters: - "!neurostatslib.glm.GLMBase.residual_deviance" From 0cd45f9d723b526eca51b88032a72f4c831960bb Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 26 Sep 2023 09:23:25 -0400 Subject: [PATCH 103/250] added new modules to index --- docs/developers_notes/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/developers_notes/README.md b/docs/developers_notes/README.md index 53dfc83a..34bbb489 100644 --- a/docs/developers_notes/README.md +++ b/docs/developers_notes/README.md @@ -6,6 +6,8 @@ Welcome to the Developer Notes of the `neurostatslib` project. These notes aim t #### - [The `basis` Module](01-basis_module.md) #### - [The `base_class` Module](02-base_class.md) +#### - [The `noise_model` Module](03-noise_model.md) +#### - [The `solver` Module](04-solver.md) #### - [The `glm` Module](05-glm.md) ## Intended Audience From 44e5558ffddc4290e3eb145cb1a4542ab5b84a4b Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Thu, 26 Oct 2023 09:08:45 -0400 Subject: [PATCH 104/250] Update docs/developers_notes/02-base_class.md Co-authored-by: William F. Broderick --- docs/developers_notes/02-base_class.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index bf4ab7c9..aedf9655 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -11,7 +11,7 @@ Designed to be compatible with the `scikit-learn` API, the class structure aims Below a scheme of how we envision the architecture of the `neurostatslib` models. ``` -Class _Base +Abstract Class _Base │ ├─ Abstract Subclass BaseRegressor │ │ From b8ca9320d687f049d1be0f81d40cd32c8ed64a1a Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Thu, 26 Oct 2023 09:09:46 -0400 Subject: [PATCH 105/250] Update docs/developers_notes/02-base_class.md Co-authored-by: William F. Broderick --- docs/developers_notes/02-base_class.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index aedf9655..6ad242bf 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -41,7 +41,7 @@ Abstract Class _Base ``` !!! Example - The current package version includes a concrete class named `neurostatslib.glm.GLM`. This class inherits from `BaseRegressor` <- `_Base`, since it falls under the " GLM regression" category. + The current package version includes a concrete class named `neurostatslib.glm.GLM`. This class inherits from `BaseRegressor`, which in turn inherits `_Base`, since it falls under the " GLM regression" category. As any `BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. From 65b2df40de416a00c58b22140ef65a32d348036c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 26 Oct 2023 09:11:34 -0400 Subject: [PATCH 106/250] changed CI to edit with isort and not just check --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index fc61991e..112eb560 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ package_cache = .tox/cache # while black, isort and flake8 are also i commands = black --check src - isort --check src + isort src flake8 --config={toxinidir}/tox.ini src pytest --cov From faf0e6088ced79a8681ddbe7ed409eb435c743f6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 26 Oct 2023 09:13:29 -0400 Subject: [PATCH 107/250] removed index --- docs/developers_notes/README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/developers_notes/README.md b/docs/developers_notes/README.md index 34bbb489..98e7e00e 100644 --- a/docs/developers_notes/README.md +++ b/docs/developers_notes/README.md @@ -2,13 +2,6 @@ Welcome to the Developer Notes of the `neurostatslib` project. These notes aim to provide detailed technical information about the various modules, classes, and functions that make up this library, as well as guidelines on how to write code that integrates nicely with our package. They are intended to help current and future developers understand the design decisions, structure, and functioning of the library, and to provide guidance on how to modify, extend, and maintain the codebase. -## Index - -#### - [The `basis` Module](01-basis_module.md) -#### - [The `base_class` Module](02-base_class.md) -#### - [The `noise_model` Module](03-noise_model.md) -#### - [The `solver` Module](04-solver.md) -#### - [The `glm` Module](05-glm.md) ## Intended Audience From 89112fd2d6b9a6e9dc172232b1ffcd575297567f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 26 Oct 2023 09:24:17 -0400 Subject: [PATCH 108/250] replaces yaml with json. removed yaml dependency --- docs/examples/coupled_neurons_params.json | 10703 ++++++++++++++++++++ docs/examples/coupled_neurons_params.yml | 4292 -------- docs/examples/plot_glm_demo.py | 9 +- pyproject.toml | 1 - tox.ini | 1 + 5 files changed, 10709 insertions(+), 4297 deletions(-) create mode 100644 docs/examples/coupled_neurons_params.json delete mode 100644 docs/examples/coupled_neurons_params.yml diff --git a/docs/examples/coupled_neurons_params.json b/docs/examples/coupled_neurons_params.json new file mode 100644 index 00000000..fef81ce4 --- /dev/null +++ b/docs/examples/coupled_neurons_params.json @@ -0,0 +1,10703 @@ +{ + "baseline_link_fr_": [ + -4.0, + -4.0 + ], + "basis_coeff_": [ + [ + -0.004372, + -0.02786, + -0.04582, + -0.0588, + -0.06539, + -0.06396, + -0.05328, + -0.03192, + 0.0002296, + 0.04143, + 0.08794, + 0.1483, + 0.2053, + 0.2483, + 0.2892, + 0.3093, + 0.2917, + 0.2225, + 0.07357, + -0.2711, + -0.006235, + -0.01047, + 0.02189, + 0.058, + 0.09002, + 0.1118, + 0.1209, + 0.1167, + 0.09909, + 0.07044, + 0.03448, + -0.01565, + -0.06823, + -0.1128, + -0.1655, + -0.2176, + -0.2621, + -0.2982, + -0.3255, + -0.3449, + 0.5, + 0.5 + ], + [ + -0.004637, + 0.02223, + 0.07071, + 0.09572, + 0.1012, + 0.08923, + 0.06464, + 0.03076, + -0.007911, + -0.04737, + -0.08429, + -0.1249, + -0.1582, + -0.1827, + -0.2081, + -0.23, + -0.2473, + -0.2616, + -0.2741, + -0.287, + 0.01127, + 0.04864, + 0.0544, + 0.05082, + 0.03975, + 0.02393, + 0.004725, + -0.01763, + -0.04202, + -0.06744, + -0.09269, + -0.1231, + -0.1522, + -0.1763, + -0.2051, + -0.2348, + -0.2629, + -0.2896, + -0.3149, + -0.3389, + 0.5, + 0.5 + ] + ], + "coupling_basis": [ + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0024979173609873673, + 0.9975020826390129 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.11451325277931029, + 0.8854867472206909, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25013898844998006, + 0.7498610115500185, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3122501403134024, + 0.687749859686596, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.28176761370807446, + 0.7182323862919272, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.17383844924397923, + 0.8261615507560222, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.04364762794083282, + 0.9563523720591665, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9912618171282106, + 0.008738182871789013, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7892946476427273, + 0.21070535235727128, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3531647741677867, + 0.6468352258322151, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.011883820048045501, + 0.9881161799519544, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7841665801263835, + 0.21583341987361648, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.17688067665784446, + 0.8231193233421555, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9253003862638604, + 0.0746996137361397, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2549435480705588, + 0.7450564519294413, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9205258993369989, + 0.07947410066300109, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.16827351931758228, + 0.8317264806824178, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.7835282009408713, + 0.21647179905912872, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019118847416525586, + 0.9808811525834744, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.4372031242218587, + 0.5627968757781414, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.9120243919870162, + 0.08797560801298382, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.044222034278324274, + 0.9557779657216758, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.40793669708774605, + 0.5920633029122541, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.8283923698925478, + 0.17160763010745222, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.9999802058373224, + 1.9794162677666538e-05, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.1458111022283093, + 0.8541888977716907, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.4778824971400245, + 0.5221175028599756, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.803486827077907, + 0.19651317292209308, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 0.9824675828481839, + 0.017532417151816082, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.029720664099906924, + 0.9702793359000932, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.19724020774947038, + 0.8027597922505296, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.44389603578613035, + 0.5561039642138698, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.6909694421867117, + 0.30903055781328825, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.8804498633788072, + 0.1195501366211929, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.9828262050955638, + 0.017173794904436157, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.005816278861877466, + 0.9941837211381226, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.07171948190677246, + 0.9282805180932275, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.19211081158089233, + 0.8078891884191077, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.3422365913893123, + 0.6577634086106878, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.49997219806462273, + 0.5000278019353773, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.6481581380891199, + 0.3518418619108801, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.775227808426499, + 0.22477219157350103, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.8747644272334134, + 0.12523557276658664, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.9445228823471115, + 0.05547711765288865, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.9852942394771702, + 0.014705760522829736, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.9998405276097415, + 0.00015947239025848603, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.00798856965539202, + 0.9920114303446079, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.03392307742054024, + 0.9660769225794598, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.07373523476821137, + 0.9262647652317886, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.12352988337197751, + 0.8764701166280225, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.17990211564285485, + 0.8200978843571451, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.2399997347398921, + 0.7600002652601079, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.3015222924967669, + 0.6984777075032332, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.36268149196393995, + 0.63731850803606, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.42214108290743424, + 0.5778589170925659, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.47894873221112266, + 0.5210512677888774, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.5324679173051469, + 0.46753208269485313, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.5823146093533313, + 0.4176853906466687, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.6283012081735033, + 0.3716987918264968, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.6703886551778314, + 0.32961134482216864, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.7086466881407022, + 0.2913533118592979, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.7432216468423799, + 0.25677835315762026, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.7743109612271127, + 0.22568903877288732, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.802143356101582, + 0.197856643898418, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.82696381862707, + 0.17303618137292998, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.8490224486822571, + 0.15097755131774288, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.8685664156253453, + 0.13143358437465474, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.8858343578296817, + 0.11416564217031833, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9010526715389762, + 0.09894732846102389, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9144332365128198, + 0.08556676348718023, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9261722145965264, + 0.07382778540347357, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9364496329422705, + 0.06355036705772948, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9454295266061546, + 0.05457047339384541, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9532604668007324, + 0.04673953319926766, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9600763426393057, + 0.039923657360694254, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9659972972699125, + 0.03400270273008754, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.971130745291511, + 0.028869254708488945, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.975572418558468, + 0.024427581441531954, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9794074030288873, + 0.020592596971112653, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9827111411428311, + 0.017288858857168965, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9855503831123861, + 0.014449616887613925, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9879840771076767, + 0.012015922892323394, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9900641931482845, + 0.009935806851715523, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9918364789707291, + 0.008163521029270815, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9933411485659462, + 0.006658851434053759, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9946135057219054, + 0.005386494278094567, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9956845059646938, + 0.004315494035306178, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9965812609202838, + 0.0034187390797163486, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.997327489436671, + 0.002672510563328956, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9979439199017871, + 0.002056080098212898, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9984486481342357, + 0.0015513518657642722, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9988574550621354, + 0.0011425449378646424, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9991840881776304, + 0.0008159118223696749, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.999440510488429, + 0.0005594895115710874, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9996371204027914, + 0.00036287959720865404, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.999782945694725, + 0.00021705430527496627, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9998858144113889, + 0.00011418558861114869, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.9999525053112863, + 4.7494688713622946e-05, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.99998888016377, + 1.1119836230089053e-05, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + ], + "feedforward_input": [ + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 2.5 + ], + [ + 2.5 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ], + [ + [ + 0.0 + ], + [ + 0.0 + ] + ] + ], + "init_spikes": [ + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ], + [ + 0.0, + 0.0 + ] + ], + "n_neurons": 2 +} \ No newline at end of file diff --git a/docs/examples/coupled_neurons_params.yml b/docs/examples/coupled_neurons_params.yml deleted file mode 100644 index d61c47f6..00000000 --- a/docs/examples/coupled_neurons_params.yml +++ /dev/null @@ -1,4292 +0,0 @@ -baseline_link_fr_: -- -4.0 -- -4.0 -basis_coeff_: -- - -0.004372 - - -0.02786 - - -0.04582 - - -0.0588 - - -0.06539 - - -0.06396 - - -0.05328 - - -0.03192 - - 0.0002296 - - 0.04143 - - 0.08794 - - 0.1483 - - 0.2053 - - 0.2483 - - 0.2892 - - 0.3093 - - 0.2917 - - 0.2225 - - 0.07357 - - -0.2711 - - -0.006235 - - -0.01047 - - 0.02189 - - 0.058 - - 0.09002 - - 0.1118 - - 0.1209 - - 0.1167 - - 0.09909 - - 0.07044 - - 0.03448 - - -0.01565 - - -0.06823 - - -0.1128 - - -0.1655 - - -0.2176 - - -0.2621 - - -0.2982 - - -0.3255 - - -0.3449 - - 0.5 - - 0.5 -- - -0.004637 - - 0.02223 - - 0.07071 - - 0.09572 - - 0.1012 - - 0.08923 - - 0.06464 - - 0.03076 - - -0.007911 - - -0.04737 - - -0.08429 - - -0.1249 - - -0.1582 - - -0.1827 - - -0.2081 - - -0.23 - - -0.2473 - - -0.2616 - - -0.2741 - - -0.287 - - 0.01127 - - 0.04864 - - 0.0544 - - 0.05082 - - 0.03975 - - 0.02393 - - 0.004725 - - -0.01763 - - -0.04202 - - -0.06744 - - -0.09269 - - -0.1231 - - -0.1522 - - -0.1763 - - -0.2051 - - -0.2348 - - -0.2629 - - -0.2896 - - -0.3149 - - -0.3389 - - 0.5 - - 0.5 -coupling_basis: -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0024979173609873673 - - 0.9975020826390129 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.11451325277931029 - - 0.8854867472206909 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.25013898844998006 - - 0.7498610115500185 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.3122501403134024 - - 0.687749859686596 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.28176761370807446 - - 0.7182323862919272 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.17383844924397923 - - 0.8261615507560222 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.04364762794083282 - - 0.9563523720591665 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9912618171282106 - - 0.008738182871789013 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.7892946476427273 - - 0.21070535235727128 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.3531647741677867 - - 0.6468352258322151 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.011883820048045501 - - 0.9881161799519544 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.7841665801263835 - - 0.21583341987361648 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.17688067665784446 - - 0.8231193233421555 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9253003862638604 - - 0.0746996137361397 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.2549435480705588 - - 0.7450564519294413 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9205258993369989 - - 0.07947410066300109 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.16827351931758228 - - 0.8317264806824178 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.7835282009408713 - - 0.21647179905912872 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.019118847416525586 - - 0.9808811525834744 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.4372031242218587 - - 0.5627968757781414 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9120243919870162 - - 0.08797560801298382 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.044222034278324274 - - 0.9557779657216758 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.40793669708774605 - - 0.5920633029122541 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.8283923698925478 - - 0.17160763010745222 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9999802058373224 - - 1.9794162677666538e-05 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.1458111022283093 - - 0.8541888977716907 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.4778824971400245 - - 0.5221175028599756 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.803486827077907 - - 0.19651317292209308 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.9824675828481839 - - 0.017532417151816082 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.029720664099906924 - - 0.9702793359000932 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.19724020774947038 - - 0.8027597922505296 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.44389603578613035 - - 0.5561039642138698 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.6909694421867117 - - 0.30903055781328825 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.8804498633788072 - - 0.1195501366211929 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.9828262050955638 - - 0.017173794904436157 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.005816278861877466 - - 0.9941837211381226 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.07171948190677246 - - 0.9282805180932275 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.19211081158089233 - - 0.8078891884191077 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.3422365913893123 - - 0.6577634086106878 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.49997219806462273 - - 0.5000278019353773 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.6481581380891199 - - 0.3518418619108801 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.775227808426499 - - 0.22477219157350103 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.8747644272334134 - - 0.12523557276658664 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.9445228823471115 - - 0.05547711765288865 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.9852942394771702 - - 0.014705760522829736 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.9998405276097415 - - 0.00015947239025848603 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.00798856965539202 - - 0.9920114303446079 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.03392307742054024 - - 0.9660769225794598 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.07373523476821137 - - 0.9262647652317886 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.12352988337197751 - - 0.8764701166280225 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.17990211564285485 - - 0.8200978843571451 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.2399997347398921 - - 0.7600002652601079 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.3015222924967669 - - 0.6984777075032332 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.36268149196393995 - - 0.63731850803606 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.42214108290743424 - - 0.5778589170925659 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.47894873221112266 - - 0.5210512677888774 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.5324679173051469 - - 0.46753208269485313 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.5823146093533313 - - 0.4176853906466687 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.6283012081735033 - - 0.3716987918264968 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.6703886551778314 - - 0.32961134482216864 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.7086466881407022 - - 0.2913533118592979 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.7432216468423799 - - 0.25677835315762026 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.7743109612271127 - - 0.22568903877288732 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.802143356101582 - - 0.197856643898418 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.82696381862707 - - 0.17303618137292998 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.8490224486822571 - - 0.15097755131774288 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.8685664156253453 - - 0.13143358437465474 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.8858343578296817 - - 0.11416564217031833 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9010526715389762 - - 0.09894732846102389 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9144332365128198 - - 0.08556676348718023 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9261722145965264 - - 0.07382778540347357 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9364496329422705 - - 0.06355036705772948 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9454295266061546 - - 0.05457047339384541 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9532604668007324 - - 0.04673953319926766 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9600763426393057 - - 0.039923657360694254 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9659972972699125 - - 0.03400270273008754 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.971130745291511 - - 0.028869254708488945 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.975572418558468 - - 0.024427581441531954 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9794074030288873 - - 0.020592596971112653 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9827111411428311 - - 0.017288858857168965 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9855503831123861 - - 0.014449616887613925 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9879840771076767 - - 0.012015922892323394 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9900641931482845 - - 0.009935806851715523 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9918364789707291 - - 0.008163521029270815 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9933411485659462 - - 0.006658851434053759 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9946135057219054 - - 0.005386494278094567 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9956845059646938 - - 0.004315494035306178 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9965812609202838 - - 0.0034187390797163486 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.997327489436671 - - 0.002672510563328956 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9979439199017871 - - 0.002056080098212898 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9984486481342357 - - 0.0015513518657642722 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9988574550621354 - - 0.0011425449378646424 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9991840881776304 - - 0.0008159118223696749 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.999440510488429 - - 0.0005594895115710874 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9996371204027914 - - 0.00036287959720865404 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.999782945694725 - - 0.00021705430527496627 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9998858144113889 - - 0.00011418558861114869 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9999525053112863 - - 4.7494688713622946e-05 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.99998888016377 - - 1.1119836230089053e-05 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 1.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -feedforward_input: -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 2.5 - - - 2.5 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -- - - 0.0 - - - 0.0 -init_spikes: -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -n_neurons: 2 diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 6b809259..f8dd96d5 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -18,12 +18,13 @@ data. """ +import json + import jax import matplotlib.pyplot as plt -from matplotlib.patches import Rectangle import numpy as np import sklearn.model_selection as sklearn_model_selection -import yaml +from matplotlib.patches import Rectangle import neurostatslib as nsl @@ -222,8 +223,8 @@ # them on the fly. # load parameters -with open("coupled_neurons_params.yml", "r") as fh: - config_dict = yaml.safe_load(fh) +with open("coupled_neurons_params.json", "r") as fh: + config_dict = json.load(fh) # basis weights & intercept for the GLM (both coupling and feedforward) # (the last coefficient is the weight of the feedforward input) diff --git a/pyproject.toml b/pyproject.toml index 236dd2ef..71bcac56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,6 @@ dev = [ "pytest-cov", # Test coverage plugin for pytest "statsmodels", # Used to compare model pseudo-r2 in testing "scikit-learn", # Testing compatibility with CV & pipelines - "PyYAML" # Load GLM params for testing ] docs = [ "mkdocs", # Documentation generator diff --git a/tox.ini b/tox.ini index 112eb560..714db7f0 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ package_cache = .tox/cache commands = black --check src isort src + isort docs/examples flake8 --config={toxinidir}/tox.ini src pytest --cov From 226a7528bb7ac9cfe186f4ca23d83272d8c63446 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Thu, 26 Oct 2023 09:32:05 -0400 Subject: [PATCH 109/250] Update mkdocs.yml Co-authored-by: William F. Broderick --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b66ffdd4..c6baba31 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,7 +12,7 @@ theme: - admonition - tables -# if footnotes is degined in theme doesn't work +# if footnotes is defined in theme doesn't work # If md_in_html is defined outside theme, it also results in # an error when building the docs. markdown_extensions: From 23f1cc551673597e769a6eb7ee7e29b590c84632 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 26 Oct 2023 09:45:57 -0400 Subject: [PATCH 110/250] removed leftover filter in mkdocstrings --- mkdocs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index b66ffdd4..373d901c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -37,8 +37,6 @@ plugins: show_source: true members_order: source inherited_members: true - filters: - - "!neurostatslib.glm.GLMBase.residual_deviance" extra_javascript: - javascripts/katex.js From 099d517dd15d41b4d9c5c221e7c8538bdf629867 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 11:33:33 -0400 Subject: [PATCH 111/250] testing coverage --- pyproject.toml | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 71bcac56..17a0297d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] testpaths = ["tests"] # Specify the directory where test files are located +addopts = "--cov=neurostatslib" [tool.coverage.report] exclude_lines = [ diff --git a/tox.ini b/tox.ini index 714db7f0..96e65259 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ commands = isort src isort docs/examples flake8 --config={toxinidir}/tox.ini src - pytest --cov + pytest [gh-actions] python = From ede53f189bb31e94dd67812cbfde0bd0a683575a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 12:12:29 -0400 Subject: [PATCH 112/250] switched to json in tests --- tests/conftest.py | 14 +- tests/simulate_coupled_neurons_params.json | 1 + tests/simulate_coupled_neurons_params.yml | 6292 -------------------- tests/test_base_class.py | 5 +- 4 files changed, 10 insertions(+), 6302 deletions(-) create mode 100644 tests/simulate_coupled_neurons_params.json delete mode 100644 tests/simulate_coupled_neurons_params.yml diff --git a/tests/conftest.py b/tests/conftest.py index eef76609..4a61912a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,13 +9,13 @@ - jax.numpy: JAX's version of NumPy, used for matrix operations. - numpy: Standard Python numerical computing library. - pytest: Testing framework. - - yaml: For parsing and loading YAML configuration files. + - json: For parsing and loading json configuration files. Functions: - poissonGLM_model_instantiation: Sets up a Poisson GLM, instantiating its parameters with random values and returning a set of test data and expected output. - - poissonGLM_coupled_model_config_simulate: Reads from a YAML configuration file, + - poissonGLM_coupled_model_config_simulate: Reads from a json configuration file, sets up a Poisson GLM with predefined parameters, and returns the initialized model along with other related parameters. @@ -24,13 +24,13 @@ and loading predefined parameters for testing various functionalities of the `neurostatslib` library. """ import inspect +import json import os import jax import jax.numpy as jnp import numpy as np import pytest -import yaml import neurostatslib as nsl @@ -64,9 +64,9 @@ def poissonGLM_model_instantiation(): @pytest.fixture def poissonGLM_coupled_model_config_simulate(): - """Set up a Poisson GLM from a predefined configuration in a YAML file. + """Set up a Poisson GLM from a predefined configuration in a json file. - This fixture reads parameters for a Poisson GLM from a YAML configuration file, initializes + This fixture reads parameters for a Poisson GLM from a json configuration file, initializes the model accordingly, and returns the model instance with other related parameters. Returns: @@ -80,8 +80,8 @@ def poissonGLM_coupled_model_config_simulate(): current_file = inspect.getfile(inspect.currentframe()) test_dir = os.path.dirname(os.path.abspath(current_file)) with open(os.path.join(test_dir, - "simulate_coupled_neurons_params.yml"), "r") as fh: - config_dict = yaml.safe_load(fh) + "simulate_coupled_neurons_params.json"), "r") as fh: + config_dict = json.load(fh) noise = nsl.noise_model.PoissonNoiseModel(jnp.exp) solver = nsl.solver.RidgeSolver("BFGS", regularizer_strength=0.1) diff --git a/tests/simulate_coupled_neurons_params.json b/tests/simulate_coupled_neurons_params.json new file mode 100644 index 00000000..a52460e1 --- /dev/null +++ b/tests/simulate_coupled_neurons_params.json @@ -0,0 +1 @@ +{"baseline_link_fr_": [-3.0, -3.0], "basis_coeff_": [[-0.004372, -0.02786, -0.04582, -0.0588, -0.06539, -0.06396, -0.05328, -0.03192, 0.0002296, 0.04143, 0.08794, 0.1483, 0.2053, 0.2483, 0.2892, 0.3093, 0.2917, 0.2225, 0.07357, -0.2711, -0.006235, -0.01047, 0.02189, 0.058, 0.09002, 0.1118, 0.1209, 0.1167, 0.09909, 0.07044, 0.03448, -0.01565, -0.06823, -0.1128, -0.1655, -0.2176, -0.2621, -0.2982, -0.3255, -0.3449, 0.5, 0.5], [-0.004637, 0.02223, 0.07071, 0.09572, 0.1012, 0.08923, 0.06464, 0.03076, -0.007911, -0.04737, -0.08429, -0.1249, -0.1582, -0.1827, -0.2081, -0.23, -0.2473, -0.2616, -0.2741, -0.287, 0.01127, 0.04864, 0.0544, 0.05082, 0.03975, 0.02393, 0.004725, -0.01763, -0.04202, -0.06744, -0.09269, -0.1231, -0.1522, -0.1763, -0.2051, -0.2348, -0.2629, -0.2896, -0.3149, -0.3389, 0.5, 0.5]], "coupling_basis": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0024979173609873673, 0.9975020826390129], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.11451325277931029, 0.8854867472206909, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25013898844998006, 0.7498610115500185, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3122501403134024, 0.687749859686596, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.28176761370807446, 0.7182323862919272, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17383844924397923, 0.8261615507560222, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04364762794083282, 0.9563523720591665, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9912618171282106, 0.008738182871789013, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7892946476427273, 0.21070535235727128, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3531647741677867, 0.6468352258322151, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.011883820048045501, 0.9881161799519544, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7841665801263835, 0.21583341987361648, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17688067665784446, 0.8231193233421555, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9253003862638604, 0.0746996137361397, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2549435480705588, 0.7450564519294413, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9205258993369989, 0.07947410066300109, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16827351931758228, 0.8317264806824178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7835282009408713, 0.21647179905912872, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.019118847416525586, 0.9808811525834744, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.4372031242218587, 0.5627968757781414, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.9120243919870162, 0.08797560801298382, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.044222034278324274, 0.9557779657216758, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.40793669708774605, 0.5920633029122541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.8283923698925478, 0.17160763010745222, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.9999802058373224, 1.9794162677666538e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.1458111022283093, 0.8541888977716907, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.4778824971400245, 0.5221175028599756, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.803486827077907, 0.19651317292209308, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.9824675828481839, 0.017532417151816082, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.029720664099906924, 0.9702793359000932, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.19724020774947038, 0.8027597922505296, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.44389603578613035, 0.5561039642138698, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.6909694421867117, 0.30903055781328825, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.8804498633788072, 0.1195501366211929, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.9828262050955638, 0.017173794904436157, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.005816278861877466, 0.9941837211381226, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.07171948190677246, 0.9282805180932275, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.19211081158089233, 0.8078891884191077, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.3422365913893123, 0.6577634086106878, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.49997219806462273, 0.5000278019353773, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.6481581380891199, 0.3518418619108801, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.775227808426499, 0.22477219157350103, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.8747644272334134, 0.12523557276658664, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9445228823471115, 0.05547711765288865, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9852942394771702, 0.014705760522829736, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9998405276097415, 0.00015947239025848603, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.00798856965539202, 0.9920114303446079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.03392307742054024, 0.9660769225794598, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.07373523476821137, 0.9262647652317886, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.12352988337197751, 0.8764701166280225, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.17990211564285485, 0.8200978843571451, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.2399997347398921, 0.7600002652601079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.3015222924967669, 0.6984777075032332, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.36268149196393995, 0.63731850803606, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.42214108290743424, 0.5778589170925659, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.47894873221112266, 0.5210512677888774, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5324679173051469, 0.46753208269485313, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5823146093533313, 0.4176853906466687, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.6283012081735033, 0.3716987918264968, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.6703886551778314, 0.32961134482216864, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7086466881407022, 0.2913533118592979, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7432216468423799, 0.25677835315762026, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7743109612271127, 0.22568903877288732, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.802143356101582, 0.197856643898418, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.82696381862707, 0.17303618137292998, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8490224486822571, 0.15097755131774288, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8685664156253453, 0.13143358437465474, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8858343578296817, 0.11416564217031833, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9010526715389762, 0.09894732846102389, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9144332365128198, 0.08556676348718023, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9261722145965264, 0.07382778540347357, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9364496329422705, 0.06355036705772948, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9454295266061546, 0.05457047339384541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9532604668007324, 0.04673953319926766, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9600763426393057, 0.039923657360694254, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9659972972699125, 0.03400270273008754, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.971130745291511, 0.028869254708488945, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.975572418558468, 0.024427581441531954, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9794074030288873, 0.020592596971112653, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9827111411428311, 0.017288858857168965, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9855503831123861, 0.014449616887613925, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9879840771076767, 0.012015922892323394, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9900641931482845, 0.009935806851715523, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9918364789707291, 0.008163521029270815, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9933411485659462, 0.006658851434053759, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9946135057219054, 0.005386494278094567, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9956845059646938, 0.004315494035306178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9965812609202838, 0.0034187390797163486, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.997327489436671, 0.002672510563328956, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9979439199017871, 0.002056080098212898, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9984486481342357, 0.0015513518657642722, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9988574550621354, 0.0011425449378646424, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9991840881776304, 0.0008159118223696749, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.999440510488429, 0.0005594895115710874, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9996371204027914, 0.00036287959720865404, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.999782945694725, 0.00021705430527496627, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9998858144113889, 0.00011418558861114869, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9999525053112863, 4.7494688713622946e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.99998888016377, 1.1119836230089053e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], "feedforward_input": [[[0.0, 1.0], [0.0, 1.0]], [[0.012578617838741058, 0.9999208860571255], [0.012578617838741058, 0.9999208860571255]], [[0.025155245389375847, 0.9996835567465339], [0.025155245389375847, 0.9996835567465339]], [[0.03772789267871718, 0.99928804962034], [0.03772789267871718, 0.99928804962034]], [[0.05029457036336618, 0.9987344272588006], [0.05029457036336618, 0.9987344272588006]], [[0.06285329004448194, 0.9980227772604111], [0.06285329004448194, 0.9980227772604111]], [[0.07540206458240159, 0.9971532122280464], [0.07540206458240159, 0.9971532122280464]], [[0.08793890841106125, 0.9961258697511429], [0.08793890841106125, 0.9961258697511429]], [[0.10046183785216795, 0.9949409123839288], [0.10046183785216795, 0.9949409123839288]], [[0.11296887142907283, 0.9935985276197029], [0.11296887142907283, 0.9935985276197029]], [[0.12545803018029603, 0.9920989278611685], [0.12545803018029603, 0.9920989278611685]], [[0.13792733797265358, 0.9904423503868246], [0.13792733797265358, 0.9904423503868246]], [[0.1503748218139367, 0.9886290573134227], [0.1503748218139367, 0.9886290573134227]], [[0.1627985121650943, 0.986659335554492], [0.1627985121650943, 0.986659335554492]], [[0.17519644325186898, 0.984533496774942], [0.17519644325186898, 0.984533496774942]], [[0.18756665337583714, 0.9822518773417481], [0.18756665337583714, 0.9822518773417481]], [[0.19990718522480458, 0.9798148382707295], [0.19990718522480458, 0.9798148382707295]], [[0.21221608618250787, 0.9772227651694256], [0.21221608618250787, 0.9772227651694256]], [[0.22449140863757258, 0.9744760681760832], [0.22449140863757258, 0.9744760681760832]], [[0.23673121029167973, 0.9715751818947602], [0.23673121029167973, 0.9715751818947602]], [[0.2489335544668916, 0.9685205653265598], [0.2489335544668916, 0.9685205653265598]], [[0.2610965104120882, 0.9653127017970033], [0.2610965104120882, 0.9653127017970033]], [[0.27321815360846585, 0.9619520988795548], [0.27321815360846585, 0.9619520988795548]], [[0.28529656607404974, 0.9584392883153087], [0.28529656607404974, 0.9584392883153087]], [[0.2973298366671723, 0.9547748259288535], [0.2973298366671723, 0.9547748259288535]], [[0.30931606138886886, 0.9509592915403253], [0.30931606138886886, 0.9509592915403253]], [[0.32125334368414366, 0.9469932888736633], [0.32125334368414366, 0.9469932888736633]], [[0.33313979474205757, 0.9428774454610842], [0.33313979474205757, 0.9428774454610842]], [[0.34497353379459045, 0.9386124125437894], [0.34497353379459045, 0.9386124125437894]], [[0.3567526884142317, 0.9341988649689198], [0.3567526884142317, 0.9341988649689198]], [[0.3684753948102499, 0.9296375010827771], [0.3684753948102499, 0.9296375010827771]], [[0.38013979812359666, 0.924929042620325], [0.38013979812359666, 0.924929042620325]], [[0.3917440527203973, 0.9200742345909914], [0.3917440527203973, 0.9200742345909914]], [[0.4032863224839812, 0.915073845160786], [0.4032863224839812, 0.915073845160786]], [[0.41476478110540693, 0.9099286655307568], [0.41476478110540693, 0.9099286655307568]], [[0.4261776123724353, 0.9046395098117981], [0.4261776123724353, 0.9046395098117981]], [[0.4375230104569043, 0.8992072148958368], [0.4375230104569043, 0.8992072148958368]], [[0.4487991802004621, 0.8936326403234123], [0.4487991802004621, 0.8936326403234123]], [[0.46000433739861096, 0.887916668147673], [0.46000433739861096, 0.887916668147673]], [[0.47113670908301786, 0.8820602027948115], [0.47113670908301786, 0.8820602027948115]], [[0.4821945338020477, 0.8760641709209582], [0.4821945338020477, 0.8760641709209582]], [[0.4931760618994744, 0.8699295212655597], [0.4931760618994744, 0.8699295212655597]], [[0.5040795557913246, 0.8636572245012607], [0.5040795557913246, 0.8636572245012607]], [[0.5149032902408126, 0.8572482730803168], [0.5149032902408126, 0.8572482730803168]], [[0.5256455526313207, 0.8507036810775614], [0.5256455526313207, 0.8507036810775614]], [[0.5363046432373825, 0.8440244840299503], [0.5363046432373825, 0.8440244840299503]], [[0.5468788754936273, 0.8372117387727107], [0.5468788754936273, 0.8372117387727107]], [[0.5573665762616421, 0.8302665232721208], [0.5573665762616421, 0.8302665232721208]], [[0.5677660860947078, 0.8231899364549453], [0.5677660860947078, 0.8231899364549453]], [[0.5780757595003707, 0.8159830980345546], [0.5780757595003707, 0.8159830980345546]], [[0.588293965200805, 0.8086471483337551], [0.588293965200805, 0.8086471483337551]], [[0.5984190863909268, 0.8011832481043575], [0.5984190863909268, 0.8011832481043575]], [[0.608449520994217, 0.7935925783435149], [0.608449520994217, 0.7935925783435149]], [[0.6183836819162153, 0.7858763401068549], [0.6183836819162153, 0.7858763401068549]], [[0.6282199972956423, 0.7780357543184395], [0.6282199972956423, 0.7780357543184395]], [[0.6379569107531118, 0.7700720615775812], [0.6379569107531118, 0.7700720615775812]], [[0.647592881637394, 0.7619865219625451], [0.647592881637394, 0.7619865219625451]], [[0.6571263852691885, 0.7537804148311695], [0.6571263852691885, 0.7537804148311695]], [[0.666555913182372, 0.7454550386184362], [0.666555913182372, 0.7454550386184362]], [[0.675879973362679, 0.7370117106310213], [0.675879973362679, 0.7370117106310213]], [[0.6850970904837809, 0.7284517668388609], [0.6850970904837809, 0.7284517668388609]], [[0.6942058061407225, 0.7197765616637636], [0.6942058061407225, 0.7197765616637636]], [[0.7032046790806838, 0.7109874677651024], [0.7032046790806838, 0.7109874677651024]], [[0.7120922854310254, 0.7020858758226226], [0.7120922854310254, 0.7020858758226226]], [[0.720867218924585, 0.6930731943163971], [0.720867218924585, 0.6930731943163971]], [[0.7295280911221884, 0.6839508493039657], [0.7295280911221884, 0.6839508493039657]], [[0.7380735316323389, 0.6747202841946927], [0.7380735316323389, 0.6747202841946927]], [[0.746502188328052, 0.6653829595213794], [0.746502188328052, 0.6653829595213794]], [[0.7548127275607989, 0.6559403527091677], [0.7548127275607989, 0.6559403527091677]], [[0.7630038343715272, 0.6463939578417693], [0.7630038343715272, 0.6463939578417693]], [[0.7710742126987247, 0.6367452854250606], [0.7710742126987247, 0.6367452854250606]], [[0.7790225855834911, 0.6269958621480786], [0.7790225855834911, 0.6269958621480786]], [[0.7868476953715899, 0.6171472306414553], [0.7868476953715899, 0.6171472306414553]], [[0.7945483039124437, 0.6072009492333317], [0.7945483039124437, 0.6072009492333317]], [[0.8021231927550437, 0.5971585917027863], [0.8021231927550437, 0.5971585917027863]], [[0.809571163340744, 0.5870217470308187], [0.809571163340744, 0.5870217470308187]], [[0.8168910371929053, 0.5767920191489297], [0.8168910371929053, 0.5767920191489297]], [[0.8240816561033644, 0.566471026685334], [0.8240816561033644, 0.566471026685334]], [[0.8311418823156935, 0.5560604027088476], [0.8311418823156935, 0.5560604027088476]], [[0.8380705987052264, 0.545561794470492], [0.8380705987052264, 0.545561794470492]], [[0.8448667089558177, 0.5349768631428518], [0.8448667089558177, 0.5349768631428518]], [[0.8515291377333112, 0.5243072835572319], [0.8515291377333112, 0.5243072835572319]], [[0.8580568308556875, 0.5135547439386516], [0.8580568308556875, 0.5135547439386516]], [[0.8644487554598649, 0.5027209456387218], [0.8644487554598649, 0.5027209456387218]], [[0.8707039001651274, 0.4918076028664418], [0.8707039001651274, 0.4918076028664418]], [[0.8768212752331536, 0.4808164424169648], [0.8768212752331536, 0.4808164424169648]], [[0.8827999127246196, 0.4697492033983709], [0.8827999127246196, 0.4697492033983709]], [[0.8886388666523558, 0.45860763695649104], [0.8886388666523558, 0.45860763695649104]], [[0.8943372131310272, 0.4473935059978269], [0.8943372131310272, 0.4473935059978269]], [[0.8998940505233182, 0.4361085849106111], [0.8998940505233182, 0.4361085849106111]], [[0.9053084995825966, 0.42475465928404793], [0.9053084995825966, 0.42475465928404793]], [[0.9105797035920355, 0.4133335256257842], [0.9105797035920355, 0.4133335256257842]], [[0.9157068285001692, 0.4018469910776512], [0.9157068285001692, 0.4018469910776512]], [[0.920689063052863, 0.3902968731297256], [0.920689063052863, 0.3902968731297256]], [[0.9255256189216778, 0.3786849993327503], [0.9255256189216778, 0.3786849993327503]], [[0.9302157308286042, 0.3670132070089654], [0.9302157308286042, 0.3670132070089654]], [[0.934758656667151, 0.35528334296139374], [0.934758656667151, 0.35528334296139374]], [[0.9391536776197676, 0.34349726318162344], [0.9391536776197676, 0.34349726318162344]], [[0.9434000982715812, 0.3316568325561391], [0.9434000982715812, 0.3316568325561391]], [[0.9474972467204298, 0.31976392457124536], [0.9474972467204298, 0.31976392457124536]], [[0.9514444746831766, 0.30782042101662793], [0.9514444746831766, 0.30782042101662793]], [[0.9552411575982869, 0.2958282116876025], [0.9552411575982869, 0.2958282116876025]], [[0.9588866947246497, 0.28378919408609693], [0.9588866947246497, 0.28378919408609693]], [[0.9623805092366334, 0.27170527312041276], [0.9623805092366334, 0.27170527312041276]], [[0.9657220483153546, 0.25957836080381586], [0.9657220483153546, 0.25957836080381586]], [[0.9689107832361495, 0.24741037595200252], [0.9689107832361495, 0.24741037595200252]], [[0.9719462094522335, 0.23520324387949015], [0.9719462094522335, 0.23520324387949015]], [[0.9748278466745341, 0.2229588960949774], [0.9748278466745341, 0.2229588960949774]], [[0.9775552389476861, 0.21067926999572642], [0.9775552389476861, 0.21067926999572642]], [[0.9801279547221765, 0.19836630856101303], [0.9801279547221765, 0.19836630856101303]], [[0.9825455869226277, 0.18602196004469224], [0.9825455869226277, 0.18602196004469224]], [[0.984807753012208, 0.17364817766693041], [0.984807753012208, 0.17364817766693041]], [[0.98691409505316, 0.16124691930515242], [0.98691409505316, 0.16124691930515242]], [[0.9888642797634357, 0.14882014718424924], [0.9888642797634357, 0.14882014718424924]], [[0.9906579985694317, 0.1363698275661], [0.9906579985694317, 0.1363698275661]], [[0.9922949676548136, 0.12389793043845522], [0.9922949676548136, 0.12389793043845522]], [[0.9937749280054242, 0.11140642920322849], [0.9937749280054242, 0.11140642920322849]], [[0.995097645450266, 0.09889730036424986], [0.995097645450266, 0.09889730036424986]], [[0.9962629106985543, 0.08637252321452853], [0.9962629106985543, 0.08637252321452853]], [[0.9972705393728327, 0.07383407952307214], [0.9972705393728327, 0.07383407952307214]], [[0.9981203720381463, 0.06128395322131638], [0.9981203720381463, 0.06128395322131638]], [[0.9988122742272691, 0.04872413008921228], [0.9988122742272691, 0.04872413008921228]], [[0.9993461364619809, 0.036156597441019206], [0.9993461364619809, 0.036156597441019206]], [[0.9997218742703887, 0.023583343810857166], [0.9997218742703887, 0.023583343810857166]], [[0.9999394282002937, 0.011006358638064812], [0.9999394282002937, 0.011006358638064812]], [[0.9999987638285974, -0.001572368047584414], [0.9999987638285974, -0.001572368047584414]], [[0.9998998717667489, -0.014150845940761853], [0.9998998717667489, -0.014150845940761853]], [[0.9996427676622299, -0.026727084775504745], [0.9996427676622299, -0.026727084775504745]], [[0.9992274921960794, -0.03929909464013115], [0.9992274921960794, -0.03929909464013115]], [[0.9986541110764565, -0.0518648862921008], [0.9986541110764565, -0.0518648862921008]], [[0.9979227150282433, -0.06442247147276806], [0.9979227150282433, -0.06442247147276806]], [[0.9970334197786902, -0.07696986322197923], [0.9970334197786902, -0.07696986322197923]], [[0.9959863660391044, -0.08950507619246638], [0.9959863660391044, -0.08950507619246638]], [[0.9947817194825853, -0.10202612696398403], [0.9947817194825853, -0.10202612696398403]], [[0.9934196707178107, -0.11453103435714077], [0.9934196707178107, -0.11453103435714077]], [[0.991900435258877, -0.12701781974687854], [0.991900435258877, -0.12701781974687854]], [[0.9902242534911986, -0.1394845073755453], [0.9902242534911986, -0.1394845073755453]], [[0.9883913906334728, -0.15192912466551547], [0.9883913906334728, -0.15192912466551547]], [[0.9864021366957146, -0.16434970253130593], [0.9864021366957146, -0.16434970253130593]], [[0.9842568064333687, -0.17674427569114137], [0.9842568064333687, -0.17674427569114137]], [[0.9819557392975067, -0.18911088297791617], [0.9819557392975067, -0.18911088297791617]], [[0.9794992993811165, -0.20144756764950503], [0.9794992993811165, -0.20144756764950503]], [[0.9768878753614926, -0.21375237769837538], [0.9768878753614926, -0.21375237769837538]], [[0.9741218804387363, -0.22602336616044894], [0.9741218804387363, -0.22602336616044894]], [[0.9712017522703763, -0.23825859142316483], [0.9712017522703763, -0.23825859142316483]], [[0.9681279529021188, -0.25045611753269825], [0.9681279529021188, -0.25045611753269825]], [[0.9649009686947391, -0.2626140145002818], [0.9649009686947391, -0.2626140145002818]], [[0.9615213102471255, -0.27473035860758266], [0.9615213102471255, -0.27473035860758266]], [[0.9579895123154889, -0.28680323271109], [0.9579895123154889, -0.28680323271109]], [[0.9543061337287488, -0.29883072654545967], [0.9543061337287488, -0.29883072654545967]], [[0.9504717573001116, -0.310810937025771], [0.9504717573001116, -0.310810937025771]], [[0.9464869897348526, -0.32274196854864906], [0.9464869897348526, -0.32274196854864906]], [[0.9423524615343186, -0.33462193329220136], [0.9423524615343186, -0.33462193329220136]], [[0.9380688268961659, -0.3464489515147234], [0.9380688268961659, -0.3464489515147234]], [[0.9336367636108462, -0.3582211518521272], [0.9336367636108462, -0.3582211518521272]], [[0.9290569729543628, -0.369936671614043], [0.9290569729543628, -0.369936671614043]], [[0.9243301795773085, -0.38159365707854837], [0.9243301795773085, -0.38159365707854837]], [[0.9194571313902055, -0.3931902637854787], [0.9194571313902055, -0.3931902637854787]], [[0.9144385994451658, -0.40472465682827324], [0.9144385994451658, -0.40472465682827324]], [[0.9092753778138886, -0.4161950111443075], [0.9092753778138886, -0.4161950111443075]], [[0.9039682834620162, -0.42759951180366895], [0.9039682834620162, -0.42759951180366895]], [[0.8985181561198674, -0.4389363542963303], [0.8985181561198674, -0.4389363542963303]], [[0.8929258581495686, -0.450203744817673], [0.8929258581495686, -0.450203744817673]], [[0.8871922744086043, -0.46139990055231683], [0.8871922744086043, -0.46139990055231683]], [[0.881318312109807, -0.47252304995621186], [0.881318312109807, -0.47252304995621186]], [[0.8753049006778131, -0.4835714330369443], [0.8753049006778131, -0.4835714330369443]], [[0.869152991601999, -0.4945433016322186], [0.869152991601999, -0.4945433016322186]], [[0.8628635582859312, -0.5054369196864643], [0.8628635582859312, -0.5054369196864643]], [[0.856437595893346, -0.5162505635255284], [0.856437595893346, -0.5162505635255284]], [[0.8498761211906867, -0.5269825221294092], [0.8498761211906867, -0.5269825221294092]], [[0.8431801723862224, -0.5376310974029872], [0.8431801723862224, -0.5376310974029872]], [[0.8363508089657762, -0.5481946044447097], [0.8363508089657762, -0.5481946044447097]], [[0.8293891115250829, -0.5586713718131919], [0.8293891115250829, -0.5586713718131919]], [[0.8222961815988096, -0.5690597417916836], [0.8222961815988096, -0.5690597417916836]], [[0.8150731414862624, -0.5793580706503667], [0.8150731414862624, -0.5793580706503667]], [[0.8077211340738071, -0.5895647289064391], [0.8077211340738071, -0.5895647289064391]], [[0.800241322654032, -0.5996781015819448], [0.800241322654032, -0.5996781015819448]], [[0.7926348907416848, -0.6096965884593069], [0.7926348907416848, -0.6096965884593069]], [[0.7849030418864046, -0.6196186043345285], [0.7849030418864046, -0.6196186043345285]], [[0.7770469994822886, -0.6294425792680156], [0.7770469994822886, -0.6294425792680156]], [[0.769068006574317, -0.6391669588329847], [0.769068006574317, -0.6391669588329847]], [[0.7609673256616678, -0.648790204361417], [0.7609673256616678, -0.648790204361417]], [[0.7527462384979551, -0.6583107931875185], [0.7527462384979551, -0.6583107931875185]], [[0.744406045888419, -0.6677272188886485], [0.744406045888419, -0.6677272188886485]], [[0.7359480674841035, -0.6770379915236763], [0.7359480674841035, -0.6770379915236763]], [[0.7273736415730488, -0.6862416378687335], [0.7273736415730488, -0.6862416378687335]], [[0.7186841248685385, -0.6953367016503177], [0.7186841248685385, -0.6953367016503177]], [[0.7098808922944289, -0.7043217437757161], [0.7098808922944289, -0.7043217437757161]], [[0.7009653367675978, -0.7131953425607098], [0.7009653367675978, -0.7131953425607098]], [[0.6919388689775463, -0.7219560939545244], [0.6919388689775463, -0.7219560939545244]], [[0.6828029171631891, -0.7306026117619886], [0.6828029171631891, -0.7306026117619886]], [[0.673558926886866, -0.739133527862871], [0.673558926886866, -0.739133527862871]], [[0.6642083608056142, -0.7475474924283534], [0.6642083608056142, -0.7475474924283534]], [[0.6547526984397353, -0.7558431741346118], [0.6547526984397353, -0.7558431741346118]], [[0.6451934359386937, -0.764019260373469], [0.6451934359386937, -0.764019260373469]], [[0.6355320858443845, -0.7720744574600859], [0.6355320858443845, -0.7720744574600859]], [[0.6257701768518059, -0.7800074908376582], [0.6257701768518059, -0.7800074908376582]], [[0.6159092535671797, -0.7878171052790867], [0.6159092535671797, -0.7878171052790867]], [[0.6059508762635484, -0.7955020650855897], [0.6059508762635484, -0.7955020650855897]], [[0.5958966206338979, -0.8030611542822255], [0.5958966206338979, -0.8030611542822255]], [[0.5857480775418397, -0.8104931768102919], [0.5857480775418397, -0.8104931768102919]], [[0.5755068527698903, -0.8177969567165775], [0.5755068527698903, -0.8177969567165775]], [[0.5651745667653929, -0.8249713383394301], [0.5651745667653929, -0.8249713383394301]], [[0.5547528543841173, -0.8320151864916135], [0.5547528543841173, -0.8320151864916135]], [[0.5442433646315792, -0.8389273866399272], [0.5442433646315792, -0.8389273866399272]], [[0.5336477604021226, -0.8457068450815559], [0.5336477604021226, -0.8457068450815559]], [[0.5229677182158028, -0.8523524891171238], [0.5229677182158028, -0.8523524891171238]], [[0.5122049279531147, -0.8588632672204258], [0.5122049279531147, -0.8588632672204258]], [[0.5013610925876063, -0.865238149204808], [0.5013610925876063, -0.865238149204808]], [[0.49043792791642066, -0.8714761263861723], [0.49043792791642066, -0.8714761263861723]], [[0.47943716228880995, -0.8775762117425775], [0.47943716228880995, -0.8775762117425775]], [[0.4683605363326608, -0.8835374400704151], [0.4683605363326608, -0.8835374400704151]], [[0.4572098026790794, -0.8893588681371302], [0.4572098026790794, -0.8893588681371302]], [[0.44598672568507636, -0.8950395748304677], [0.44598672568507636, -0.8950395748304677]], [[0.4346930811543961, -0.9005786613042182], [0.4346930811543961, -0.9005786613042182]], [[0.4233306560565345, -0.9059752511204399], [0.4233306560565345, -0.9059752511204399]], [[0.4119012482439928, -0.9112284903881356], [0.4119012482439928, -0.9112284903881356]], [[0.40040666616780407, -0.916337547898363], [0.40040666616780407, -0.916337547898363]], [[0.3888487285913878, -0.9213016152557539], [0.3888487285913878, -0.9213016152557539]], [[0.37722926430277026, -0.9261199070064258], [0.37722926430277026, -0.9261199070064258]], [[0.36555011182521946, -0.9307916607622618], [0.36555011182521946, -0.9307916607622618]], [[0.3538131191263388, -0.9353161373215428], [0.3538131191263388, -0.9353161373215428]], [[0.3420201433256689, -0.9396926207859083], [0.3420201433256689, -0.9396926207859083]], [[0.330173050400837, -0.9439204186736329], [0.330173050400837, -0.9439204186736329]], [[0.3182737148923088, -0.9479988620291954], [0.3182737148923088, -0.9479988620291954]], [[0.3063240196067838, -0.9519273055291264], [0.3063240196067838, -0.9519273055291264]], [[0.29432585531928224, -0.9557051275841167], [0.29432585531928224, -0.9557051275841167]], [[0.2822811204739722, -0.9593317304373701], [0.2822811204739722, -0.9593317304373701]], [[0.27019172088378224, -0.9628065402591843], [0.27019172088378224, -0.9628065402591843]], [[0.25805956942885044, -0.9661290072377479], [0.25805956942885044, -0.9661290072377479]], [[0.24588658575385056, -0.9692986056661355], [0.24588658575385056, -0.9692986056661355]], [[0.23367469596425278, -0.9723148340254889], [0.23367469596425278, -0.9723148340254889]], [[0.22142583232155955, -0.975177215064372], [0.22142583232155955, -0.975177215064372]], [[0.20914193293756786, -0.977885295874285], [0.20914193293756786, -0.977885295874285]], [[0.19682494146770554, -0.9804386479613267], [0.19682494146770554, -0.9804386479613267]], [[0.18447680680349254, -0.9828368673139948], [0.18447680680349254, -0.9828368673139948]], [[0.17209948276416928, -0.9850795744671115], [0.17209948276416928, -0.9850795744671115]], [[0.15969492778754976, -0.9871664145618657], [0.15969492778754976, -0.9871664145618657]], [[0.14726510462014156, -0.9890970574019613], [0.14726510462014156, -0.9890970574019613]], [[0.1348119800065847, -0.9908711975058636], [0.1348119800065847, -0.9908711975058636]], [[0.12233752437845731, -0.992488554155135], [0.12233752437845731, -0.992488554155135]], [[0.1098437115425002, -0.9939488714388522], [0.1098437115425002, -0.9939488714388522]], [[0.09733251836830287, -0.9952519182940991], [0.09733251836830287, -0.9952519182940991]], [[0.0848059244755095, -0.9963974885425265], [0.0848059244755095, -0.9963974885425265]], [[0.07226591192058739, -0.9973854009229761], [0.07226591192058739, -0.9973854009229761]], [[0.05971446488321034, -0.9982154991201608], [0.05971446488321034, -0.9982154991201608]], [[0.04715356935230619, -0.9988876517893978], [0.04715356935230619, -0.9988876517893978]], [[0.034585212811817465, -0.9994017525773913], [0.034585212811817465, -0.9994017525773913]], [[0.022011383926227784, -0.9997577201390606], [0.022011383926227784, -0.9997577201390606]], [[0.009434072225897046, -0.999955498150411], [0.009434072225897046, -0.999955498150411]], [[-0.0031447322077359985, -0.9999950553174459], [-0.0031447322077359985, -0.9999950553174459]], [[-0.015723039057040564, -0.9998763853811183], [-0.015723039057040564, -0.9998763853811183]], [[-0.02829885808311759, -0.9995995071183217], [-0.02829885808311759, -0.9995995071183217]], [[-0.04087019944071145, -0.9991644643389178], [-0.04087019944071145, -0.9991644643389178]], [[-0.053435073993057226, -0.9985713258788059], [-0.053435073993057226, -0.9985713258788059]], [[-0.06599149362662023, -0.9978201855890307], [-0.06599149362662023, -0.9978201855890307]], [[-0.07853747156566927, -0.996911162320932], [-0.07853747156566927, -0.996911162320932]], [[-0.09107102268664041, -0.9958443999073396], [-0.09107102268664041, -0.9958443999073396]], [[-0.10359016383223883, -0.9946200671398149], [-0.10359016383223883, -0.9946200671398149]], [[-0.11609291412522968, -0.993238357741943], [-0.11609291412522968, -0.993238357741943]], [[-0.12857729528186848, -0.9916994903386808], [-0.12857729528186848, -0.9916994903386808]], [[-0.14104133192491908, -0.9900037084217639], [-0.14104133192491908, -0.9900037084217639]], [[-0.15348305189621594, -0.9881512803111796], [-0.15348305189621594, -0.9881512803111796]], [[-0.16590048656871298, -0.9861424991127116], [-0.16590048656871298, -0.9861424991127116]], [[-0.1782916711579755, -0.9839776826715616], [-0.1782916711579755, -0.9839776826715616]], [[-0.19065464503306404, -0.9816571735220583], [-0.19065464503306404, -0.9816571735220583]], [[-0.20298745202676116, -0.979181338833458], [-0.20298745202676116, -0.979181338833458]], [[-0.2152881407450901, -0.9765505703518493], [-0.2152881407450901, -0.9765505703518493]], [[-0.2275547648760821, -0.9737652843381669], [-0.2275547648760821, -0.9737652843381669]], [[-0.23978538349773562, -0.9708259215023277], [-0.23978538349773562, -0.9708259215023277]], [[-0.25197806138512474, -0.967732946933499], [-0.25197806138512474, -0.967732946933499]], [[-0.2641308693166058, -0.9644868500265071], [-0.2641308693166058, -0.9644868500265071]], [[-0.2762418843790738, -0.9610881444044029], [-0.2762418843790738, -0.9610881444044029]], [[-0.2883091902722216, -0.9575373678371909], [-0.2883091902722216, -0.9575373678371909]], [[-0.3003308776117502, -0.9538350821567405], [-0.3003308776117502, -0.9538350821567405]], [[-0.31230504423148914, -0.9499818731678872], [-0.31230504423148914, -0.9499818731678872]], [[-0.32422979548437053, -0.9459783505557425], [-0.32422979548437053, -0.9459783505557425]], [[-0.33610324454221563, -0.9418251477892251], [-0.33610324454221563, -0.9418251477892251]], [[-0.34792351269428334, -0.9375229220208277], [-0.34792351269428334, -0.9375229220208277]], [[-0.3596887296445355, -0.9330723539826374], [-0.3596887296445355, -0.9330723539826374]], [[-0.3713970338075679, -0.9284741478786258], [-0.3713970338075679, -0.9284741478786258]], [[-0.3830465726031674, -0.9237290312732227], [-0.3830465726031674, -0.9237290312732227]], [[-0.3946355027494405, -0.9188377549761962], [-0.3946355027494405, -0.9188377549761962]], [[-0.406161990554472, -0.9138010929238535], [-0.406161990554472, -0.9138010929238535]], [[-0.41762421220646645, -0.9086198420565822], [-0.41762421220646645, -0.9086198420565822]], [[-0.4290203540623263, -0.9032948221927524], [-0.4290203540623263, -0.9032948221927524]], [[-0.44034861293461913, -0.8978268758989992], [-0.44034861293461913, -0.8978268758989992]], [[-0.4516071963768948, -0.892216868356904], [-0.4516071963768948, -0.892216868356904]], [[-0.46279432296729867, -0.8864656872260989], [-0.46279432296729867, -0.8864656872260989]], [[-0.47390822259044274, -0.8805742425038149], [-0.47390822259044274, -0.8805742425038149]], [[-0.4849471367174873, -0.8745434663808944], [-0.4849471367174873, -0.8745434663808944]], [[-0.495909318684389, -0.8683743130942929], [-0.495909318684389, -0.8683743130942929]], [[-0.5067930339682724, -0.8620677587760915], [-0.5067930339682724, -0.8620677587760915]], [[-0.5175965604618782, -0.8556248012990468], [-0.5175965604618782, -0.8556248012990468]], [[-0.5283181887460511, -0.849046460118698], [-0.5283181887460511, -0.849046460118698]], [[-0.538956222360216, -0.842333776112062], [-0.538956222360216, -0.842333776112062]], [[-0.5495089780708056, -0.8354878114129367], [-0.5495089780708056, -0.8354878114129367]], [[-0.5599747861375949, -0.8285096492438424], [-0.5599747861375949, -0.8285096492438424]], [[-0.5703519905779012, -0.8214003937446254], [-0.5703519905779012, -0.8214003937446254]], [[-0.5806389494286053, -0.814161169797753], [-0.5806389494286053, -0.814161169797753]], [[-0.5908340350059578, -0.8067931228503245], [-0.5908340350059578, -0.8067931228503245]], [[-0.6009356341631226, -0.7992974187328304], [-0.6009356341631226, -0.7992974187328304]], [[-0.6109421485454225, -0.7916752434746857], [-0.6109421485454225, -0.7916752434746857]], [[-0.6208519948432432, -0.7839278031165661], [-0.6208519948432432, -0.7839278031165661]], [[-0.630663605042557, -0.7760563235195791], [-0.630663605042557, -0.7760563235195791]], [[-0.6403754266730258, -0.7680620501712998], [-0.6403754266730258, -0.7680620501712998]], [[-0.6499859230536464, -0.7599462479886977], [-0.6499859230536464, -0.7599462479886977]], [[-0.6594935735358957, -0.7517102011179935], [-0.6594935735358957, -0.7517102011179935]], [[-0.6688968737443391, -0.7433552127314704], [-0.6688968737443391, -0.7433552127314704]], [[-0.6781943358146659, -0.7348826048212762], [-0.6781943358146659, -0.7348826048212762]], [[-0.6873844886291098, -0.7262937179902474], [-0.6873844886291098, -0.7262937179902474]], [[-0.6964658780492216, -0.717589911239788], [-0.6964658780492216, -0.717589911239788]], [[-0.7054370671459529, -0.7087725617548385], [-0.7054370671459529, -0.7087725617548385]], [[-0.7142966364270207, -0.6998430646859656], [-0.7142966364270207, -0.6998430646859656]], [[-0.723043184061509, -0.6908028329286112], [-0.723043184061509, -0.6908028329286112]], [[-0.731675326101678, -0.6816532968995332], [-0.731675326101678, -0.6816532968995332]], [[-0.7401916967019432, -0.6723959043104729], [-0.7401916967019432, -0.6723959043104729]], [[-0.7485909483349904, -0.6630321199390868], [-0.7485909483349904, -0.6630321199390868]], [[-0.7568717520049916, -0.6535634253971795], [-0.7568717520049916, -0.6535634253971795]], [[-0.7650327974578898, -0.6439913188962686], [-0.7650327974578898, -0.6439913188962686]], [[-0.7730727933887175, -0.634317315010528], [-0.7730727933887175, -0.634317315010528]], [[-0.7809904676459172, -0.6245429444371393], [-0.7809904676459172, -0.6245429444371393]], [[-0.788784567432631, -0.6146697537540928], [-0.788784567432631, -0.6146697537540928]], [[-0.7964538595049286, -0.6046993051754759], [-0.7964538595049286, -0.6046993051754759]], [[-0.8039971303669401, -0.5946331763042871], [-0.8039971303669401, -0.5946331763042871]], [[-0.8114131864628653, -0.5844729598828156], [-0.8114131864628653, -0.5844729598828156]], [[-0.8187008543658276, -0.5742202635406243], [-0.8187008543658276, -0.5742202635406243]], [[-0.825858980963543, -0.5638767095401779], [-0.825858980963543, -0.5638767095401779]], [[-0.8328864336407734, -0.5534439345201586], [-0.8328864336407734, -0.5534439345201586]], [[-0.8397821004585396, -0.5429235892364995], [-0.8397821004585396, -0.5429235892364995]], [[-0.8465448903300604, -0.5323173383011922], [-0.8465448903300604, -0.5323173383011922]], [[-0.8531737331933926, -0.521626859918898], [-0.8531737331933926, -0.521626859918898]], [[-0.8596675801807451, -0.5108538456214089], [-0.8596675801807451, -0.5108538456214089]], [[-0.8660254037844384, -0.5000000000000004], [-0.8660254037844384, -0.5000000000000004]], [[-0.872246198019486, -0.4890670404357173], [-0.872246198019486, -0.4890670404357173]], [[-0.8783289785827684, -0.4780566968276366], [-0.8783289785827684, -0.4780566968276366]], [[-0.8842727830087774, -0.46697071131914863], [-0.8842727830087774, -0.46697071131914863]], [[-0.8900766708219056, -0.4558108380223019], [-0.8900766708219056, -0.4558108380223019]], [[-0.895739723685255, -0.4445788427402534], [-0.895739723685255, -0.4445788427402534]], [[-0.9012610455459443, -0.4332765026878693], [-0.9012610455459443, -0.4332765026878693]], [[-0.9066397627768893, -0.4219056062105194], [-0.9066397627768893, -0.4219056062105194]], [[-0.9118750243150336, -0.410467952501114], [-0.9118750243150336, -0.410467952501114]], [[-0.9169660017960133, -0.39896535131541655], [-0.9169660017960133, -0.39896535131541655]], [[-0.921911889685225, -0.38739962268569333], [-0.921911889685225, -0.38739962268569333]], [[-0.9267119054052849, -0.37577259663273255], [-0.9267119054052849, -0.37577259663273255]], [[-0.931365289459854, -0.3640861128762842], [-0.931365289459854, -0.3640861128762842]], [[-0.9358713055538119, -0.3523420205439648], [-0.9358713055538119, -0.3523420205439648]], [[-0.9402292407097588, -0.3405421778786742], [-0.9402292407097588, -0.3405421778786742]], [[-0.9444384053808287, -0.32868845194456947], [-0.9444384053808287, -0.32868845194456947]], [[-0.948498133559795, -0.3167827183316434], [-0.948498133559795, -0.3167827183316434]], [[-0.9524077828844512, -0.30482686085895394], [-0.9524077828844512, -0.30482686085895394]], [[-0.9561667347392507, -0.2928227712765512], [-0.9561667347392507, -0.2928227712765512]], [[-0.959774394353189, -0.28077234896614933], [-0.959774394353189, -0.28077234896614933]], [[-0.9632301908939126, -0.26867750064059465], [-0.9632301908939126, -0.26867750064059465]], [[-0.9665335775580413, -0.25654014004216524], [-0.9665335775580413, -0.25654014004216524]], [[-0.9696840316576876, -0.2443621876397672], [-0.9696840316576876, -0.2443621876397672]], [[-0.97268105470316, -0.2321455703250619], [-0.97268105470316, -0.2321455703250619]], [[-0.9755241724818386, -0.21989222110757806], [-0.9755241724818386, -0.21989222110757806]], [[-0.9782129351332083, -0.2076040788088557], [-0.9782129351332083, -0.2076040788088557]], [[-0.9807469172200395, -0.19528308775567055], [-0.9807469172200395, -0.19528308775567055]], [[-0.9831257177957041, -0.18293119747238726], [-0.9831257177957041, -0.18293119747238726]], [[-0.9853489604676163, -0.17055036237249038], [-0.9853489604676163, -0.17055036237249038]], [[-0.9874162934567888, -0.15814254144934156], [-0.9874162934567888, -0.15814254144934156]], [[-0.9893273896534934, -0.14570969796621222], [-0.9893273896534934, -0.14570969796621222]], [[-0.9910819466690195, -0.1332537991456406], [-0.9910819466690195, -0.1332537991456406]], [[-0.9926796868835203, -0.1207768158581612], [-0.9926796868835203, -0.1207768158581612]], [[-0.9941203574899392, -0.10828072231046196], [-0.9941203574899392, -0.10828072231046196]], [[-0.9954037305340125, -0.09576749573300417], [-0.9954037305340125, -0.09576749573300417]], [[-0.9965296029503367, -0.08323911606717305], [-0.9965296029503367, -0.08323911606717305]], [[-0.9974977965944997, -0.070697565651995], [-0.9974977965944997, -0.070697565651995]], [[-0.9983081582712682, -0.05814482891047624], [-0.9983081582712682, -0.05814482891047624]], [[-0.9989605597588274, -0.04558289203561173], [-0.9989605597588274, -0.04558289203561173]], [[-0.9994548978290693, -0.0330137426761141], [-0.9994548978290693, -0.0330137426761141]], [[-0.9997910942639261, -0.020439369621912166], [-0.9997910942639261, -0.020439369621912166]], [[-0.9999690958677468, -0.007861762489468911], [-0.9999690958677468, -0.007861762489468911]], [[-0.999988874475714, 0.004717088593031313], [-0.999988874475714, 0.004717088593031313]], [[-0.9998504269583004, 0.01729519330057657], [-0.9998504269583004, 0.01729519330057657]], [[-0.9995537752217639, 0.029870561426252256], [-0.9995537752217639, 0.029870561426252256]], [[-0.9990989662046815, 0.04244120319614822], [-0.9990989662046815, 0.04244120319614822]], [[-0.9984860718705224, 0.055005129584192916], [-0.9984860718705224, 0.055005129584192916]], [[-0.9977151891962615, 0.06756035262687816], [-0.9977151891962615, 0.06756035262687816]], [[-0.9967864401570343, 0.08010488573780679], [-0.9967864401570343, 0.08010488573780679]], [[-0.9956999717068378, 0.09263674402202696], [-0.9956999717068378, 0.09263674402202696]], [[-0.9944559557552776, 0.10515394459009784], [-0.9944559557552776, 0.10515394459009784]], [[-0.9930545891403677, 0.11765450687183807], [-0.9930545891403677, 0.11765450687183807]], [[-0.9914960935973849, 0.1301364529297071], [-0.9914960935973849, 0.1301364529297071]], [[-0.9897807157237836, 0.1425978077717702], [-0.9897807157237836, 0.1425978077717702]], [[-0.9879087269401782, 0.1550365996641971], [-0.9879087269401782, 0.1550365996641971]], [[-0.9858804234473959, 0.16745086044324545], [-0.9858804234473959, 0.16745086044324545]], [[-0.9836961261796103, 0.17983862582667898], [-0.9836961261796103, 0.17983862582667898]], [[-0.9813561807535597, 0.19219793572457194], [-0.9813561807535597, 0.19219793572457194]], [[-0.9788609574138615, 0.20452683454945075], [-0.9788609574138615, 0.20452683454945075]], [[-0.9762108509744296, 0.21682337152571898], [-0.9762108509744296, 0.21682337152571898]], [[-0.9734062807560028, 0.22908560099832972], [-0.9734062807560028, 0.22908560099832972]], [[-0.9704476905197971, 0.24131158274063894], [-0.9704476905197971, 0.24131158274063894]], [[-0.9673355483972903, 0.25349938226140434], [-0.9673355483972903, 0.25349938226140434]], [[-0.9640703468161508, 0.2656470711108758], [-0.9640703468161508, 0.2656470711108758]], [[-0.9606526024223212, 0.27775272718593], [-0.9606526024223212, 0.27775272718593]], [[-0.957082855998271, 0.28981443503420057], [-0.957082855998271, 0.28981443503420057]], [[-0.9533616723774295, 0.30183028615715607], [-0.9533616723774295, 0.30183028615715607]], [[-0.9494896403548136, 0.31379837931207794], [-0.9494896403548136, 0.31379837931207794]], [[-0.9454673725938637, 0.3257168208128897], [-0.9454673725938637, 0.3257168208128897]], [[-0.9412955055295036, 0.33758372482979143], [-0.9412955055295036, 0.33758372482979143]], [[-0.9369746992674384, 0.34939721368765], [-0.9369746992674384, 0.34939721368765]], [[-0.9325056374797075, 0.361155418163101], [-0.9325056374797075, 0.361155418163101]], [[-0.9278890272965095, 0.3728564777803084], [-0.9278890272965095, 0.3728564777803084]], [[-0.9231255991943125, 0.3844985411053488], [-0.9231255991943125, 0.3844985411053488]], [[-0.9182161068802741, 0.3960797660391565], [-0.9182161068802741, 0.3960797660391565]], [[-0.9131613271729835, 0.4075983201089958], [-0.9131613271729835, 0.4075983201089958]], [[-0.9079620598795464, 0.41905238075840945], [-0.9079620598795464, 0.41905238075840945]], [[-0.9026191276690343, 0.4304401356355976], [-0.9026191276690343, 0.4304401356355976]], [[-0.8971333759423143, 0.4417597828801825], [-0.8971333759423143, 0.4417597828801825]], [[-0.8915056726982842, 0.4530095314083134], [-0.8915056726982842, 0.4530095314083134]], [[-0.8857369083965297, 0.4641876011960654], [-0.8857369083965297, 0.4641876011960654]], [[-0.8798279958164298, 0.4752922235610892], [-0.8798279958164298, 0.4752922235610892]], [[-0.873779869912729, 0.486321641442466], [-0.873779869912729, 0.486321641442466]], [[-0.8675934876676018, 0.49727410967872326], [-0.8675934876676018, 0.49727410967872326]], [[-0.8612698279392309, 0.5081478952839691], [-0.8612698279392309, 0.5081478952839691]], [[-0.8548098913069261, 0.5189412777220956], [-0.8548098913069261, 0.5189412777220956]], [[-0.8482146999128025, 0.5296525491790203], [-0.8482146999128025, 0.5296525491790203]], [[-0.8414852973000504, 0.5402800148329067], [-0.8414852973000504, 0.5402800148329067]], [[-0.8346227482478176, 0.5508219931223336], [-0.8346227482478176, 0.5508219931223336]], [[-0.8276281386027314, 0.5612768160123647], [-0.8276281386027314, 0.5612768160123647]], [[-0.8205025751070878, 0.5716428292584782], [-0.8205025751070878, 0.5716428292584782]], [[-0.8132471852237334, 0.5819183926683146], [-0.8132471852237334, 0.5819183926683146]], [[-0.8058631169576695, 0.5921018803612005], [-0.8058631169576695, 0.5921018803612005]], [[-0.7983515386744064, 0.6021916810254089], [-0.7983515386744064, 0.6021916810254089]], [[-0.7907136389150943, 0.6121861981731129], [-0.7907136389150943, 0.6121861981731129]], [[-0.7829506262084637, 0.6220838503929953], [-0.7829506262084637, 0.6220838503929953]], [[-0.7750637288796017, 0.6318830716004721], [-0.7750637288796017, 0.6318830716004721]], [[-0.7670541948555989, 0.6415823112854881], [-0.7670541948555989, 0.6415823112854881]], [[-0.7589232914680891, 0.6511800347578556], [-0.7589232914680891, 0.6511800347578556]], [[-0.7506723052527245, 0.6606747233900812], [-0.7506723052527245, 0.6606747233900812]], [[-0.7423025417456096, 0.670064874857657], [-0.7423025417456096, 0.670064874857657]], [[-0.7338153252767281, 0.6793490033767694], [-0.7338153252767281, 0.6793490033767694]], [[-0.7252119987603977, 0.6885256399393918], [-0.7252119987603977, 0.6885256399393918]], [[-0.7164939234827836, 0.6975933325457224], [-0.7164939234827836, 0.6975933325457224]], [[-0.7076624788865049, 0.706550646433932], [-0.7076624788865049, 0.706550646433932]], [[-0.698719062352368, 0.7153961643071813], [-0.698719062352368, 0.7153961643071813]], [[-0.6896650889782625, 0.7241284865578796], [-0.6896650889782625, 0.7241284865578796]], [[-0.6805019913552531, 0.7327462314891391], [-0.6805019913552531, 0.7327462314891391]], [[-0.6712312193409035, 0.7412480355333995], [-0.6712312193409035, 0.7412480355333995]], [[-0.6618542398298681, 0.7496325534681825], [-0.6618542398298681, 0.7496325534681825]], [[-0.6523725365217912, 0.7578984586289408], [-0.6523725365217912, 0.7578984586289408]], [[-0.6427876096865396, 0.7660444431189778], [-0.6427876096865396, 0.7660444431189778]], [[-0.6331009759268216, 0.7740692180163904], [-0.6331009759268216, 0.7740692180163904]], [[-0.623314167938217, 0.7819715135780128], [-0.623314167938217, 0.7819715135780128]], [[-0.6134287342666622, 0.7897500794403256], [-0.6134287342666622, 0.7897500794403256]], [[-0.6034462390634266, 0.7974036848172986], [-0.6034462390634266, 0.7974036848172986]], [[-0.5933682618376209, 0.8049311186951345], [-0.5933682618376209, 0.8049311186951345]], [[-0.5831963972062739, 0.8123311900238854], [-0.5831963972062739, 0.8123311900238854]], [[-0.5729322546420206, 0.819602727905911], [-0.5729322546420206, 0.819602727905911]], [[-0.5625774582184379, 0.826744581781146], [-0.5625774582184379, 0.826744581781146]], [[-0.552133646353071, 0.8337556216091511], [-0.552133646353071, 0.8337556216091511]], [[-0.541602471548191, 0.8406347380479176], [-0.541602471548191, 0.8406347380479176]], [[-0.5309856001293205, 0.8473808426293961], [-0.5309856001293205, 0.8473808426293961]], [[-0.5202847119815792, 0.8539928679317206], [-0.5202847119815792, 0.8539928679317206]], [[-0.5095015002838734, 0.8604697677481075], [-0.5095015002838734, 0.8604697677481075]], [[-0.4986376712409919, 0.8668105172523927], [-0.4986376712409919, 0.8668105172523927]], [[-0.487694943813635, 0.8730141131611879], [-0.487694943813635, 0.8730141131611879]], [[-0.47667504944642797, 0.8790795738926286], [-0.47667504944642797, 0.8790795738926286]], [[-0.4655797317939577, 0.8850059397216871], [-0.4655797317939577, 0.8850059397216871]], [[-0.45441074644487806, 0.890792272932028], [-0.45441074644487806, 0.890792272932028]], [[-0.4431698606441268, 0.8964376579643814], [-0.4431698606441268, 0.8964376579643814]], [[-0.4318588530132981, 0.9019412015614092], [-0.4318588530132981, 0.9019412015614092]], [[-0.4204795132692152, 0.907302032909044], [-0.4204795132692152, 0.907302032909044]], [[-0.4090336419407468, 0.9125193037742757], [-0.4090336419407468, 0.9125193037742757]], [[-0.3975230500839139, 0.9175921886393661], [-0.3975230500839139, 0.9175921886393661]], [[-0.38594955899532896, 0.9225198848324686], [-0.38594955899532896, 0.9225198848324686]], [[-0.3743149999240192, 0.9273016126546322], [-0.3743149999240192, 0.9273016126546322]], [[-0.3626212137816673, 0.9319366155031737], [-0.3626212137816673, 0.9319366155031737]], [[-0.35087005085133094, 0.9364241599913922], [-0.35087005085133094, 0.9364241599913922]], [[-0.3390633704946757, 0.9407635360646108], [-0.3390633704946757, 0.9407635360646108]], [[-0.3272030408577722, 0.9449540571125281], [-0.3272030408577722, 0.9449540571125281]], [[-0.3152909385755031, 0.9489950600778585], [-0.3152909385755031, 0.9489950600778585]], [[-0.3033289484746273, 0.9528859055612465], [-0.3033289484746273, 0.9528859055612465]], [[-0.29131896327554796, 0.9566259779224375], [-0.29131896327554796, 0.9566259779224375]], [[-0.2792628832928309, 0.9602146853776892], [-0.2792628832928309, 0.9602146853776892]], [[-0.26716261613452225, 0.9636514600934084], [-0.26716261613452225, 0.9636514600934084]], [[-0.25502007640031144, 0.9669357582759981], [-0.25502007640031144, 0.9669357582759981]], [[-0.24283718537858734, 0.9700670602579007], [-0.24283718537858734, 0.9700670602579007]], [[-0.23061587074244044, 0.9730448705798238], [-0.23061587074244044, 0.9730448705798238]], [[-0.21835806624464577, 0.975868718069136], [-0.21835806624464577, 0.975868718069136]], [[-0.20606571141169297, 0.9785381559144195], [-0.20606571141169297, 0.9785381559144195]], [[-0.19374075123689813, 0.981052761736168], [-0.19374075123689813, 0.981052761736168]], [[-0.18138513587265162, 0.9834121376536186], [-0.18138513587265162, 0.9834121376536186]], [[-0.16900082032184968, 0.9856159103477083], [-0.16900082032184968, 0.9856159103477083]], [[-0.15658976412855838, 0.9876637311201432], [-0.15658976412855838, 0.9876637311201432]], [[-0.14415393106795907, 0.9895552759485718], [-0.14415393106795907, 0.9895552759485718]], [[-0.13169528883562445, 0.9912902455378553], [-0.13169528883562445, 0.9912902455378553]], [[-0.11921580873617425, 0.9928683653674237], [-0.11921580873617425, 0.9928683653674237]], [[-0.10671746537135988, 0.9942893857347128], [-0.10671746537135988, 0.9942893857347128]], [[-0.0942022363276273, 0.9955530817946745], [-0.0942022363276273, 0.9955530817946745]], [[-0.08167210186320688, 0.9966592535953529], [-0.08167210186320688, 0.9966592535953529]], [[-0.06912904459478485, 0.9976077261095226], [-0.06912904459478485, 0.9976077261095226]], [[-0.056575049183792726, 0.998398349262383], [-0.056575049183792726, 0.998398349262383]], [[-0.04401210202238211, 0.9990309979553044], [-0.04401210202238211, 0.9990309979553044]], [[-0.031442190919121114, 0.9995055720856215], [-0.031442190919121114, 0.9995055720856215]], [[-0.018867304784467676, 0.9998219965624732], [-0.018867304784467676, 0.9998219965624732]], [[-0.006289433316068405, 0.9999802213186832], [-0.006289433316068405, 0.9999802213186832]], [[0.006289433316067026, 0.9999802213186832], [0.006289433316067026, 0.9999802213186832]], [[0.0188673047844663, 0.9998219965624732], [0.0188673047844663, 0.9998219965624732]], [[0.03144219091911974, 0.9995055720856215], [0.03144219091911974, 0.9995055720856215]], [[0.04401210202238073, 0.9990309979553045], [0.04401210202238073, 0.9990309979553045]], [[0.056575049183791346, 0.9983983492623831], [0.056575049183791346, 0.9983983492623831]], [[0.06912904459478347, 0.9976077261095226], [0.06912904459478347, 0.9976077261095226]], [[0.08167210186320639, 0.9966592535953529], [0.08167210186320639, 0.9966592535953529]], [[0.09420223632762592, 0.9955530817946746], [0.09420223632762592, 0.9955530817946746]], [[0.10671746537135851, 0.994289385734713], [0.10671746537135851, 0.994289385734713]], [[0.11921580873617288, 0.9928683653674238], [0.11921580873617288, 0.9928683653674238]], [[0.13169528883562306, 0.9912902455378555], [0.13169528883562306, 0.9912902455378555]], [[0.14415393106795768, 0.9895552759485721], [0.14415393106795768, 0.9895552759485721]], [[0.15658976412855702, 0.9876637311201434], [0.15658976412855702, 0.9876637311201434]], [[0.16900082032184832, 0.9856159103477086], [0.16900082032184832, 0.9856159103477086]], [[0.18138513587265026, 0.9834121376536189], [0.18138513587265026, 0.9834121376536189]], [[0.19374075123689677, 0.9810527617361683], [0.19374075123689677, 0.9810527617361683]], [[0.2060657114116916, 0.9785381559144198], [0.2060657114116916, 0.9785381559144198]], [[0.21835806624464443, 0.9758687180691363], [0.21835806624464443, 0.9758687180691363]], [[0.2306158707424391, 0.9730448705798241], [0.2306158707424391, 0.9730448705798241]], [[0.24283718537858687, 0.9700670602579009], [0.24283718537858687, 0.9700670602579009]], [[0.2550200764003101, 0.9669357582759984], [0.2550200764003101, 0.9669357582759984]], [[0.2671626161345209, 0.9636514600934087], [0.2671626161345209, 0.9636514600934087]], [[0.2792628832928296, 0.9602146853776896], [0.2792628832928296, 0.9602146853776896]], [[0.2913189632755466, 0.956625977922438], [0.2913189632755466, 0.956625977922438]], [[0.30332894847462605, 0.952885905561247], [0.30332894847462605, 0.952885905561247]], [[0.3152909385755018, 0.9489950600778589], [0.3152909385755018, 0.9489950600778589]], [[0.3272030408577709, 0.9449540571125286], [0.3272030408577709, 0.9449540571125286]], [[0.33906337049467444, 0.9407635360646113], [0.33906337049467444, 0.9407635360646113]], [[0.3508700508513296, 0.9364241599913926], [0.3508700508513296, 0.9364241599913926]], [[0.36262121378166595, 0.9319366155031743], [0.36262121378166595, 0.9319366155031743]], [[0.3743149999240179, 0.9273016126546327], [0.3743149999240179, 0.9273016126546327]], [[0.3859495589953277, 0.9225198848324692], [0.3859495589953277, 0.9225198848324692]], [[0.39752305008391264, 0.9175921886393666], [0.39752305008391264, 0.9175921886393666]], [[0.40903364194074554, 0.9125193037742763], [0.40903364194074554, 0.9125193037742763]], [[0.4204795132692139, 0.9073020329090445], [0.4204795132692139, 0.9073020329090445]], [[0.4318588530132969, 0.9019412015614098], [0.4318588530132969, 0.9019412015614098]], [[0.44316986064412556, 0.896437657964382], [0.44316986064412556, 0.896437657964382]], [[0.45441074644487683, 0.8907922729320287], [0.45441074644487683, 0.8907922729320287]], [[0.46557973179395645, 0.8850059397216877], [0.46557973179395645, 0.8850059397216877]], [[0.47667504944642675, 0.8790795738926293], [0.47667504944642675, 0.8790795738926293]], [[0.48769494381363376, 0.8730141131611886], [0.48769494381363376, 0.8730141131611886]], [[0.4986376712409907, 0.8668105172523933], [0.4986376712409907, 0.8668105172523933]], [[0.5095015002838723, 0.8604697677481082], [0.5095015002838723, 0.8604697677481082]], [[0.520284711981578, 0.8539928679317214], [0.520284711981578, 0.8539928679317214]], [[0.5309856001293194, 0.8473808426293968], [0.5309856001293194, 0.8473808426293968]], [[0.5416024715481897, 0.8406347380479183], [0.5416024715481897, 0.8406347380479183]], [[0.5521336463530699, 0.8337556216091518], [0.5521336463530699, 0.8337556216091518]], [[0.5625774582184366, 0.8267445817811466], [0.5625774582184366, 0.8267445817811466]], [[0.5729322546420195, 0.8196027279059118], [0.5729322546420195, 0.8196027279059118]], [[0.5831963972062728, 0.8123311900238863], [0.5831963972062728, 0.8123311900238863]], [[0.5933682618376198, 0.8049311186951352], [0.5933682618376198, 0.8049311186951352]], [[0.6034462390634255, 0.7974036848172994], [0.6034462390634255, 0.7974036848172994]], [[0.6134287342666611, 0.7897500794403265], [0.6134287342666611, 0.7897500794403265]], [[0.6233141679382159, 0.7819715135780135], [0.6233141679382159, 0.7819715135780135]], [[0.6331009759268206, 0.7740692180163913], [0.6331009759268206, 0.7740692180163913]], [[0.6427876096865385, 0.7660444431189787], [0.6427876096865385, 0.7660444431189787]], [[0.6523725365217901, 0.7578984586289417], [0.6523725365217901, 0.7578984586289417]], [[0.6618542398298678, 0.7496325534681827], [0.6618542398298678, 0.7496325534681827]], [[0.6712312193409025, 0.7412480355334005], [0.6712312193409025, 0.7412480355334005]], [[0.6805019913552521, 0.7327462314891401], [0.6805019913552521, 0.7327462314891401]], [[0.6896650889782615, 0.7241284865578805], [0.6896650889782615, 0.7241284865578805]], [[0.698719062352367, 0.7153961643071823], [0.698719062352367, 0.7153961643071823]], [[0.7076624788865039, 0.7065506464339328], [0.7076624788865039, 0.7065506464339328]], [[0.7164939234827827, 0.6975933325457234], [0.7164939234827827, 0.6975933325457234]], [[0.7252119987603968, 0.6885256399393928], [0.7252119987603968, 0.6885256399393928]], [[0.7338153252767271, 0.6793490033767704], [0.7338153252767271, 0.6793490033767704]], [[0.7423025417456087, 0.670064874857658], [0.7423025417456087, 0.670064874857658]], [[0.7506723052527237, 0.6606747233900823], [0.7506723052527237, 0.6606747233900823]], [[0.7589232914680881, 0.6511800347578566], [0.7589232914680881, 0.6511800347578566]], [[0.767054194855598, 0.6415823112854891], [0.767054194855598, 0.6415823112854891]], [[0.7750637288796014, 0.6318830716004724], [0.7750637288796014, 0.6318830716004724]], [[0.7829506262084629, 0.6220838503929964], [0.7829506262084629, 0.6220838503929964]], [[0.7907136389150935, 0.612186198173114], [0.7907136389150935, 0.612186198173114]], [[0.7983515386744056, 0.60219168102541], [0.7983515386744056, 0.60219168102541]], [[0.8058631169576688, 0.5921018803612016], [0.8058631169576688, 0.5921018803612016]], [[0.8132471852237325, 0.5819183926683157], [0.8132471852237325, 0.5819183926683157]], [[0.820502575107087, 0.5716428292584793], [0.820502575107087, 0.5716428292584793]], [[0.8276281386027308, 0.5612768160123658], [0.8276281386027308, 0.5612768160123658]], [[0.8346227482478168, 0.5508219931223347], [0.8346227482478168, 0.5508219931223347]], [[0.8414852973000496, 0.5402800148329078], [0.8414852973000496, 0.5402800148329078]], [[0.8482146999128017, 0.5296525491790214], [0.8482146999128017, 0.5296525491790214]], [[0.8548098913069254, 0.5189412777220967], [0.8548098913069254, 0.5189412777220967]], [[0.8612698279392301, 0.5081478952839703], [0.8612698279392301, 0.5081478952839703]], [[0.8675934876676011, 0.49727410967872443], [0.8675934876676011, 0.49727410967872443]], [[0.8737798699127283, 0.48632164144246715], [0.8737798699127283, 0.48632164144246715]], [[0.8798279958164291, 0.4752922235610904], [0.8798279958164291, 0.4752922235610904]], [[0.8857369083965291, 0.4641876011960666], [0.8857369083965291, 0.4641876011960666]], [[0.8915056726982836, 0.4530095314083147], [0.8915056726982836, 0.4530095314083147]], [[0.8971333759423138, 0.4417597828801838], [0.8971333759423138, 0.4417597828801838]], [[0.9026191276690336, 0.43044013563559885], [0.9026191276690336, 0.43044013563559885]], [[0.9079620598795458, 0.4190523807584107], [0.9079620598795458, 0.4190523807584107]], [[0.9131613271729829, 0.4075983201089971], [0.9131613271729829, 0.4075983201089971]], [[0.9182161068802737, 0.39607976603915773], [0.9182161068802737, 0.39607976603915773]], [[0.9231255991943119, 0.3844985411053501], [0.9231255991943119, 0.3844985411053501]], [[0.9278890272965089, 0.37285647778030967], [0.9278890272965089, 0.37285647778030967]], [[0.932505637479707, 0.36115541816310226], [0.932505637479707, 0.36115541816310226]], [[0.9369746992674379, 0.3493972136876513], [0.9369746992674379, 0.3493972136876513]], [[0.9412955055295031, 0.3375837248297927], [0.9412955055295031, 0.3375837248297927]], [[0.9454673725938633, 0.32571682081289105], [0.9454673725938633, 0.32571682081289105]], [[0.9494896403548132, 0.3137983793120792], [0.9494896403548132, 0.3137983793120792]], [[0.9533616723774291, 0.3018302861571574], [0.9533616723774291, 0.3018302861571574]], [[0.9570828559982706, 0.2898144350342019], [0.9570828559982706, 0.2898144350342019]], [[0.9606526024223209, 0.27775272718593136], [0.9606526024223209, 0.27775272718593136]], [[0.9640703468161504, 0.26564707111087715], [0.9640703468161504, 0.26564707111087715]], [[0.96733554839729, 0.25349938226140567], [0.96733554839729, 0.25349938226140567]], [[0.9704476905197967, 0.24131158274064027], [0.9704476905197967, 0.24131158274064027]], [[0.9734062807560024, 0.22908560099833106], [0.9734062807560024, 0.22908560099833106]], [[0.9762108509744293, 0.21682337152572034], [0.9762108509744293, 0.21682337152572034]], [[0.9788609574138614, 0.20452683454945125], [0.9788609574138614, 0.20452683454945125]], [[0.9813561807535595, 0.1921979357245733], [0.9813561807535595, 0.1921979357245733]], [[0.98369612617961, 0.17983862582668034], [0.98369612617961, 0.17983862582668034]], [[0.9858804234473957, 0.1674508604432468], [0.9858804234473957, 0.1674508604432468]], [[0.987908726940178, 0.15503659966419847], [0.987908726940178, 0.15503659966419847]], [[0.9897807157237833, 0.14259780777177156], [0.9897807157237833, 0.14259780777177156]], [[0.9914960935973847, 0.13013645292970846], [0.9914960935973847, 0.13013645292970846]], [[0.9930545891403676, 0.11765450687183943], [0.9930545891403676, 0.11765450687183943]], [[0.9944559557552775, 0.1051539445900992], [0.9944559557552775, 0.1051539445900992]], [[0.9956999717068375, 0.09263674402202833], [0.9956999717068375, 0.09263674402202833]], [[0.9967864401570342, 0.08010488573780816], [0.9967864401570342, 0.08010488573780816]], [[0.9977151891962615, 0.06756035262687954], [0.9977151891962615, 0.06756035262687954]], [[0.9984860718705224, 0.05500512958419429], [0.9984860718705224, 0.05500512958419429]], [[0.9990989662046814, 0.042441203196148705], [0.9990989662046814, 0.042441203196148705]], [[0.9995537752217638, 0.029870561426253633], [0.9995537752217638, 0.029870561426253633]], [[0.9998504269583004, 0.01729519330057795], [0.9998504269583004, 0.01729519330057795]], [[0.999988874475714, 0.004717088593032691], [0.999988874475714, 0.004717088593032691]], [[0.999969095867747, -0.007861762489467534], [0.999969095867747, -0.007861762489467534]], [[0.9997910942639262, -0.020439369621910786], [0.9997910942639262, -0.020439369621910786]], [[0.9994548978290694, -0.03301374267611272], [0.9994548978290694, -0.03301374267611272]], [[0.9989605597588275, -0.045582892035610355], [0.9989605597588275, -0.045582892035610355]], [[0.9983081582712683, -0.058144828910474865], [0.9983081582712683, -0.058144828910474865]], [[0.9974977965944998, -0.07069756565199363], [0.9974977965944998, -0.07069756565199363]], [[0.9965296029503368, -0.08323911606717167], [0.9965296029503368, -0.08323911606717167]], [[0.9954037305340127, -0.09576749573300279], [0.9954037305340127, -0.09576749573300279]], [[0.9941203574899394, -0.1082807223104606], [0.9941203574899394, -0.1082807223104606]], [[0.9926796868835203, -0.12077681585816072], [0.9926796868835203, -0.12077681585816072]], [[0.9910819466690197, -0.1332537991456392], [0.9910819466690197, -0.1332537991456392]], [[0.9893273896534936, -0.14570969796621086], [0.9893273896534936, -0.14570969796621086]], [[0.9874162934567892, -0.1581425414493393], [0.9874162934567892, -0.1581425414493393]], [[0.9853489604676167, -0.17055036237248902], [0.9853489604676167, -0.17055036237248902]], [[0.9831257177957046, -0.18293119747238504], [0.9831257177957046, -0.18293119747238504]], [[0.9807469172200398, -0.1952830877556692], [0.9807469172200398, -0.1952830877556692]], [[0.9782129351332084, -0.2076040788088552], [0.9782129351332084, -0.2076040788088552]], [[0.9755241724818389, -0.2198922211075767], [0.9755241724818389, -0.2198922211075767]], [[0.9726810547031601, -0.23214557032506142], [0.9726810547031601, -0.23214557032506142]], [[0.9696840316576879, -0.24436218763976586], [0.9696840316576879, -0.24436218763976586]], [[0.9665335775580415, -0.25654014004216474], [0.9665335775580415, -0.25654014004216474]], [[0.9632301908939129, -0.2686775006405933], [0.9632301908939129, -0.2686775006405933]], [[0.9597743943531892, -0.2807723489661489], [0.9597743943531892, -0.2807723489661489]], [[0.9561667347392514, -0.29282277127654904], [0.9561667347392514, -0.29282277127654904]], [[0.9524077828844516, -0.3048268608589526], [0.9524077828844516, -0.3048268608589526]], [[0.9484981335597957, -0.3167827183316413], [0.9484981335597957, -0.3167827183316413]], [[0.9444384053808291, -0.32868845194456814], [0.9444384053808291, -0.32868845194456814]], [[0.9402292407097596, -0.340542177878672], [0.9402292407097596, -0.340542177878672]], [[0.9358713055538124, -0.3523420205439635], [0.9358713055538124, -0.3523420205439635]], [[0.9313652894598542, -0.36408611287628373], [0.9313652894598542, -0.36408611287628373]], [[0.9267119054052854, -0.37577259663273127], [0.9267119054052854, -0.37577259663273127]], [[0.9219118896852252, -0.38739962268569283], [0.9219118896852252, -0.38739962268569283]], [[0.9169660017960138, -0.3989653513154153], [0.9169660017960138, -0.3989653513154153]], [[0.9118750243150339, -0.4104679525011135], [0.9118750243150339, -0.4104679525011135]], [[0.9066397627768898, -0.4219056062105182], [0.9066397627768898, -0.4219056062105182]], [[0.901261045545945, -0.4332765026878681], [0.901261045545945, -0.4332765026878681]], [[0.895739723685256, -0.44457884274025133], [0.895739723685256, -0.44457884274025133]], [[0.8900766708219062, -0.45581083802230066], [0.8900766708219062, -0.45581083802230066]], [[0.8842727830087785, -0.46697071131914664], [0.8842727830087785, -0.46697071131914664]], [[0.878328978582769, -0.47805669682763535], [0.878328978582769, -0.47805669682763535]], [[0.8722461980194871, -0.48906704043571536], [0.8722461980194871, -0.48906704043571536]], [[0.8660254037844392, -0.4999999999999992], [0.8660254037844392, -0.4999999999999992]], [[0.8596675801807453, -0.5108538456214086], [0.8596675801807453, -0.5108538456214086]], [[0.8531737331933934, -0.5216268599188969], [0.8531737331933934, -0.5216268599188969]], [[0.8465448903300608, -0.5323173383011919], [0.8465448903300608, -0.5323173383011919]], [[0.8397821004585404, -0.5429235892364983], [0.8397821004585404, -0.5429235892364983]], [[0.8328864336407736, -0.5534439345201582], [0.8328864336407736, -0.5534439345201582]], [[0.8258589809635439, -0.5638767095401768], [0.8258589809635439, -0.5638767095401768]], [[0.8187008543658284, -0.5742202635406232], [0.8187008543658284, -0.5742202635406232]], [[0.8114131864628666, -0.5844729598828138], [0.8114131864628666, -0.5844729598828138]], [[0.803997130366941, -0.5946331763042861], [0.803997130366941, -0.5946331763042861]], [[0.7964538595049301, -0.6046993051754741], [0.7964538595049301, -0.6046993051754741]], [[0.7887845674326319, -0.6146697537540917], [0.7887845674326319, -0.6146697537540917]], [[0.7809904676459185, -0.6245429444371375], [0.7809904676459185, -0.6245429444371375]], [[0.7730727933887184, -0.6343173150105269], [0.7730727933887184, -0.6343173150105269]], [[0.76503279745789, -0.6439913188962683], [0.76503279745789, -0.6439913188962683]], [[0.7568717520049925, -0.6535634253971785], [0.7568717520049925, -0.6535634253971785]], [[0.7485909483349908, -0.6630321199390865], [0.7485909483349908, -0.6630321199390865]], [[0.7401916967019444, -0.6723959043104716], [0.7401916967019444, -0.6723959043104716]], [[0.7316753261016786, -0.6816532968995326], [0.7316753261016786, -0.6816532968995326]], [[0.7230431840615102, -0.69080283292861], [0.7230431840615102, -0.69080283292861]], [[0.7142966364270213, -0.6998430646859649], [0.7142966364270213, -0.6998430646859649]], [[0.7054370671459542, -0.7087725617548373], [0.7054370671459542, -0.7087725617548373]], [[0.6964658780492222, -0.7175899112397874], [0.6964658780492222, -0.7175899112397874]], [[0.6873844886291115, -0.7262937179902459], [0.6873844886291115, -0.7262937179902459]], [[0.678194335814667, -0.7348826048212753], [0.678194335814667, -0.7348826048212753]], [[0.6688968737443408, -0.7433552127314689], [0.6688968737443408, -0.7433552127314689]], [[0.6594935735358967, -0.7517102011179926], [0.6594935735358967, -0.7517102011179926]], [[0.6499859230536468, -0.7599462479886974], [0.6499859230536468, -0.7599462479886974]], [[0.6403754266730268, -0.7680620501712988], [0.6403754266730268, -0.7680620501712988]], [[0.6306636050425575, -0.7760563235195788], [0.6306636050425575, -0.7760563235195788]], [[0.6208519948432446, -0.7839278031165648], [0.6208519948432446, -0.7839278031165648]], [[0.6109421485454233, -0.7916752434746851], [0.6109421485454233, -0.7916752434746851]], [[0.600935634163124, -0.7992974187328293], [0.600935634163124, -0.7992974187328293]], [[0.5908340350059585, -0.8067931228503239], [0.5908340350059585, -0.8067931228503239]], [[0.5806389494286068, -0.8141611697977519], [0.5806389494286068, -0.8141611697977519]], [[0.570351990577902, -0.8214003937446248], [0.570351990577902, -0.8214003937446248]], [[0.5599747861375968, -0.8285096492438412], [0.5599747861375968, -0.8285096492438412]], [[0.5495089780708068, -0.8354878114129359], [0.5495089780708068, -0.8354878114129359]], [[0.5389562223602165, -0.8423337761120617], [0.5389562223602165, -0.8423337761120617]], [[0.5283181887460523, -0.8490464601186973], [0.5283181887460523, -0.8490464601186973]], [[0.5175965604618786, -0.8556248012990465], [0.5175965604618786, -0.8556248012990465]], [[0.5067930339682736, -0.8620677587760909], [0.5067930339682736, -0.8620677587760909]], [[0.49590931868438975, -0.8683743130942925], [0.49590931868438975, -0.8683743130942925]], [[0.4849471367174889, -0.8745434663808935], [0.4849471367174889, -0.8745434663808935]], [[0.4739082225904436, -0.8805742425038144], [0.4739082225904436, -0.8805742425038144]], [[0.4627943229673003, -0.886465687226098], [0.4627943229673003, -0.886465687226098]], [[0.4516071963768956, -0.8922168683569035], [0.4516071963768956, -0.8922168683569035]], [[0.44034861293462074, -0.8978268758989985], [0.44034861293462074, -0.8978268758989985]], [[0.42902035406232714, -0.903294822192752], [0.42902035406232714, -0.903294822192752]], [[0.4176242122064685, -0.9086198420565812], [0.4176242122064685, -0.9086198420565812]], [[0.4061619905544733, -0.9138010929238529], [0.4061619905544733, -0.9138010929238529]], [[0.3946355027494409, -0.918837754976196], [0.3946355027494409, -0.918837754976196]], [[0.38304657260316866, -0.9237290312732221], [0.38304657260316866, -0.9237290312732221]], [[0.37139703380756833, -0.9284741478786256], [0.37139703380756833, -0.9284741478786256]], [[0.3596887296445368, -0.9330723539826369], [0.3596887296445368, -0.9330723539826369]], [[0.34792351269428423, -0.9375229220208273], [0.34792351269428423, -0.9375229220208273]], [[0.3361032445422173, -0.9418251477892244], [0.3361032445422173, -0.9418251477892244]], [[0.3242297954843714, -0.9459783505557422], [0.3242297954843714, -0.9459783505557422]], [[0.31230504423149086, -0.9499818731678866], [0.31230504423149086, -0.9499818731678866]], [[0.3003308776117511, -0.9538350821567402], [0.3003308776117511, -0.9538350821567402]], [[0.28830919027222335, -0.9575373678371905], [0.28830919027222335, -0.9575373678371905]], [[0.27624188437907515, -0.9610881444044025], [0.27624188437907515, -0.9610881444044025]], [[0.264130869316608, -0.9644868500265066], [0.264130869316608, -0.9644868500265066]], [[0.2519780613851261, -0.9677329469334987], [0.2519780613851261, -0.9677329469334987]], [[0.2397853834977361, -0.9708259215023276], [0.2397853834977361, -0.9708259215023276]], [[0.22755476487608342, -0.9737652843381666], [0.22755476487608342, -0.9737652843381666]], [[0.2152881407450906, -0.9765505703518492], [0.2152881407450906, -0.9765505703518492]], [[0.20298745202676252, -0.9791813388334577], [0.20298745202676252, -0.9791813388334577]], [[0.19065464503306495, -0.9816571735220581], [0.19065464503306495, -0.9816571735220581]], [[0.17829167115797728, -0.9839776826715613], [0.17829167115797728, -0.9839776826715613]], [[0.1659004865687139, -0.9861424991127113], [0.1659004865687139, -0.9861424991127113]], [[0.15348305189621775, -0.9881512803111794], [0.15348305189621775, -0.9881512803111794]], [[0.14104133192492, -0.9900037084217637], [0.14104133192492, -0.9900037084217637]], [[0.12857729528187029, -0.9916994903386805], [0.12857729528187029, -0.9916994903386805]], [[0.11609291412523105, -0.9932383577419429], [0.11609291412523105, -0.9932383577419429]], [[0.10359016383224108, -0.9946200671398147], [0.10359016383224108, -0.9946200671398147]], [[0.09107102268664179, -0.9958443999073395], [0.09107102268664179, -0.9958443999073395]], [[0.07853747156566976, -0.996911162320932], [0.07853747156566976, -0.996911162320932]], [[0.0659914936266216, -0.9978201855890306], [0.0659914936266216, -0.9978201855890306]], [[0.05343507399305771, -0.9985713258788059], [0.05343507399305771, -0.9985713258788059]], [[0.04087019944071283, -0.9991644643389177], [0.04087019944071283, -0.9991644643389177]], [[0.028298858083118522, -0.9995995071183216], [0.028298858083118522, -0.9995995071183216]], [[0.01572303905704239, -0.9998763853811183], [0.01572303905704239, -0.9998763853811183]], [[0.003144732207736932, -0.9999950553174458], [0.003144732207736932, -0.9999950553174458]], [[-0.009434072225895224, -0.999955498150411], [-0.009434072225895224, -0.999955498150411]], [[-0.02201138392622685, -0.9997577201390606], [-0.02201138392622685, -0.9997577201390606]], [[-0.03458521281181564, -0.9994017525773914], [-0.03458521281181564, -0.9994017525773914]], [[-0.04715356935230482, -0.9988876517893979], [-0.04715356935230482, -0.9988876517893979]], [[-0.05971446488320808, -0.9982154991201609], [-0.05971446488320808, -0.9982154991201609]], [[-0.07226591192058601, -0.9973854009229762], [-0.07226591192058601, -0.9973854009229762]], [[-0.08480592447550901, -0.9963974885425265], [-0.08480592447550901, -0.9963974885425265]], [[-0.0973325183683015, -0.9952519182940992], [-0.0973325183683015, -0.9952519182940992]], [[-0.1098437115424997, -0.9939488714388522], [-0.1098437115424997, -0.9939488714388522]], [[-0.12233752437845594, -0.9924885541551351], [-0.12233752437845594, -0.9924885541551351]], [[-0.13481198000658376, -0.9908711975058637], [-0.13481198000658376, -0.9908711975058637]], [[-0.14726510462013975, -0.9890970574019616], [-0.14726510462013975, -0.9890970574019616]], [[-0.15969492778754882, -0.9871664145618658], [-0.15969492778754882, -0.9871664145618658]], [[-0.17209948276416748, -0.9850795744671118], [-0.17209948276416748, -0.9850795744671118]], [[-0.18447680680349163, -0.9828368673139949], [-0.18447680680349163, -0.9828368673139949]], [[-0.19682494146770374, -0.9804386479613271], [-0.19682494146770374, -0.9804386479613271]], [[-0.2091419329375665, -0.9778852958742853], [-0.2091419329375665, -0.9778852958742853]], [[-0.22142583232155733, -0.9751772150643726], [-0.22142583232155733, -0.9751772150643726]], [[-0.23367469596425144, -0.9723148340254892], [-0.23367469596425144, -0.9723148340254892]], [[-0.24588658575385006, -0.9692986056661356], [-0.24588658575385006, -0.9692986056661356]], [[-0.2580595694288491, -0.9661290072377483], [-0.2580595694288491, -0.9661290072377483]], [[-0.2701917208837818, -0.9628065402591844], [-0.2701917208837818, -0.9628065402591844]], [[-0.2822811204739704, -0.9593317304373705], [-0.2822811204739704, -0.9593317304373705]], [[-0.29432585531928135, -0.9557051275841171], [-0.29432585531928135, -0.9557051275841171]], [[-0.30632401960678207, -0.951927305529127], [-0.30632401960678207, -0.951927305529127]], [[-0.31827371489230794, -0.9479988620291956], [-0.31827371489230794, -0.9479988620291956]], [[-0.3301730504008353, -0.9439204186736335], [-0.3301730504008353, -0.9439204186736335]], [[-0.342020143325668, -0.9396926207859086], [-0.342020143325668, -0.9396926207859086]], [[-0.35381311912633706, -0.9353161373215435], [-0.35381311912633706, -0.9353161373215435]], [[-0.3655501118252182, -0.9307916607622624], [-0.3655501118252182, -0.9307916607622624]], [[-0.37722926430276815, -0.9261199070064267], [-0.37722926430276815, -0.9261199070064267]], [[-0.3888487285913865, -0.9213016152557545], [-0.3888487285913865, -0.9213016152557545]], [[-0.4004066661678036, -0.9163375478983632], [-0.4004066661678036, -0.9163375478983632]], [[-0.4119012482439916, -0.9112284903881362], [-0.4119012482439916, -0.9112284903881362]], [[-0.4233306560565341, -0.9059752511204401], [-0.4233306560565341, -0.9059752511204401]], [[-0.4346930811543944, -0.9005786613042189], [-0.4346930811543944, -0.9005786613042189]], [[-0.4459867256850755, -0.8950395748304681], [-0.4459867256850755, -0.8950395748304681]], [[-0.4572098026790778, -0.8893588681371309], [-0.4572098026790778, -0.8893588681371309]], [[-0.46836053633265995, -0.8835374400704156], [-0.46836053633265995, -0.8835374400704156]], [[-0.47943716228880834, -0.8775762117425784], [-0.47943716228880834, -0.8775762117425784]], [[-0.4904379279164198, -0.8714761263861728], [-0.4904379279164198, -0.8714761263861728]], [[-0.5013610925876044, -0.8652381492048091], [-0.5013610925876044, -0.8652381492048091]], [[-0.5122049279531135, -0.8588632672204265], [-0.5122049279531135, -0.8588632672204265]], [[-0.5229677182158008, -0.852352489117125], [-0.5229677182158008, -0.852352489117125]], [[-0.5336477604021214, -0.8457068450815567], [-0.5336477604021214, -0.8457068450815567]], [[-0.5442433646315787, -0.8389273866399275], [-0.5442433646315787, -0.8389273866399275]], [[-0.5547528543841161, -0.8320151864916143], [-0.5547528543841161, -0.8320151864916143]], [[-0.5651745667653925, -0.8249713383394304], [-0.5651745667653925, -0.8249713383394304]], [[-0.5755068527698889, -0.8177969567165786], [-0.5755068527698889, -0.8177969567165786]], [[-0.5857480775418389, -0.8104931768102923], [-0.5857480775418389, -0.8104931768102923]], [[-0.5958966206338965, -0.8030611542822266], [-0.5958966206338965, -0.8030611542822266]], [[-0.6059508762635476, -0.7955020650855904], [-0.6059508762635476, -0.7955020650855904]], [[-0.6159092535671783, -0.7878171052790878], [-0.6159092535671783, -0.7878171052790878]], [[-0.6257701768518052, -0.7800074908376589], [-0.6257701768518052, -0.7800074908376589]], [[-0.6355320858443827, -0.7720744574600873], [-0.6355320858443827, -0.7720744574600873]], [[-0.6451934359386927, -0.76401926037347], [-0.6451934359386927, -0.76401926037347]], [[-0.6547526984397336, -0.7558431741346133], [-0.6547526984397336, -0.7558431741346133]], [[-0.6642083608056132, -0.7475474924283543], [-0.6642083608056132, -0.7475474924283543]], [[-0.6735589268868657, -0.7391335278628713], [-0.6735589268868657, -0.7391335278628713]], [[-0.6828029171631881, -0.7306026117619896], [-0.6828029171631881, -0.7306026117619896]], [[-0.6919388689775459, -0.7219560939545248], [-0.6919388689775459, -0.7219560939545248]], [[-0.7009653367675964, -0.7131953425607112], [-0.7009653367675964, -0.7131953425607112]], [[-0.7098808922944282, -0.7043217437757168], [-0.7098808922944282, -0.7043217437757168]], [[-0.7186841248685372, -0.695336701650319], [-0.7186841248685372, -0.695336701650319]], [[-0.7273736415730482, -0.6862416378687342], [-0.7273736415730482, -0.6862416378687342]], [[-0.7359480674841022, -0.6770379915236775], [-0.7359480674841022, -0.6770379915236775]], [[-0.7444060458884184, -0.6677272188886492], [-0.7444060458884184, -0.6677272188886492]], [[-0.7527462384979536, -0.6583107931875202], [-0.7527462384979536, -0.6583107931875202]], [[-0.7609673256616669, -0.648790204361418], [-0.7609673256616669, -0.648790204361418]], [[-0.7690680065743155, -0.6391669588329865], [-0.7690680065743155, -0.6391669588329865]], [[-0.7770469994822877, -0.6294425792680167], [-0.7770469994822877, -0.6294425792680167]], [[-0.7849030418864043, -0.619618604334529], [-0.7849030418864043, -0.619618604334529]], [[-0.7926348907416839, -0.609696588459308], [-0.7926348907416839, -0.609696588459308]], [[-0.8002413226540318, -0.5996781015819452], [-0.8002413226540318, -0.5996781015819452]], [[-0.807721134073806, -0.5895647289064406], [-0.807721134073806, -0.5895647289064406]], [[-0.8150731414862619, -0.5793580706503675], [-0.8150731414862619, -0.5793580706503675]], [[-0.8222961815988086, -0.5690597417916851], [-0.8222961815988086, -0.5690597417916851]], [[-0.8293891115250823, -0.5586713718131927], [-0.8293891115250823, -0.5586713718131927]], [[-0.8363508089657752, -0.5481946044447112], [-0.8363508089657752, -0.5481946044447112]], [[-0.8431801723862219, -0.537631097402988], [-0.8431801723862219, -0.537631097402988]], [[-0.8498761211906855, -0.5269825221294112], [-0.8498761211906855, -0.5269825221294112]], [[-0.8564375958933453, -0.5162505635255297], [-0.8564375958933453, -0.5162505635255297]], [[-0.8628635582859301, -0.5054369196864662], [-0.8628635582859301, -0.5054369196864662]], [[-0.8691529916019983, -0.49454330163221977], [-0.8691529916019983, -0.49454330163221977]], [[-0.8753049006778127, -0.4835714330369447], [-0.8753049006778127, -0.4835714330369447]], [[-0.8813183121098064, -0.4725230499562131], [-0.8813183121098064, -0.4725230499562131]], [[-0.8871922744086038, -0.46139990055231767], [-0.8871922744086038, -0.46139990055231767]], [[-0.8929258581495678, -0.4502037448176746], [-0.8929258581495678, -0.4502037448176746]], [[-0.898518156119867, -0.43893635429633115], [-0.898518156119867, -0.43893635429633115]], [[-0.9039682834620154, -0.42759951180367056], [-0.9039682834620154, -0.42759951180367056]], [[-0.9092753778138881, -0.4161950111443084], [-0.9092753778138881, -0.4161950111443084]], [[-0.914438599445165, -0.40472465682827513], [-0.914438599445165, -0.40472465682827513]], [[-0.919457131390205, -0.39319026378547983], [-0.919457131390205, -0.39319026378547983]], [[-0.9243301795773077, -0.38159365707855025], [-0.9243301795773077, -0.38159365707855025]], [[-0.9290569729543624, -0.36993667161404425], [-0.9290569729543624, -0.36993667161404425]], [[-0.9336367636108461, -0.3582211518521277], [-0.9336367636108461, -0.3582211518521277]], [[-0.9380688268961654, -0.34644895151472466], [-0.9380688268961654, -0.34644895151472466]], [[-0.9423524615343185, -0.3346219332922018], [-0.9423524615343185, -0.3346219332922018]], [[-0.946486989734852, -0.32274196854865056], [-0.946486989734852, -0.32274196854865056]], [[-0.9504717573001114, -0.31081093702577167], [-0.9504717573001114, -0.31081093702577167]], [[-0.9543061337287484, -0.2988307265454612], [-0.9543061337287484, -0.2988307265454612]], [[-0.9579895123154887, -0.2868032327110909], [-0.9579895123154887, -0.2868032327110909]], [[-0.9615213102471251, -0.27473035860758444], [-0.9615213102471251, -0.27473035860758444]], [[-0.9649009686947388, -0.2626140145002827], [-0.9649009686947388, -0.2626140145002827]], [[-0.9681279529021183, -0.25045611753270025], [-0.9681279529021183, -0.25045611753270025]], [[-0.9712017522703761, -0.23825859142316594], [-0.9712017522703761, -0.23825859142316594]], [[-0.9741218804387358, -0.22602336616045093], [-0.9741218804387358, -0.22602336616045093]], [[-0.9768878753614922, -0.21375237769837674], [-0.9768878753614922, -0.21375237769837674]], [[-0.9794992993811164, -0.2014475676495055], [-0.9794992993811164, -0.2014475676495055]], [[-0.9819557392975065, -0.18911088297791753], [-0.9819557392975065, -0.18911088297791753]], [[-0.9842568064333685, -0.17674427569114207], [-0.9842568064333685, -0.17674427569114207]], [[-0.9864021366957143, -0.1643497025313075], [-0.9864021366957143, -0.1643497025313075]], [[-0.9883913906334727, -0.1519291246655162], [-0.9883913906334727, -0.1519291246655162]], [[-0.9902242534911982, -0.1394845073755471], [-0.9902242534911982, -0.1394845073755471]], [[-0.9919004352588768, -0.12701781974687945], [-0.9919004352588768, -0.12701781974687945]], [[-0.9934196707178105, -0.11453103435714257], [-0.9934196707178105, -0.11453103435714257]], [[-0.9947817194825852, -0.10202612696398496], [-0.9947817194825852, -0.10202612696398496]], [[-0.9959863660391042, -0.08950507619246842], [-0.9959863660391042, -0.08950507619246842]], [[-0.9970334197786901, -0.07696986322198038], [-0.9970334197786901, -0.07696986322198038]], [[-0.9979227150282431, -0.0644224714727701], [-0.9979227150282431, -0.0644224714727701]], [[-0.9986541110764564, -0.051864886292102175], [-0.9986541110764564, -0.051864886292102175]], [[-0.9992274921960794, -0.03929909464013164], [-0.9992274921960794, -0.03929909464013164]], [[-0.9996427676622299, -0.026727084775506123], [-0.9996427676622299, -0.026727084775506123]], [[-0.9998998717667489, -0.014150845940762564], [-0.9998998717667489, -0.014150845940762564]], [[-0.9999987638285974, -0.001572368047586014], [-0.9999987638285974, -0.001572368047586014]], [[-0.9999394282002937, 0.0110063586380641], [-0.9999394282002937, 0.0110063586380641]], [[-0.9997218742703887, 0.02358334381085534], [-0.9997218742703887, 0.02358334381085534]], [[-0.9993461364619809, 0.036156597441018276], [-0.9993461364619809, 0.036156597441018276]], [[-0.9988122742272693, 0.04872413008921046], [-0.9988122742272693, 0.04872413008921046]], [[-0.9981203720381463, 0.06128395322131545], [-0.9981203720381463, 0.06128395322131545]], [[-0.9972705393728328, 0.0738340795230701], [-0.9972705393728328, 0.0738340795230701]], [[-0.9962629106985544, 0.08637252321452737], [-0.9962629106985544, 0.08637252321452737]], [[-0.9950976454502662, 0.09889730036424782], [-0.9950976454502662, 0.09889730036424782]], [[-0.9937749280054243, 0.11140642920322712], [-0.9937749280054243, 0.11140642920322712]], [[-0.9922949676548137, 0.12389793043845473], [-0.9922949676548137, 0.12389793043845473]], [[-0.9906579985694319, 0.1363698275660986], [-0.9906579985694319, 0.1363698275660986]], [[-0.9888642797634358, 0.14882014718424852], [-0.9888642797634358, 0.14882014718424852]], [[-0.9869140950531602, 0.16124691930515087], [-0.9869140950531602, 0.16124691930515087]], [[-0.9848077530122081, 0.17364817766692972], [-0.9848077530122081, 0.17364817766692972]], [[-0.9825455869226281, 0.18602196004469043], [-0.9825455869226281, 0.18602196004469043]], [[-0.9801279547221767, 0.19836630856101212], [-0.9801279547221767, 0.19836630856101212]], [[-0.9775552389476866, 0.21067926999572462], [-0.9775552389476866, 0.21067926999572462]], [[-0.9748278466745344, 0.2229588960949763], [-0.9748278466745344, 0.2229588960949763]], [[-0.9719462094522341, 0.23520324387948816], [-0.9719462094522341, 0.23520324387948816]], [[-0.9689107832361499, 0.24741037595200138], [-0.9689107832361499, 0.24741037595200138]], [[-0.9657220483153551, 0.25957836080381363], [-0.9657220483153551, 0.25957836080381363]], [[-0.9623805092366339, 0.27170527312041143], [-0.9623805092366339, 0.27170527312041143]], [[-0.9588866947246498, 0.2837891940860965], [-0.9588866947246498, 0.2837891940860965]], [[-0.9552411575982872, 0.29582821168760115], [-0.9552411575982872, 0.29582821168760115]], [[-0.9514444746831768, 0.30782042101662727], [-0.9514444746831768, 0.30782042101662727]], [[-0.9474972467204302, 0.31976392457124386], [-0.9474972467204302, 0.31976392457124386]], [[-0.9434000982715814, 0.3316568325561384], [-0.9434000982715814, 0.3316568325561384]], [[-0.9391536776197683, 0.3434972631816217], [-0.9391536776197683, 0.3434972631816217]], [[-0.9347586566671513, 0.35528334296139286], [-0.9347586566671513, 0.35528334296139286]], [[-0.9302157308286049, 0.3670132070089637], [-0.9302157308286049, 0.3670132070089637]], [[-0.9255256189216783, 0.3786849993327492], [-0.9255256189216783, 0.3786849993327492]], [[-0.9206890630528639, 0.3902968731297237], [-0.9206890630528639, 0.3902968731297237]], [[-0.9157068285001696, 0.40184699107765015], [-0.9157068285001696, 0.40184699107765015]], [[-0.9105797035920364, 0.41333352562578207], [-0.9105797035920364, 0.41333352562578207]], [[-0.9053084995825972, 0.4247546592840467], [-0.9053084995825972, 0.4247546592840467]], [[-0.8998940505233184, 0.4361085849106107], [-0.8998940505233184, 0.4361085849106107]], [[-0.8943372131310279, 0.4473935059978257], [-0.8943372131310279, 0.4473935059978257]], [[-0.8886388666523561, 0.45860763695649037], [-0.8886388666523561, 0.45860763695649037]], [[-0.8827999127246203, 0.4697492033983695], [-0.8827999127246203, 0.4697492033983695]], [[-0.8768212752331539, 0.48081644241696414], [-0.8768212752331539, 0.48081644241696414]], [[-0.8707039001651283, 0.49180760286644026], [-0.8707039001651283, 0.49180760286644026]], [[-0.8644487554598653, 0.502720945638721], [-0.8644487554598653, 0.502720945638721]], [[-0.8580568308556884, 0.5135547439386501], [-0.8580568308556884, 0.5135547439386501]], [[-0.8515291377333118, 0.5243072835572309], [-0.8515291377333118, 0.5243072835572309]], [[-0.8448667089558188, 0.53497686314285], [-0.8448667089558188, 0.53497686314285]], [[-0.838070598705227, 0.5455617944704909], [-0.838070598705227, 0.5455617944704909]], [[-0.8311418823156947, 0.5560604027088458], [-0.8311418823156947, 0.5560604027088458]], [[-0.8240816561033651, 0.5664710266853329], [-0.8240816561033651, 0.5664710266853329]], [[-0.8168910371929057, 0.5767920191489293], [-0.8168910371929057, 0.5767920191489293]], [[-0.8095711633407447, 0.5870217470308176], [-0.8095711633407447, 0.5870217470308176]], [[-0.8021231927550442, 0.5971585917027857], [-0.8021231927550442, 0.5971585917027857]], [[-0.7945483039124446, 0.6072009492333305], [-0.7945483039124446, 0.6072009492333305]], [[-0.7868476953715905, 0.6171472306414546], [-0.7868476953715905, 0.6171472306414546]], [[-0.7790225855834922, 0.6269958621480771], [-0.7790225855834922, 0.6269958621480771]], [[-0.7710742126987252, 0.6367452854250599], [-0.7710742126987252, 0.6367452854250599]], [[-0.7630038343715285, 0.6463939578417678], [-0.7630038343715285, 0.6463939578417678]], [[-0.7548127275607995, 0.6559403527091668], [-0.7548127275607995, 0.6559403527091668]], [[-0.7465021883280534, 0.6653829595213779], [-0.7465021883280534, 0.6653829595213779]], [[-0.7380735316323398, 0.6747202841946918], [-0.7380735316323398, 0.6747202841946918]], [[-0.7295280911221899, 0.6839508493039641], [-0.7295280911221899, 0.6839508493039641]], [[-0.7208672189245859, 0.6930731943163961], [-0.7208672189245859, 0.6930731943163961]], [[-0.7120922854310258, 0.7020858758226223], [-0.7120922854310258, 0.7020858758226223]], [[-0.703204679080685, 0.7109874677651012], [-0.703204679080685, 0.7109874677651012]], [[-0.694205806140723, 0.719776561663763], [-0.694205806140723, 0.719776561663763]], [[-0.685097090483782, 0.7284517668388598], [-0.685097090483782, 0.7284517668388598]], [[-0.6758799733626797, 0.7370117106310208], [-0.6758799733626797, 0.7370117106310208]], [[-0.6665559131823733, 0.745455038618435], [-0.6665559131823733, 0.745455038618435]], [[-0.6571263852691893, 0.7537804148311689], [-0.6571263852691893, 0.7537804148311689]], [[-0.6475928816373955, 0.7619865219625438], [-0.6475928816373955, 0.7619865219625438]], [[-0.6379569107531127, 0.7700720615775806], [-0.6379569107531127, 0.7700720615775806]], [[-0.6282199972956439, 0.7780357543184383], [-0.6282199972956439, 0.7780357543184383]], [[-0.6183836819162163, 0.7858763401068541], [-0.6183836819162163, 0.7858763401068541]], [[-0.6084495209942188, 0.7935925783435136], [-0.6084495209942188, 0.7935925783435136]], [[-0.5984190863909279, 0.8011832481043567], [-0.5984190863909279, 0.8011832481043567]], [[-0.5882939652008056, 0.8086471483337546], [-0.5882939652008056, 0.8086471483337546]], [[-0.5780757595003719, 0.8159830980345537], [-0.5780757595003719, 0.8159830980345537]], [[-0.5677660860947084, 0.8231899364549449], [-0.5677660860947084, 0.8231899364549449]], [[-0.5573665762616435, 0.8302665232721198], [-0.5573665762616435, 0.8302665232721198]], [[-0.546878875493628, 0.8372117387727103], [-0.546878875493628, 0.8372117387727103]], [[-0.5363046432373839, 0.8440244840299495], [-0.5363046432373839, 0.8440244840299495]], [[-0.5256455526313215, 0.850703681077561], [-0.5256455526313215, 0.850703681077561]], [[-0.5149032902408143, 0.8572482730803158], [-0.5149032902408143, 0.8572482730803158]], [[-0.5040795557913256, 0.86365722450126], [-0.5040795557913256, 0.86365722450126]], [[-0.49317606189947616, 0.8699295212655587], [-0.49317606189947616, 0.8699295212655587]], [[-0.4821945338020488, 0.8760641709209576], [-0.4821945338020488, 0.8760641709209576]], [[-0.4711367090830182, 0.8820602027948112], [-0.4711367090830182, 0.8820602027948112]], [[-0.46000433739861224, 0.8879166681476723], [-0.46000433739861224, 0.8879166681476723]], [[-0.44879918020046267, 0.893632640323412], [-0.44879918020046267, 0.893632640323412]], [[-0.43752301045690567, 0.8992072148958361], [-0.43752301045690567, 0.8992072148958361]], [[-0.4261776123724359, 0.9046395098117977], [-0.4261776123724359, 0.9046395098117977]], [[-0.4147647811054085, 0.909928665530756], [-0.4147647811054085, 0.909928665530756]], [[-0.403286322483982, 0.9150738451607857], [-0.403286322483982, 0.9150738451607857]], [[-0.39174405272039897, 0.9200742345909907], [-0.39174405272039897, 0.9200742345909907]], [[-0.3801397981235976, 0.9249290426203247], [-0.3801397981235976, 0.9249290426203247]], [[-0.3684753948102517, 0.9296375010827764], [-0.3684753948102517, 0.9296375010827764]], [[-0.3567526884142328, 0.9341988649689195], [-0.3567526884142328, 0.9341988649689195]], [[-0.34497353379459245, 0.9386124125437886], [-0.34497353379459245, 0.9386124125437886]], [[-0.33313979474205874, 0.9428774454610838], [-0.33313979474205874, 0.9428774454610838]], [[-0.3212533436841441, 0.9469932888736632], [-0.3212533436841441, 0.9469932888736632]], [[-0.30931606138887024, 0.9509592915403249], [-0.30931606138887024, 0.9509592915403249]], [[-0.2973298366671729, 0.9547748259288534], [-0.2973298366671729, 0.9547748259288534]], [[-0.28529656607405124, 0.9584392883153082], [-0.28529656607405124, 0.9584392883153082]], [[-0.2732181536084666, 0.9619520988795546], [-0.2732181536084666, 0.9619520988795546]], [[-0.26109651041208987, 0.9653127017970029], [-0.26109651041208987, 0.9653127017970029]], [[-0.24893355446689247, 0.9685205653265596], [-0.24893355446689247, 0.9685205653265596]], [[-0.2367312102916815, 0.9715751818947599], [-0.2367312102916815, 0.9715751818947599]], [[-0.22449140863757358, 0.974476068176083], [-0.22449140863757358, 0.974476068176083]], [[-0.2122160861825098, 0.9772227651694252], [-0.2122160861825098, 0.9772227651694252]], [[-0.19990718522480572, 0.9798148382707292], [-0.19990718522480572, 0.9798148382707292]], [[-0.1875666533758392, 0.9822518773417477], [-0.1875666533758392, 0.9822518773417477]], [[-0.17519644325187023, 0.9845334967749417], [-0.17519644325187023, 0.9845334967749417]], [[-0.16279851216509478, 0.9866593355544919], [-0.16279851216509478, 0.9866593355544919]], [[-0.1503748218139381, 0.9886290573134224], [-0.1503748218139381, 0.9886290573134224]], [[-0.1379273379726542, 0.9904423503868245], [-0.1379273379726542, 0.9904423503868245]], [[-0.12545803018029758, 0.9920989278611683], [-0.12545803018029758, 0.9920989278611683]], [[-0.11296887142907358, 0.9935985276197029], [-0.11296887142907358, 0.9935985276197029]], [[-0.10046183785216964, 0.9949409123839287], [-0.10046183785216964, 0.9949409123839287]], [[-0.08793890841106214, 0.9961258697511428], [-0.08793890841106214, 0.9961258697511428]], [[-0.07540206458240344, 0.9971532122280462], [-0.07540206458240344, 0.9971532122280462]], [[-0.06285329004448297, 0.9980227772604111], [-0.06285329004448297, 0.9980227772604111]], [[-0.05029457036336817, 0.9987344272588005], [-0.05029457036336817, 0.9987344272588005]], [[-0.037727892678718344, 0.99928804962034], [-0.037727892678718344, 0.99928804962034]], [[-0.025155245389377974, 0.9996835567465338], [-0.025155245389377974, 0.9996835567465338]], [[-0.012578617838742366, 0.9999208860571255], [-0.012578617838742366, 0.9999208860571255]], [[-4.898587196589413e-16, 1.0], [-4.898587196589413e-16, 1.0]]], "init_spikes": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], "n_neurons": 2} \ No newline at end of file diff --git a/tests/simulate_coupled_neurons_params.yml b/tests/simulate_coupled_neurons_params.yml deleted file mode 100644 index 1be6c9cc..00000000 --- a/tests/simulate_coupled_neurons_params.yml +++ /dev/null @@ -1,6292 +0,0 @@ -baseline_link_fr_: -- -3.0 -- -3.0 -basis_coeff_: -- - -0.004372 - - -0.02786 - - -0.04582 - - -0.0588 - - -0.06539 - - -0.06396 - - -0.05328 - - -0.03192 - - 0.0002296 - - 0.04143 - - 0.08794 - - 0.1483 - - 0.2053 - - 0.2483 - - 0.2892 - - 0.3093 - - 0.2917 - - 0.2225 - - 0.07357 - - -0.2711 - - -0.006235 - - -0.01047 - - 0.02189 - - 0.058 - - 0.09002 - - 0.1118 - - 0.1209 - - 0.1167 - - 0.09909 - - 0.07044 - - 0.03448 - - -0.01565 - - -0.06823 - - -0.1128 - - -0.1655 - - -0.2176 - - -0.2621 - - -0.2982 - - -0.3255 - - -0.3449 - - 0.5 - - 0.5 -- - -0.004637 - - 0.02223 - - 0.07071 - - 0.09572 - - 0.1012 - - 0.08923 - - 0.06464 - - 0.03076 - - -0.007911 - - -0.04737 - - -0.08429 - - -0.1249 - - -0.1582 - - -0.1827 - - -0.2081 - - -0.23 - - -0.2473 - - -0.2616 - - -0.2741 - - -0.287 - - 0.01127 - - 0.04864 - - 0.0544 - - 0.05082 - - 0.03975 - - 0.02393 - - 0.004725 - - -0.01763 - - -0.04202 - - -0.06744 - - -0.09269 - - -0.1231 - - -0.1522 - - -0.1763 - - -0.2051 - - -0.2348 - - -0.2629 - - -0.2896 - - -0.3149 - - -0.3389 - - 0.5 - - 0.5 -coupling_basis: -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0024979173609873673 - - 0.9975020826390129 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.11451325277931029 - - 0.8854867472206909 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.25013898844998006 - - 0.7498610115500185 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.3122501403134024 - - 0.687749859686596 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.28176761370807446 - - 0.7182323862919272 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.17383844924397923 - - 0.8261615507560222 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.04364762794083282 - - 0.9563523720591665 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9912618171282106 - - 0.008738182871789013 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.7892946476427273 - - 0.21070535235727128 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.3531647741677867 - - 0.6468352258322151 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.011883820048045501 - - 0.9881161799519544 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.7841665801263835 - - 0.21583341987361648 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.17688067665784446 - - 0.8231193233421555 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9253003862638604 - - 0.0746996137361397 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.2549435480705588 - - 0.7450564519294413 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9205258993369989 - - 0.07947410066300109 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.16827351931758228 - - 0.8317264806824178 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.7835282009408713 - - 0.21647179905912872 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.019118847416525586 - - 0.9808811525834744 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.4372031242218587 - - 0.5627968757781414 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9120243919870162 - - 0.08797560801298382 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.044222034278324274 - - 0.9557779657216758 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.40793669708774605 - - 0.5920633029122541 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.8283923698925478 - - 0.17160763010745222 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.9999802058373224 - - 1.9794162677666538e-05 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.1458111022283093 - - 0.8541888977716907 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.4778824971400245 - - 0.5221175028599756 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.803486827077907 - - 0.19651317292209308 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.0 - - 0.9824675828481839 - - 0.017532417151816082 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.029720664099906924 - - 0.9702793359000932 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.19724020774947038 - - 0.8027597922505296 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.44389603578613035 - - 0.5561039642138698 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.6909694421867117 - - 0.30903055781328825 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.8804498633788072 - - 0.1195501366211929 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.0 - - 0.9828262050955638 - - 0.017173794904436157 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.005816278861877466 - - 0.9941837211381226 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.07171948190677246 - - 0.9282805180932275 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.19211081158089233 - - 0.8078891884191077 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.3422365913893123 - - 0.6577634086106878 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.49997219806462273 - - 0.5000278019353773 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.6481581380891199 - - 0.3518418619108801 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.775227808426499 - - 0.22477219157350103 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.8747644272334134 - - 0.12523557276658664 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.9445228823471115 - - 0.05547711765288865 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.9852942394771702 - - 0.014705760522829736 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.0 - - 0.9998405276097415 - - 0.00015947239025848603 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.00798856965539202 - - 0.9920114303446079 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.03392307742054024 - - 0.9660769225794598 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.07373523476821137 - - 0.9262647652317886 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.12352988337197751 - - 0.8764701166280225 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.17990211564285485 - - 0.8200978843571451 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.2399997347398921 - - 0.7600002652601079 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.3015222924967669 - - 0.6984777075032332 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.36268149196393995 - - 0.63731850803606 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.42214108290743424 - - 0.5778589170925659 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.47894873221112266 - - 0.5210512677888774 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.5324679173051469 - - 0.46753208269485313 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.5823146093533313 - - 0.4176853906466687 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.6283012081735033 - - 0.3716987918264968 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.6703886551778314 - - 0.32961134482216864 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.7086466881407022 - - 0.2913533118592979 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.7432216468423799 - - 0.25677835315762026 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.7743109612271127 - - 0.22568903877288732 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.802143356101582 - - 0.197856643898418 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.82696381862707 - - 0.17303618137292998 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.8490224486822571 - - 0.15097755131774288 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.8685664156253453 - - 0.13143358437465474 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.8858343578296817 - - 0.11416564217031833 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9010526715389762 - - 0.09894732846102389 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9144332365128198 - - 0.08556676348718023 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9261722145965264 - - 0.07382778540347357 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9364496329422705 - - 0.06355036705772948 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9454295266061546 - - 0.05457047339384541 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9532604668007324 - - 0.04673953319926766 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9600763426393057 - - 0.039923657360694254 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9659972972699125 - - 0.03400270273008754 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.971130745291511 - - 0.028869254708488945 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.975572418558468 - - 0.024427581441531954 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9794074030288873 - - 0.020592596971112653 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9827111411428311 - - 0.017288858857168965 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9855503831123861 - - 0.014449616887613925 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9879840771076767 - - 0.012015922892323394 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9900641931482845 - - 0.009935806851715523 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9918364789707291 - - 0.008163521029270815 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9933411485659462 - - 0.006658851434053759 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9946135057219054 - - 0.005386494278094567 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9956845059646938 - - 0.004315494035306178 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9965812609202838 - - 0.0034187390797163486 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.997327489436671 - - 0.002672510563328956 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9979439199017871 - - 0.002056080098212898 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9984486481342357 - - 0.0015513518657642722 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9988574550621354 - - 0.0011425449378646424 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9991840881776304 - - 0.0008159118223696749 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.999440510488429 - - 0.0005594895115710874 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9996371204027914 - - 0.00036287959720865404 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.999782945694725 - - 0.00021705430527496627 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9998858144113889 - - 0.00011418558861114869 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.9999525053112863 - - 4.7494688713622946e-05 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 0.99998888016377 - - 1.1119836230089053e-05 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -- - 1.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 - - 0.0 -feedforward_input: -- - - 0.0 - - 1.0 - - - 0.0 - - 1.0 -- - - 0.012578617838741058 - - 0.9999208860571255 - - - 0.012578617838741058 - - 0.9999208860571255 -- - - 0.025155245389375847 - - 0.9996835567465339 - - - 0.025155245389375847 - - 0.9996835567465339 -- - - 0.03772789267871718 - - 0.99928804962034 - - - 0.03772789267871718 - - 0.99928804962034 -- - - 0.05029457036336618 - - 0.9987344272588006 - - - 0.05029457036336618 - - 0.9987344272588006 -- - - 0.06285329004448194 - - 0.9980227772604111 - - - 0.06285329004448194 - - 0.9980227772604111 -- - - 0.07540206458240159 - - 0.9971532122280464 - - - 0.07540206458240159 - - 0.9971532122280464 -- - - 0.08793890841106125 - - 0.9961258697511429 - - - 0.08793890841106125 - - 0.9961258697511429 -- - - 0.10046183785216795 - - 0.9949409123839288 - - - 0.10046183785216795 - - 0.9949409123839288 -- - - 0.11296887142907283 - - 0.9935985276197029 - - - 0.11296887142907283 - - 0.9935985276197029 -- - - 0.12545803018029603 - - 0.9920989278611685 - - - 0.12545803018029603 - - 0.9920989278611685 -- - - 0.13792733797265358 - - 0.9904423503868246 - - - 0.13792733797265358 - - 0.9904423503868246 -- - - 0.1503748218139367 - - 0.9886290573134227 - - - 0.1503748218139367 - - 0.9886290573134227 -- - - 0.1627985121650943 - - 0.986659335554492 - - - 0.1627985121650943 - - 0.986659335554492 -- - - 0.17519644325186898 - - 0.984533496774942 - - - 0.17519644325186898 - - 0.984533496774942 -- - - 0.18756665337583714 - - 0.9822518773417481 - - - 0.18756665337583714 - - 0.9822518773417481 -- - - 0.19990718522480458 - - 0.9798148382707295 - - - 0.19990718522480458 - - 0.9798148382707295 -- - - 0.21221608618250787 - - 0.9772227651694256 - - - 0.21221608618250787 - - 0.9772227651694256 -- - - 0.22449140863757258 - - 0.9744760681760832 - - - 0.22449140863757258 - - 0.9744760681760832 -- - - 0.23673121029167973 - - 0.9715751818947602 - - - 0.23673121029167973 - - 0.9715751818947602 -- - - 0.2489335544668916 - - 0.9685205653265598 - - - 0.2489335544668916 - - 0.9685205653265598 -- - - 0.2610965104120882 - - 0.9653127017970033 - - - 0.2610965104120882 - - 0.9653127017970033 -- - - 0.27321815360846585 - - 0.9619520988795548 - - - 0.27321815360846585 - - 0.9619520988795548 -- - - 0.28529656607404974 - - 0.9584392883153087 - - - 0.28529656607404974 - - 0.9584392883153087 -- - - 0.2973298366671723 - - 0.9547748259288535 - - - 0.2973298366671723 - - 0.9547748259288535 -- - - 0.30931606138886886 - - 0.9509592915403253 - - - 0.30931606138886886 - - 0.9509592915403253 -- - - 0.32125334368414366 - - 0.9469932888736633 - - - 0.32125334368414366 - - 0.9469932888736633 -- - - 0.33313979474205757 - - 0.9428774454610842 - - - 0.33313979474205757 - - 0.9428774454610842 -- - - 0.34497353379459045 - - 0.9386124125437894 - - - 0.34497353379459045 - - 0.9386124125437894 -- - - 0.3567526884142317 - - 0.9341988649689198 - - - 0.3567526884142317 - - 0.9341988649689198 -- - - 0.3684753948102499 - - 0.9296375010827771 - - - 0.3684753948102499 - - 0.9296375010827771 -- - - 0.38013979812359666 - - 0.924929042620325 - - - 0.38013979812359666 - - 0.924929042620325 -- - - 0.3917440527203973 - - 0.9200742345909914 - - - 0.3917440527203973 - - 0.9200742345909914 -- - - 0.4032863224839812 - - 0.915073845160786 - - - 0.4032863224839812 - - 0.915073845160786 -- - - 0.41476478110540693 - - 0.9099286655307568 - - - 0.41476478110540693 - - 0.9099286655307568 -- - - 0.4261776123724353 - - 0.9046395098117981 - - - 0.4261776123724353 - - 0.9046395098117981 -- - - 0.4375230104569043 - - 0.8992072148958368 - - - 0.4375230104569043 - - 0.8992072148958368 -- - - 0.4487991802004621 - - 0.8936326403234123 - - - 0.4487991802004621 - - 0.8936326403234123 -- - - 0.46000433739861096 - - 0.887916668147673 - - - 0.46000433739861096 - - 0.887916668147673 -- - - 0.47113670908301786 - - 0.8820602027948115 - - - 0.47113670908301786 - - 0.8820602027948115 -- - - 0.4821945338020477 - - 0.8760641709209582 - - - 0.4821945338020477 - - 0.8760641709209582 -- - - 0.4931760618994744 - - 0.8699295212655597 - - - 0.4931760618994744 - - 0.8699295212655597 -- - - 0.5040795557913246 - - 0.8636572245012607 - - - 0.5040795557913246 - - 0.8636572245012607 -- - - 0.5149032902408126 - - 0.8572482730803168 - - - 0.5149032902408126 - - 0.8572482730803168 -- - - 0.5256455526313207 - - 0.8507036810775614 - - - 0.5256455526313207 - - 0.8507036810775614 -- - - 0.5363046432373825 - - 0.8440244840299503 - - - 0.5363046432373825 - - 0.8440244840299503 -- - - 0.5468788754936273 - - 0.8372117387727107 - - - 0.5468788754936273 - - 0.8372117387727107 -- - - 0.5573665762616421 - - 0.8302665232721208 - - - 0.5573665762616421 - - 0.8302665232721208 -- - - 0.5677660860947078 - - 0.8231899364549453 - - - 0.5677660860947078 - - 0.8231899364549453 -- - - 0.5780757595003707 - - 0.8159830980345546 - - - 0.5780757595003707 - - 0.8159830980345546 -- - - 0.588293965200805 - - 0.8086471483337551 - - - 0.588293965200805 - - 0.8086471483337551 -- - - 0.5984190863909268 - - 0.8011832481043575 - - - 0.5984190863909268 - - 0.8011832481043575 -- - - 0.608449520994217 - - 0.7935925783435149 - - - 0.608449520994217 - - 0.7935925783435149 -- - - 0.6183836819162153 - - 0.7858763401068549 - - - 0.6183836819162153 - - 0.7858763401068549 -- - - 0.6282199972956423 - - 0.7780357543184395 - - - 0.6282199972956423 - - 0.7780357543184395 -- - - 0.6379569107531118 - - 0.7700720615775812 - - - 0.6379569107531118 - - 0.7700720615775812 -- - - 0.647592881637394 - - 0.7619865219625451 - - - 0.647592881637394 - - 0.7619865219625451 -- - - 0.6571263852691885 - - 0.7537804148311695 - - - 0.6571263852691885 - - 0.7537804148311695 -- - - 0.666555913182372 - - 0.7454550386184362 - - - 0.666555913182372 - - 0.7454550386184362 -- - - 0.675879973362679 - - 0.7370117106310213 - - - 0.675879973362679 - - 0.7370117106310213 -- - - 0.6850970904837809 - - 0.7284517668388609 - - - 0.6850970904837809 - - 0.7284517668388609 -- - - 0.6942058061407225 - - 0.7197765616637636 - - - 0.6942058061407225 - - 0.7197765616637636 -- - - 0.7032046790806838 - - 0.7109874677651024 - - - 0.7032046790806838 - - 0.7109874677651024 -- - - 0.7120922854310254 - - 0.7020858758226226 - - - 0.7120922854310254 - - 0.7020858758226226 -- - - 0.720867218924585 - - 0.6930731943163971 - - - 0.720867218924585 - - 0.6930731943163971 -- - - 0.7295280911221884 - - 0.6839508493039657 - - - 0.7295280911221884 - - 0.6839508493039657 -- - - 0.7380735316323389 - - 0.6747202841946927 - - - 0.7380735316323389 - - 0.6747202841946927 -- - - 0.746502188328052 - - 0.6653829595213794 - - - 0.746502188328052 - - 0.6653829595213794 -- - - 0.7548127275607989 - - 0.6559403527091677 - - - 0.7548127275607989 - - 0.6559403527091677 -- - - 0.7630038343715272 - - 0.6463939578417693 - - - 0.7630038343715272 - - 0.6463939578417693 -- - - 0.7710742126987247 - - 0.6367452854250606 - - - 0.7710742126987247 - - 0.6367452854250606 -- - - 0.7790225855834911 - - 0.6269958621480786 - - - 0.7790225855834911 - - 0.6269958621480786 -- - - 0.7868476953715899 - - 0.6171472306414553 - - - 0.7868476953715899 - - 0.6171472306414553 -- - - 0.7945483039124437 - - 0.6072009492333317 - - - 0.7945483039124437 - - 0.6072009492333317 -- - - 0.8021231927550437 - - 0.5971585917027863 - - - 0.8021231927550437 - - 0.5971585917027863 -- - - 0.809571163340744 - - 0.5870217470308187 - - - 0.809571163340744 - - 0.5870217470308187 -- - - 0.8168910371929053 - - 0.5767920191489297 - - - 0.8168910371929053 - - 0.5767920191489297 -- - - 0.8240816561033644 - - 0.566471026685334 - - - 0.8240816561033644 - - 0.566471026685334 -- - - 0.8311418823156935 - - 0.5560604027088476 - - - 0.8311418823156935 - - 0.5560604027088476 -- - - 0.8380705987052264 - - 0.545561794470492 - - - 0.8380705987052264 - - 0.545561794470492 -- - - 0.8448667089558177 - - 0.5349768631428518 - - - 0.8448667089558177 - - 0.5349768631428518 -- - - 0.8515291377333112 - - 0.5243072835572319 - - - 0.8515291377333112 - - 0.5243072835572319 -- - - 0.8580568308556875 - - 0.5135547439386516 - - - 0.8580568308556875 - - 0.5135547439386516 -- - - 0.8644487554598649 - - 0.5027209456387218 - - - 0.8644487554598649 - - 0.5027209456387218 -- - - 0.8707039001651274 - - 0.4918076028664418 - - - 0.8707039001651274 - - 0.4918076028664418 -- - - 0.8768212752331536 - - 0.4808164424169648 - - - 0.8768212752331536 - - 0.4808164424169648 -- - - 0.8827999127246196 - - 0.4697492033983709 - - - 0.8827999127246196 - - 0.4697492033983709 -- - - 0.8886388666523558 - - 0.45860763695649104 - - - 0.8886388666523558 - - 0.45860763695649104 -- - - 0.8943372131310272 - - 0.4473935059978269 - - - 0.8943372131310272 - - 0.4473935059978269 -- - - 0.8998940505233182 - - 0.4361085849106111 - - - 0.8998940505233182 - - 0.4361085849106111 -- - - 0.9053084995825966 - - 0.42475465928404793 - - - 0.9053084995825966 - - 0.42475465928404793 -- - - 0.9105797035920355 - - 0.4133335256257842 - - - 0.9105797035920355 - - 0.4133335256257842 -- - - 0.9157068285001692 - - 0.4018469910776512 - - - 0.9157068285001692 - - 0.4018469910776512 -- - - 0.920689063052863 - - 0.3902968731297256 - - - 0.920689063052863 - - 0.3902968731297256 -- - - 0.9255256189216778 - - 0.3786849993327503 - - - 0.9255256189216778 - - 0.3786849993327503 -- - - 0.9302157308286042 - - 0.3670132070089654 - - - 0.9302157308286042 - - 0.3670132070089654 -- - - 0.934758656667151 - - 0.35528334296139374 - - - 0.934758656667151 - - 0.35528334296139374 -- - - 0.9391536776197676 - - 0.34349726318162344 - - - 0.9391536776197676 - - 0.34349726318162344 -- - - 0.9434000982715812 - - 0.3316568325561391 - - - 0.9434000982715812 - - 0.3316568325561391 -- - - 0.9474972467204298 - - 0.31976392457124536 - - - 0.9474972467204298 - - 0.31976392457124536 -- - - 0.9514444746831766 - - 0.30782042101662793 - - - 0.9514444746831766 - - 0.30782042101662793 -- - - 0.9552411575982869 - - 0.2958282116876025 - - - 0.9552411575982869 - - 0.2958282116876025 -- - - 0.9588866947246497 - - 0.28378919408609693 - - - 0.9588866947246497 - - 0.28378919408609693 -- - - 0.9623805092366334 - - 0.27170527312041276 - - - 0.9623805092366334 - - 0.27170527312041276 -- - - 0.9657220483153546 - - 0.25957836080381586 - - - 0.9657220483153546 - - 0.25957836080381586 -- - - 0.9689107832361495 - - 0.24741037595200252 - - - 0.9689107832361495 - - 0.24741037595200252 -- - - 0.9719462094522335 - - 0.23520324387949015 - - - 0.9719462094522335 - - 0.23520324387949015 -- - - 0.9748278466745341 - - 0.2229588960949774 - - - 0.9748278466745341 - - 0.2229588960949774 -- - - 0.9775552389476861 - - 0.21067926999572642 - - - 0.9775552389476861 - - 0.21067926999572642 -- - - 0.9801279547221765 - - 0.19836630856101303 - - - 0.9801279547221765 - - 0.19836630856101303 -- - - 0.9825455869226277 - - 0.18602196004469224 - - - 0.9825455869226277 - - 0.18602196004469224 -- - - 0.984807753012208 - - 0.17364817766693041 - - - 0.984807753012208 - - 0.17364817766693041 -- - - 0.98691409505316 - - 0.16124691930515242 - - - 0.98691409505316 - - 0.16124691930515242 -- - - 0.9888642797634357 - - 0.14882014718424924 - - - 0.9888642797634357 - - 0.14882014718424924 -- - - 0.9906579985694317 - - 0.1363698275661 - - - 0.9906579985694317 - - 0.1363698275661 -- - - 0.9922949676548136 - - 0.12389793043845522 - - - 0.9922949676548136 - - 0.12389793043845522 -- - - 0.9937749280054242 - - 0.11140642920322849 - - - 0.9937749280054242 - - 0.11140642920322849 -- - - 0.995097645450266 - - 0.09889730036424986 - - - 0.995097645450266 - - 0.09889730036424986 -- - - 0.9962629106985543 - - 0.08637252321452853 - - - 0.9962629106985543 - - 0.08637252321452853 -- - - 0.9972705393728327 - - 0.07383407952307214 - - - 0.9972705393728327 - - 0.07383407952307214 -- - - 0.9981203720381463 - - 0.06128395322131638 - - - 0.9981203720381463 - - 0.06128395322131638 -- - - 0.9988122742272691 - - 0.04872413008921228 - - - 0.9988122742272691 - - 0.04872413008921228 -- - - 0.9993461364619809 - - 0.036156597441019206 - - - 0.9993461364619809 - - 0.036156597441019206 -- - - 0.9997218742703887 - - 0.023583343810857166 - - - 0.9997218742703887 - - 0.023583343810857166 -- - - 0.9999394282002937 - - 0.011006358638064812 - - - 0.9999394282002937 - - 0.011006358638064812 -- - - 0.9999987638285974 - - -0.001572368047584414 - - - 0.9999987638285974 - - -0.001572368047584414 -- - - 0.9998998717667489 - - -0.014150845940761853 - - - 0.9998998717667489 - - -0.014150845940761853 -- - - 0.9996427676622299 - - -0.026727084775504745 - - - 0.9996427676622299 - - -0.026727084775504745 -- - - 0.9992274921960794 - - -0.03929909464013115 - - - 0.9992274921960794 - - -0.03929909464013115 -- - - 0.9986541110764565 - - -0.0518648862921008 - - - 0.9986541110764565 - - -0.0518648862921008 -- - - 0.9979227150282433 - - -0.06442247147276806 - - - 0.9979227150282433 - - -0.06442247147276806 -- - - 0.9970334197786902 - - -0.07696986322197923 - - - 0.9970334197786902 - - -0.07696986322197923 -- - - 0.9959863660391044 - - -0.08950507619246638 - - - 0.9959863660391044 - - -0.08950507619246638 -- - - 0.9947817194825853 - - -0.10202612696398403 - - - 0.9947817194825853 - - -0.10202612696398403 -- - - 0.9934196707178107 - - -0.11453103435714077 - - - 0.9934196707178107 - - -0.11453103435714077 -- - - 0.991900435258877 - - -0.12701781974687854 - - - 0.991900435258877 - - -0.12701781974687854 -- - - 0.9902242534911986 - - -0.1394845073755453 - - - 0.9902242534911986 - - -0.1394845073755453 -- - - 0.9883913906334728 - - -0.15192912466551547 - - - 0.9883913906334728 - - -0.15192912466551547 -- - - 0.9864021366957146 - - -0.16434970253130593 - - - 0.9864021366957146 - - -0.16434970253130593 -- - - 0.9842568064333687 - - -0.17674427569114137 - - - 0.9842568064333687 - - -0.17674427569114137 -- - - 0.9819557392975067 - - -0.18911088297791617 - - - 0.9819557392975067 - - -0.18911088297791617 -- - - 0.9794992993811165 - - -0.20144756764950503 - - - 0.9794992993811165 - - -0.20144756764950503 -- - - 0.9768878753614926 - - -0.21375237769837538 - - - 0.9768878753614926 - - -0.21375237769837538 -- - - 0.9741218804387363 - - -0.22602336616044894 - - - 0.9741218804387363 - - -0.22602336616044894 -- - - 0.9712017522703763 - - -0.23825859142316483 - - - 0.9712017522703763 - - -0.23825859142316483 -- - - 0.9681279529021188 - - -0.25045611753269825 - - - 0.9681279529021188 - - -0.25045611753269825 -- - - 0.9649009686947391 - - -0.2626140145002818 - - - 0.9649009686947391 - - -0.2626140145002818 -- - - 0.9615213102471255 - - -0.27473035860758266 - - - 0.9615213102471255 - - -0.27473035860758266 -- - - 0.9579895123154889 - - -0.28680323271109 - - - 0.9579895123154889 - - -0.28680323271109 -- - - 0.9543061337287488 - - -0.29883072654545967 - - - 0.9543061337287488 - - -0.29883072654545967 -- - - 0.9504717573001116 - - -0.310810937025771 - - - 0.9504717573001116 - - -0.310810937025771 -- - - 0.9464869897348526 - - -0.32274196854864906 - - - 0.9464869897348526 - - -0.32274196854864906 -- - - 0.9423524615343186 - - -0.33462193329220136 - - - 0.9423524615343186 - - -0.33462193329220136 -- - - 0.9380688268961659 - - -0.3464489515147234 - - - 0.9380688268961659 - - -0.3464489515147234 -- - - 0.9336367636108462 - - -0.3582211518521272 - - - 0.9336367636108462 - - -0.3582211518521272 -- - - 0.9290569729543628 - - -0.369936671614043 - - - 0.9290569729543628 - - -0.369936671614043 -- - - 0.9243301795773085 - - -0.38159365707854837 - - - 0.9243301795773085 - - -0.38159365707854837 -- - - 0.9194571313902055 - - -0.3931902637854787 - - - 0.9194571313902055 - - -0.3931902637854787 -- - - 0.9144385994451658 - - -0.40472465682827324 - - - 0.9144385994451658 - - -0.40472465682827324 -- - - 0.9092753778138886 - - -0.4161950111443075 - - - 0.9092753778138886 - - -0.4161950111443075 -- - - 0.9039682834620162 - - -0.42759951180366895 - - - 0.9039682834620162 - - -0.42759951180366895 -- - - 0.8985181561198674 - - -0.4389363542963303 - - - 0.8985181561198674 - - -0.4389363542963303 -- - - 0.8929258581495686 - - -0.450203744817673 - - - 0.8929258581495686 - - -0.450203744817673 -- - - 0.8871922744086043 - - -0.46139990055231683 - - - 0.8871922744086043 - - -0.46139990055231683 -- - - 0.881318312109807 - - -0.47252304995621186 - - - 0.881318312109807 - - -0.47252304995621186 -- - - 0.8753049006778131 - - -0.4835714330369443 - - - 0.8753049006778131 - - -0.4835714330369443 -- - - 0.869152991601999 - - -0.4945433016322186 - - - 0.869152991601999 - - -0.4945433016322186 -- - - 0.8628635582859312 - - -0.5054369196864643 - - - 0.8628635582859312 - - -0.5054369196864643 -- - - 0.856437595893346 - - -0.5162505635255284 - - - 0.856437595893346 - - -0.5162505635255284 -- - - 0.8498761211906867 - - -0.5269825221294092 - - - 0.8498761211906867 - - -0.5269825221294092 -- - - 0.8431801723862224 - - -0.5376310974029872 - - - 0.8431801723862224 - - -0.5376310974029872 -- - - 0.8363508089657762 - - -0.5481946044447097 - - - 0.8363508089657762 - - -0.5481946044447097 -- - - 0.8293891115250829 - - -0.5586713718131919 - - - 0.8293891115250829 - - -0.5586713718131919 -- - - 0.8222961815988096 - - -0.5690597417916836 - - - 0.8222961815988096 - - -0.5690597417916836 -- - - 0.8150731414862624 - - -0.5793580706503667 - - - 0.8150731414862624 - - -0.5793580706503667 -- - - 0.8077211340738071 - - -0.5895647289064391 - - - 0.8077211340738071 - - -0.5895647289064391 -- - - 0.800241322654032 - - -0.5996781015819448 - - - 0.800241322654032 - - -0.5996781015819448 -- - - 0.7926348907416848 - - -0.6096965884593069 - - - 0.7926348907416848 - - -0.6096965884593069 -- - - 0.7849030418864046 - - -0.6196186043345285 - - - 0.7849030418864046 - - -0.6196186043345285 -- - - 0.7770469994822886 - - -0.6294425792680156 - - - 0.7770469994822886 - - -0.6294425792680156 -- - - 0.769068006574317 - - -0.6391669588329847 - - - 0.769068006574317 - - -0.6391669588329847 -- - - 0.7609673256616678 - - -0.648790204361417 - - - 0.7609673256616678 - - -0.648790204361417 -- - - 0.7527462384979551 - - -0.6583107931875185 - - - 0.7527462384979551 - - -0.6583107931875185 -- - - 0.744406045888419 - - -0.6677272188886485 - - - 0.744406045888419 - - -0.6677272188886485 -- - - 0.7359480674841035 - - -0.6770379915236763 - - - 0.7359480674841035 - - -0.6770379915236763 -- - - 0.7273736415730488 - - -0.6862416378687335 - - - 0.7273736415730488 - - -0.6862416378687335 -- - - 0.7186841248685385 - - -0.6953367016503177 - - - 0.7186841248685385 - - -0.6953367016503177 -- - - 0.7098808922944289 - - -0.7043217437757161 - - - 0.7098808922944289 - - -0.7043217437757161 -- - - 0.7009653367675978 - - -0.7131953425607098 - - - 0.7009653367675978 - - -0.7131953425607098 -- - - 0.6919388689775463 - - -0.7219560939545244 - - - 0.6919388689775463 - - -0.7219560939545244 -- - - 0.6828029171631891 - - -0.7306026117619886 - - - 0.6828029171631891 - - -0.7306026117619886 -- - - 0.673558926886866 - - -0.739133527862871 - - - 0.673558926886866 - - -0.739133527862871 -- - - 0.6642083608056142 - - -0.7475474924283534 - - - 0.6642083608056142 - - -0.7475474924283534 -- - - 0.6547526984397353 - - -0.7558431741346118 - - - 0.6547526984397353 - - -0.7558431741346118 -- - - 0.6451934359386937 - - -0.764019260373469 - - - 0.6451934359386937 - - -0.764019260373469 -- - - 0.6355320858443845 - - -0.7720744574600859 - - - 0.6355320858443845 - - -0.7720744574600859 -- - - 0.6257701768518059 - - -0.7800074908376582 - - - 0.6257701768518059 - - -0.7800074908376582 -- - - 0.6159092535671797 - - -0.7878171052790867 - - - 0.6159092535671797 - - -0.7878171052790867 -- - - 0.6059508762635484 - - -0.7955020650855897 - - - 0.6059508762635484 - - -0.7955020650855897 -- - - 0.5958966206338979 - - -0.8030611542822255 - - - 0.5958966206338979 - - -0.8030611542822255 -- - - 0.5857480775418397 - - -0.8104931768102919 - - - 0.5857480775418397 - - -0.8104931768102919 -- - - 0.5755068527698903 - - -0.8177969567165775 - - - 0.5755068527698903 - - -0.8177969567165775 -- - - 0.5651745667653929 - - -0.8249713383394301 - - - 0.5651745667653929 - - -0.8249713383394301 -- - - 0.5547528543841173 - - -0.8320151864916135 - - - 0.5547528543841173 - - -0.8320151864916135 -- - - 0.5442433646315792 - - -0.8389273866399272 - - - 0.5442433646315792 - - -0.8389273866399272 -- - - 0.5336477604021226 - - -0.8457068450815559 - - - 0.5336477604021226 - - -0.8457068450815559 -- - - 0.5229677182158028 - - -0.8523524891171238 - - - 0.5229677182158028 - - -0.8523524891171238 -- - - 0.5122049279531147 - - -0.8588632672204258 - - - 0.5122049279531147 - - -0.8588632672204258 -- - - 0.5013610925876063 - - -0.865238149204808 - - - 0.5013610925876063 - - -0.865238149204808 -- - - 0.49043792791642066 - - -0.8714761263861723 - - - 0.49043792791642066 - - -0.8714761263861723 -- - - 0.47943716228880995 - - -0.8775762117425775 - - - 0.47943716228880995 - - -0.8775762117425775 -- - - 0.4683605363326608 - - -0.8835374400704151 - - - 0.4683605363326608 - - -0.8835374400704151 -- - - 0.4572098026790794 - - -0.8893588681371302 - - - 0.4572098026790794 - - -0.8893588681371302 -- - - 0.44598672568507636 - - -0.8950395748304677 - - - 0.44598672568507636 - - -0.8950395748304677 -- - - 0.4346930811543961 - - -0.9005786613042182 - - - 0.4346930811543961 - - -0.9005786613042182 -- - - 0.4233306560565345 - - -0.9059752511204399 - - - 0.4233306560565345 - - -0.9059752511204399 -- - - 0.4119012482439928 - - -0.9112284903881356 - - - 0.4119012482439928 - - -0.9112284903881356 -- - - 0.40040666616780407 - - -0.916337547898363 - - - 0.40040666616780407 - - -0.916337547898363 -- - - 0.3888487285913878 - - -0.9213016152557539 - - - 0.3888487285913878 - - -0.9213016152557539 -- - - 0.37722926430277026 - - -0.9261199070064258 - - - 0.37722926430277026 - - -0.9261199070064258 -- - - 0.36555011182521946 - - -0.9307916607622618 - - - 0.36555011182521946 - - -0.9307916607622618 -- - - 0.3538131191263388 - - -0.9353161373215428 - - - 0.3538131191263388 - - -0.9353161373215428 -- - - 0.3420201433256689 - - -0.9396926207859083 - - - 0.3420201433256689 - - -0.9396926207859083 -- - - 0.330173050400837 - - -0.9439204186736329 - - - 0.330173050400837 - - -0.9439204186736329 -- - - 0.3182737148923088 - - -0.9479988620291954 - - - 0.3182737148923088 - - -0.9479988620291954 -- - - 0.3063240196067838 - - -0.9519273055291264 - - - 0.3063240196067838 - - -0.9519273055291264 -- - - 0.29432585531928224 - - -0.9557051275841167 - - - 0.29432585531928224 - - -0.9557051275841167 -- - - 0.2822811204739722 - - -0.9593317304373701 - - - 0.2822811204739722 - - -0.9593317304373701 -- - - 0.27019172088378224 - - -0.9628065402591843 - - - 0.27019172088378224 - - -0.9628065402591843 -- - - 0.25805956942885044 - - -0.9661290072377479 - - - 0.25805956942885044 - - -0.9661290072377479 -- - - 0.24588658575385056 - - -0.9692986056661355 - - - 0.24588658575385056 - - -0.9692986056661355 -- - - 0.23367469596425278 - - -0.9723148340254889 - - - 0.23367469596425278 - - -0.9723148340254889 -- - - 0.22142583232155955 - - -0.975177215064372 - - - 0.22142583232155955 - - -0.975177215064372 -- - - 0.20914193293756786 - - -0.977885295874285 - - - 0.20914193293756786 - - -0.977885295874285 -- - - 0.19682494146770554 - - -0.9804386479613267 - - - 0.19682494146770554 - - -0.9804386479613267 -- - - 0.18447680680349254 - - -0.9828368673139948 - - - 0.18447680680349254 - - -0.9828368673139948 -- - - 0.17209948276416928 - - -0.9850795744671115 - - - 0.17209948276416928 - - -0.9850795744671115 -- - - 0.15969492778754976 - - -0.9871664145618657 - - - 0.15969492778754976 - - -0.9871664145618657 -- - - 0.14726510462014156 - - -0.9890970574019613 - - - 0.14726510462014156 - - -0.9890970574019613 -- - - 0.1348119800065847 - - -0.9908711975058636 - - - 0.1348119800065847 - - -0.9908711975058636 -- - - 0.12233752437845731 - - -0.992488554155135 - - - 0.12233752437845731 - - -0.992488554155135 -- - - 0.1098437115425002 - - -0.9939488714388522 - - - 0.1098437115425002 - - -0.9939488714388522 -- - - 0.09733251836830287 - - -0.9952519182940991 - - - 0.09733251836830287 - - -0.9952519182940991 -- - - 0.0848059244755095 - - -0.9963974885425265 - - - 0.0848059244755095 - - -0.9963974885425265 -- - - 0.07226591192058739 - - -0.9973854009229761 - - - 0.07226591192058739 - - -0.9973854009229761 -- - - 0.05971446488321034 - - -0.9982154991201608 - - - 0.05971446488321034 - - -0.9982154991201608 -- - - 0.04715356935230619 - - -0.9988876517893978 - - - 0.04715356935230619 - - -0.9988876517893978 -- - - 0.034585212811817465 - - -0.9994017525773913 - - - 0.034585212811817465 - - -0.9994017525773913 -- - - 0.022011383926227784 - - -0.9997577201390606 - - - 0.022011383926227784 - - -0.9997577201390606 -- - - 0.009434072225897046 - - -0.999955498150411 - - - 0.009434072225897046 - - -0.999955498150411 -- - - -0.0031447322077359985 - - -0.9999950553174459 - - - -0.0031447322077359985 - - -0.9999950553174459 -- - - -0.015723039057040564 - - -0.9998763853811183 - - - -0.015723039057040564 - - -0.9998763853811183 -- - - -0.02829885808311759 - - -0.9995995071183217 - - - -0.02829885808311759 - - -0.9995995071183217 -- - - -0.04087019944071145 - - -0.9991644643389178 - - - -0.04087019944071145 - - -0.9991644643389178 -- - - -0.053435073993057226 - - -0.9985713258788059 - - - -0.053435073993057226 - - -0.9985713258788059 -- - - -0.06599149362662023 - - -0.9978201855890307 - - - -0.06599149362662023 - - -0.9978201855890307 -- - - -0.07853747156566927 - - -0.996911162320932 - - - -0.07853747156566927 - - -0.996911162320932 -- - - -0.09107102268664041 - - -0.9958443999073396 - - - -0.09107102268664041 - - -0.9958443999073396 -- - - -0.10359016383223883 - - -0.9946200671398149 - - - -0.10359016383223883 - - -0.9946200671398149 -- - - -0.11609291412522968 - - -0.993238357741943 - - - -0.11609291412522968 - - -0.993238357741943 -- - - -0.12857729528186848 - - -0.9916994903386808 - - - -0.12857729528186848 - - -0.9916994903386808 -- - - -0.14104133192491908 - - -0.9900037084217639 - - - -0.14104133192491908 - - -0.9900037084217639 -- - - -0.15348305189621594 - - -0.9881512803111796 - - - -0.15348305189621594 - - -0.9881512803111796 -- - - -0.16590048656871298 - - -0.9861424991127116 - - - -0.16590048656871298 - - -0.9861424991127116 -- - - -0.1782916711579755 - - -0.9839776826715616 - - - -0.1782916711579755 - - -0.9839776826715616 -- - - -0.19065464503306404 - - -0.9816571735220583 - - - -0.19065464503306404 - - -0.9816571735220583 -- - - -0.20298745202676116 - - -0.979181338833458 - - - -0.20298745202676116 - - -0.979181338833458 -- - - -0.2152881407450901 - - -0.9765505703518493 - - - -0.2152881407450901 - - -0.9765505703518493 -- - - -0.2275547648760821 - - -0.9737652843381669 - - - -0.2275547648760821 - - -0.9737652843381669 -- - - -0.23978538349773562 - - -0.9708259215023277 - - - -0.23978538349773562 - - -0.9708259215023277 -- - - -0.25197806138512474 - - -0.967732946933499 - - - -0.25197806138512474 - - -0.967732946933499 -- - - -0.2641308693166058 - - -0.9644868500265071 - - - -0.2641308693166058 - - -0.9644868500265071 -- - - -0.2762418843790738 - - -0.9610881444044029 - - - -0.2762418843790738 - - -0.9610881444044029 -- - - -0.2883091902722216 - - -0.9575373678371909 - - - -0.2883091902722216 - - -0.9575373678371909 -- - - -0.3003308776117502 - - -0.9538350821567405 - - - -0.3003308776117502 - - -0.9538350821567405 -- - - -0.31230504423148914 - - -0.9499818731678872 - - - -0.31230504423148914 - - -0.9499818731678872 -- - - -0.32422979548437053 - - -0.9459783505557425 - - - -0.32422979548437053 - - -0.9459783505557425 -- - - -0.33610324454221563 - - -0.9418251477892251 - - - -0.33610324454221563 - - -0.9418251477892251 -- - - -0.34792351269428334 - - -0.9375229220208277 - - - -0.34792351269428334 - - -0.9375229220208277 -- - - -0.3596887296445355 - - -0.9330723539826374 - - - -0.3596887296445355 - - -0.9330723539826374 -- - - -0.3713970338075679 - - -0.9284741478786258 - - - -0.3713970338075679 - - -0.9284741478786258 -- - - -0.3830465726031674 - - -0.9237290312732227 - - - -0.3830465726031674 - - -0.9237290312732227 -- - - -0.3946355027494405 - - -0.9188377549761962 - - - -0.3946355027494405 - - -0.9188377549761962 -- - - -0.406161990554472 - - -0.9138010929238535 - - - -0.406161990554472 - - -0.9138010929238535 -- - - -0.41762421220646645 - - -0.9086198420565822 - - - -0.41762421220646645 - - -0.9086198420565822 -- - - -0.4290203540623263 - - -0.9032948221927524 - - - -0.4290203540623263 - - -0.9032948221927524 -- - - -0.44034861293461913 - - -0.8978268758989992 - - - -0.44034861293461913 - - -0.8978268758989992 -- - - -0.4516071963768948 - - -0.892216868356904 - - - -0.4516071963768948 - - -0.892216868356904 -- - - -0.46279432296729867 - - -0.8864656872260989 - - - -0.46279432296729867 - - -0.8864656872260989 -- - - -0.47390822259044274 - - -0.8805742425038149 - - - -0.47390822259044274 - - -0.8805742425038149 -- - - -0.4849471367174873 - - -0.8745434663808944 - - - -0.4849471367174873 - - -0.8745434663808944 -- - - -0.495909318684389 - - -0.8683743130942929 - - - -0.495909318684389 - - -0.8683743130942929 -- - - -0.5067930339682724 - - -0.8620677587760915 - - - -0.5067930339682724 - - -0.8620677587760915 -- - - -0.5175965604618782 - - -0.8556248012990468 - - - -0.5175965604618782 - - -0.8556248012990468 -- - - -0.5283181887460511 - - -0.849046460118698 - - - -0.5283181887460511 - - -0.849046460118698 -- - - -0.538956222360216 - - -0.842333776112062 - - - -0.538956222360216 - - -0.842333776112062 -- - - -0.5495089780708056 - - -0.8354878114129367 - - - -0.5495089780708056 - - -0.8354878114129367 -- - - -0.5599747861375949 - - -0.8285096492438424 - - - -0.5599747861375949 - - -0.8285096492438424 -- - - -0.5703519905779012 - - -0.8214003937446254 - - - -0.5703519905779012 - - -0.8214003937446254 -- - - -0.5806389494286053 - - -0.814161169797753 - - - -0.5806389494286053 - - -0.814161169797753 -- - - -0.5908340350059578 - - -0.8067931228503245 - - - -0.5908340350059578 - - -0.8067931228503245 -- - - -0.6009356341631226 - - -0.7992974187328304 - - - -0.6009356341631226 - - -0.7992974187328304 -- - - -0.6109421485454225 - - -0.7916752434746857 - - - -0.6109421485454225 - - -0.7916752434746857 -- - - -0.6208519948432432 - - -0.7839278031165661 - - - -0.6208519948432432 - - -0.7839278031165661 -- - - -0.630663605042557 - - -0.7760563235195791 - - - -0.630663605042557 - - -0.7760563235195791 -- - - -0.6403754266730258 - - -0.7680620501712998 - - - -0.6403754266730258 - - -0.7680620501712998 -- - - -0.6499859230536464 - - -0.7599462479886977 - - - -0.6499859230536464 - - -0.7599462479886977 -- - - -0.6594935735358957 - - -0.7517102011179935 - - - -0.6594935735358957 - - -0.7517102011179935 -- - - -0.6688968737443391 - - -0.7433552127314704 - - - -0.6688968737443391 - - -0.7433552127314704 -- - - -0.6781943358146659 - - -0.7348826048212762 - - - -0.6781943358146659 - - -0.7348826048212762 -- - - -0.6873844886291098 - - -0.7262937179902474 - - - -0.6873844886291098 - - -0.7262937179902474 -- - - -0.6964658780492216 - - -0.717589911239788 - - - -0.6964658780492216 - - -0.717589911239788 -- - - -0.7054370671459529 - - -0.7087725617548385 - - - -0.7054370671459529 - - -0.7087725617548385 -- - - -0.7142966364270207 - - -0.6998430646859656 - - - -0.7142966364270207 - - -0.6998430646859656 -- - - -0.723043184061509 - - -0.6908028329286112 - - - -0.723043184061509 - - -0.6908028329286112 -- - - -0.731675326101678 - - -0.6816532968995332 - - - -0.731675326101678 - - -0.6816532968995332 -- - - -0.7401916967019432 - - -0.6723959043104729 - - - -0.7401916967019432 - - -0.6723959043104729 -- - - -0.7485909483349904 - - -0.6630321199390868 - - - -0.7485909483349904 - - -0.6630321199390868 -- - - -0.7568717520049916 - - -0.6535634253971795 - - - -0.7568717520049916 - - -0.6535634253971795 -- - - -0.7650327974578898 - - -0.6439913188962686 - - - -0.7650327974578898 - - -0.6439913188962686 -- - - -0.7730727933887175 - - -0.634317315010528 - - - -0.7730727933887175 - - -0.634317315010528 -- - - -0.7809904676459172 - - -0.6245429444371393 - - - -0.7809904676459172 - - -0.6245429444371393 -- - - -0.788784567432631 - - -0.6146697537540928 - - - -0.788784567432631 - - -0.6146697537540928 -- - - -0.7964538595049286 - - -0.6046993051754759 - - - -0.7964538595049286 - - -0.6046993051754759 -- - - -0.8039971303669401 - - -0.5946331763042871 - - - -0.8039971303669401 - - -0.5946331763042871 -- - - -0.8114131864628653 - - -0.5844729598828156 - - - -0.8114131864628653 - - -0.5844729598828156 -- - - -0.8187008543658276 - - -0.5742202635406243 - - - -0.8187008543658276 - - -0.5742202635406243 -- - - -0.825858980963543 - - -0.5638767095401779 - - - -0.825858980963543 - - -0.5638767095401779 -- - - -0.8328864336407734 - - -0.5534439345201586 - - - -0.8328864336407734 - - -0.5534439345201586 -- - - -0.8397821004585396 - - -0.5429235892364995 - - - -0.8397821004585396 - - -0.5429235892364995 -- - - -0.8465448903300604 - - -0.5323173383011922 - - - -0.8465448903300604 - - -0.5323173383011922 -- - - -0.8531737331933926 - - -0.521626859918898 - - - -0.8531737331933926 - - -0.521626859918898 -- - - -0.8596675801807451 - - -0.5108538456214089 - - - -0.8596675801807451 - - -0.5108538456214089 -- - - -0.8660254037844384 - - -0.5000000000000004 - - - -0.8660254037844384 - - -0.5000000000000004 -- - - -0.872246198019486 - - -0.4890670404357173 - - - -0.872246198019486 - - -0.4890670404357173 -- - - -0.8783289785827684 - - -0.4780566968276366 - - - -0.8783289785827684 - - -0.4780566968276366 -- - - -0.8842727830087774 - - -0.46697071131914863 - - - -0.8842727830087774 - - -0.46697071131914863 -- - - -0.8900766708219056 - - -0.4558108380223019 - - - -0.8900766708219056 - - -0.4558108380223019 -- - - -0.895739723685255 - - -0.4445788427402534 - - - -0.895739723685255 - - -0.4445788427402534 -- - - -0.9012610455459443 - - -0.4332765026878693 - - - -0.9012610455459443 - - -0.4332765026878693 -- - - -0.9066397627768893 - - -0.4219056062105194 - - - -0.9066397627768893 - - -0.4219056062105194 -- - - -0.9118750243150336 - - -0.410467952501114 - - - -0.9118750243150336 - - -0.410467952501114 -- - - -0.9169660017960133 - - -0.39896535131541655 - - - -0.9169660017960133 - - -0.39896535131541655 -- - - -0.921911889685225 - - -0.38739962268569333 - - - -0.921911889685225 - - -0.38739962268569333 -- - - -0.9267119054052849 - - -0.37577259663273255 - - - -0.9267119054052849 - - -0.37577259663273255 -- - - -0.931365289459854 - - -0.3640861128762842 - - - -0.931365289459854 - - -0.3640861128762842 -- - - -0.9358713055538119 - - -0.3523420205439648 - - - -0.9358713055538119 - - -0.3523420205439648 -- - - -0.9402292407097588 - - -0.3405421778786742 - - - -0.9402292407097588 - - -0.3405421778786742 -- - - -0.9444384053808287 - - -0.32868845194456947 - - - -0.9444384053808287 - - -0.32868845194456947 -- - - -0.948498133559795 - - -0.3167827183316434 - - - -0.948498133559795 - - -0.3167827183316434 -- - - -0.9524077828844512 - - -0.30482686085895394 - - - -0.9524077828844512 - - -0.30482686085895394 -- - - -0.9561667347392507 - - -0.2928227712765512 - - - -0.9561667347392507 - - -0.2928227712765512 -- - - -0.959774394353189 - - -0.28077234896614933 - - - -0.959774394353189 - - -0.28077234896614933 -- - - -0.9632301908939126 - - -0.26867750064059465 - - - -0.9632301908939126 - - -0.26867750064059465 -- - - -0.9665335775580413 - - -0.25654014004216524 - - - -0.9665335775580413 - - -0.25654014004216524 -- - - -0.9696840316576876 - - -0.2443621876397672 - - - -0.9696840316576876 - - -0.2443621876397672 -- - - -0.97268105470316 - - -0.2321455703250619 - - - -0.97268105470316 - - -0.2321455703250619 -- - - -0.9755241724818386 - - -0.21989222110757806 - - - -0.9755241724818386 - - -0.21989222110757806 -- - - -0.9782129351332083 - - -0.2076040788088557 - - - -0.9782129351332083 - - -0.2076040788088557 -- - - -0.9807469172200395 - - -0.19528308775567055 - - - -0.9807469172200395 - - -0.19528308775567055 -- - - -0.9831257177957041 - - -0.18293119747238726 - - - -0.9831257177957041 - - -0.18293119747238726 -- - - -0.9853489604676163 - - -0.17055036237249038 - - - -0.9853489604676163 - - -0.17055036237249038 -- - - -0.9874162934567888 - - -0.15814254144934156 - - - -0.9874162934567888 - - -0.15814254144934156 -- - - -0.9893273896534934 - - -0.14570969796621222 - - - -0.9893273896534934 - - -0.14570969796621222 -- - - -0.9910819466690195 - - -0.1332537991456406 - - - -0.9910819466690195 - - -0.1332537991456406 -- - - -0.9926796868835203 - - -0.1207768158581612 - - - -0.9926796868835203 - - -0.1207768158581612 -- - - -0.9941203574899392 - - -0.10828072231046196 - - - -0.9941203574899392 - - -0.10828072231046196 -- - - -0.9954037305340125 - - -0.09576749573300417 - - - -0.9954037305340125 - - -0.09576749573300417 -- - - -0.9965296029503367 - - -0.08323911606717305 - - - -0.9965296029503367 - - -0.08323911606717305 -- - - -0.9974977965944997 - - -0.070697565651995 - - - -0.9974977965944997 - - -0.070697565651995 -- - - -0.9983081582712682 - - -0.05814482891047624 - - - -0.9983081582712682 - - -0.05814482891047624 -- - - -0.9989605597588274 - - -0.04558289203561173 - - - -0.9989605597588274 - - -0.04558289203561173 -- - - -0.9994548978290693 - - -0.0330137426761141 - - - -0.9994548978290693 - - -0.0330137426761141 -- - - -0.9997910942639261 - - -0.020439369621912166 - - - -0.9997910942639261 - - -0.020439369621912166 -- - - -0.9999690958677468 - - -0.007861762489468911 - - - -0.9999690958677468 - - -0.007861762489468911 -- - - -0.999988874475714 - - 0.004717088593031313 - - - -0.999988874475714 - - 0.004717088593031313 -- - - -0.9998504269583004 - - 0.01729519330057657 - - - -0.9998504269583004 - - 0.01729519330057657 -- - - -0.9995537752217639 - - 0.029870561426252256 - - - -0.9995537752217639 - - 0.029870561426252256 -- - - -0.9990989662046815 - - 0.04244120319614822 - - - -0.9990989662046815 - - 0.04244120319614822 -- - - -0.9984860718705224 - - 0.055005129584192916 - - - -0.9984860718705224 - - 0.055005129584192916 -- - - -0.9977151891962615 - - 0.06756035262687816 - - - -0.9977151891962615 - - 0.06756035262687816 -- - - -0.9967864401570343 - - 0.08010488573780679 - - - -0.9967864401570343 - - 0.08010488573780679 -- - - -0.9956999717068378 - - 0.09263674402202696 - - - -0.9956999717068378 - - 0.09263674402202696 -- - - -0.9944559557552776 - - 0.10515394459009784 - - - -0.9944559557552776 - - 0.10515394459009784 -- - - -0.9930545891403677 - - 0.11765450687183807 - - - -0.9930545891403677 - - 0.11765450687183807 -- - - -0.9914960935973849 - - 0.1301364529297071 - - - -0.9914960935973849 - - 0.1301364529297071 -- - - -0.9897807157237836 - - 0.1425978077717702 - - - -0.9897807157237836 - - 0.1425978077717702 -- - - -0.9879087269401782 - - 0.1550365996641971 - - - -0.9879087269401782 - - 0.1550365996641971 -- - - -0.9858804234473959 - - 0.16745086044324545 - - - -0.9858804234473959 - - 0.16745086044324545 -- - - -0.9836961261796103 - - 0.17983862582667898 - - - -0.9836961261796103 - - 0.17983862582667898 -- - - -0.9813561807535597 - - 0.19219793572457194 - - - -0.9813561807535597 - - 0.19219793572457194 -- - - -0.9788609574138615 - - 0.20452683454945075 - - - -0.9788609574138615 - - 0.20452683454945075 -- - - -0.9762108509744296 - - 0.21682337152571898 - - - -0.9762108509744296 - - 0.21682337152571898 -- - - -0.9734062807560028 - - 0.22908560099832972 - - - -0.9734062807560028 - - 0.22908560099832972 -- - - -0.9704476905197971 - - 0.24131158274063894 - - - -0.9704476905197971 - - 0.24131158274063894 -- - - -0.9673355483972903 - - 0.25349938226140434 - - - -0.9673355483972903 - - 0.25349938226140434 -- - - -0.9640703468161508 - - 0.2656470711108758 - - - -0.9640703468161508 - - 0.2656470711108758 -- - - -0.9606526024223212 - - 0.27775272718593 - - - -0.9606526024223212 - - 0.27775272718593 -- - - -0.957082855998271 - - 0.28981443503420057 - - - -0.957082855998271 - - 0.28981443503420057 -- - - -0.9533616723774295 - - 0.30183028615715607 - - - -0.9533616723774295 - - 0.30183028615715607 -- - - -0.9494896403548136 - - 0.31379837931207794 - - - -0.9494896403548136 - - 0.31379837931207794 -- - - -0.9454673725938637 - - 0.3257168208128897 - - - -0.9454673725938637 - - 0.3257168208128897 -- - - -0.9412955055295036 - - 0.33758372482979143 - - - -0.9412955055295036 - - 0.33758372482979143 -- - - -0.9369746992674384 - - 0.34939721368765 - - - -0.9369746992674384 - - 0.34939721368765 -- - - -0.9325056374797075 - - 0.361155418163101 - - - -0.9325056374797075 - - 0.361155418163101 -- - - -0.9278890272965095 - - 0.3728564777803084 - - - -0.9278890272965095 - - 0.3728564777803084 -- - - -0.9231255991943125 - - 0.3844985411053488 - - - -0.9231255991943125 - - 0.3844985411053488 -- - - -0.9182161068802741 - - 0.3960797660391565 - - - -0.9182161068802741 - - 0.3960797660391565 -- - - -0.9131613271729835 - - 0.4075983201089958 - - - -0.9131613271729835 - - 0.4075983201089958 -- - - -0.9079620598795464 - - 0.41905238075840945 - - - -0.9079620598795464 - - 0.41905238075840945 -- - - -0.9026191276690343 - - 0.4304401356355976 - - - -0.9026191276690343 - - 0.4304401356355976 -- - - -0.8971333759423143 - - 0.4417597828801825 - - - -0.8971333759423143 - - 0.4417597828801825 -- - - -0.8915056726982842 - - 0.4530095314083134 - - - -0.8915056726982842 - - 0.4530095314083134 -- - - -0.8857369083965297 - - 0.4641876011960654 - - - -0.8857369083965297 - - 0.4641876011960654 -- - - -0.8798279958164298 - - 0.4752922235610892 - - - -0.8798279958164298 - - 0.4752922235610892 -- - - -0.873779869912729 - - 0.486321641442466 - - - -0.873779869912729 - - 0.486321641442466 -- - - -0.8675934876676018 - - 0.49727410967872326 - - - -0.8675934876676018 - - 0.49727410967872326 -- - - -0.8612698279392309 - - 0.5081478952839691 - - - -0.8612698279392309 - - 0.5081478952839691 -- - - -0.8548098913069261 - - 0.5189412777220956 - - - -0.8548098913069261 - - 0.5189412777220956 -- - - -0.8482146999128025 - - 0.5296525491790203 - - - -0.8482146999128025 - - 0.5296525491790203 -- - - -0.8414852973000504 - - 0.5402800148329067 - - - -0.8414852973000504 - - 0.5402800148329067 -- - - -0.8346227482478176 - - 0.5508219931223336 - - - -0.8346227482478176 - - 0.5508219931223336 -- - - -0.8276281386027314 - - 0.5612768160123647 - - - -0.8276281386027314 - - 0.5612768160123647 -- - - -0.8205025751070878 - - 0.5716428292584782 - - - -0.8205025751070878 - - 0.5716428292584782 -- - - -0.8132471852237334 - - 0.5819183926683146 - - - -0.8132471852237334 - - 0.5819183926683146 -- - - -0.8058631169576695 - - 0.5921018803612005 - - - -0.8058631169576695 - - 0.5921018803612005 -- - - -0.7983515386744064 - - 0.6021916810254089 - - - -0.7983515386744064 - - 0.6021916810254089 -- - - -0.7907136389150943 - - 0.6121861981731129 - - - -0.7907136389150943 - - 0.6121861981731129 -- - - -0.7829506262084637 - - 0.6220838503929953 - - - -0.7829506262084637 - - 0.6220838503929953 -- - - -0.7750637288796017 - - 0.6318830716004721 - - - -0.7750637288796017 - - 0.6318830716004721 -- - - -0.7670541948555989 - - 0.6415823112854881 - - - -0.7670541948555989 - - 0.6415823112854881 -- - - -0.7589232914680891 - - 0.6511800347578556 - - - -0.7589232914680891 - - 0.6511800347578556 -- - - -0.7506723052527245 - - 0.6606747233900812 - - - -0.7506723052527245 - - 0.6606747233900812 -- - - -0.7423025417456096 - - 0.670064874857657 - - - -0.7423025417456096 - - 0.670064874857657 -- - - -0.7338153252767281 - - 0.6793490033767694 - - - -0.7338153252767281 - - 0.6793490033767694 -- - - -0.7252119987603977 - - 0.6885256399393918 - - - -0.7252119987603977 - - 0.6885256399393918 -- - - -0.7164939234827836 - - 0.6975933325457224 - - - -0.7164939234827836 - - 0.6975933325457224 -- - - -0.7076624788865049 - - 0.706550646433932 - - - -0.7076624788865049 - - 0.706550646433932 -- - - -0.698719062352368 - - 0.7153961643071813 - - - -0.698719062352368 - - 0.7153961643071813 -- - - -0.6896650889782625 - - 0.7241284865578796 - - - -0.6896650889782625 - - 0.7241284865578796 -- - - -0.6805019913552531 - - 0.7327462314891391 - - - -0.6805019913552531 - - 0.7327462314891391 -- - - -0.6712312193409035 - - 0.7412480355333995 - - - -0.6712312193409035 - - 0.7412480355333995 -- - - -0.6618542398298681 - - 0.7496325534681825 - - - -0.6618542398298681 - - 0.7496325534681825 -- - - -0.6523725365217912 - - 0.7578984586289408 - - - -0.6523725365217912 - - 0.7578984586289408 -- - - -0.6427876096865396 - - 0.7660444431189778 - - - -0.6427876096865396 - - 0.7660444431189778 -- - - -0.6331009759268216 - - 0.7740692180163904 - - - -0.6331009759268216 - - 0.7740692180163904 -- - - -0.623314167938217 - - 0.7819715135780128 - - - -0.623314167938217 - - 0.7819715135780128 -- - - -0.6134287342666622 - - 0.7897500794403256 - - - -0.6134287342666622 - - 0.7897500794403256 -- - - -0.6034462390634266 - - 0.7974036848172986 - - - -0.6034462390634266 - - 0.7974036848172986 -- - - -0.5933682618376209 - - 0.8049311186951345 - - - -0.5933682618376209 - - 0.8049311186951345 -- - - -0.5831963972062739 - - 0.8123311900238854 - - - -0.5831963972062739 - - 0.8123311900238854 -- - - -0.5729322546420206 - - 0.819602727905911 - - - -0.5729322546420206 - - 0.819602727905911 -- - - -0.5625774582184379 - - 0.826744581781146 - - - -0.5625774582184379 - - 0.826744581781146 -- - - -0.552133646353071 - - 0.8337556216091511 - - - -0.552133646353071 - - 0.8337556216091511 -- - - -0.541602471548191 - - 0.8406347380479176 - - - -0.541602471548191 - - 0.8406347380479176 -- - - -0.5309856001293205 - - 0.8473808426293961 - - - -0.5309856001293205 - - 0.8473808426293961 -- - - -0.5202847119815792 - - 0.8539928679317206 - - - -0.5202847119815792 - - 0.8539928679317206 -- - - -0.5095015002838734 - - 0.8604697677481075 - - - -0.5095015002838734 - - 0.8604697677481075 -- - - -0.4986376712409919 - - 0.8668105172523927 - - - -0.4986376712409919 - - 0.8668105172523927 -- - - -0.487694943813635 - - 0.8730141131611879 - - - -0.487694943813635 - - 0.8730141131611879 -- - - -0.47667504944642797 - - 0.8790795738926286 - - - -0.47667504944642797 - - 0.8790795738926286 -- - - -0.4655797317939577 - - 0.8850059397216871 - - - -0.4655797317939577 - - 0.8850059397216871 -- - - -0.45441074644487806 - - 0.890792272932028 - - - -0.45441074644487806 - - 0.890792272932028 -- - - -0.4431698606441268 - - 0.8964376579643814 - - - -0.4431698606441268 - - 0.8964376579643814 -- - - -0.4318588530132981 - - 0.9019412015614092 - - - -0.4318588530132981 - - 0.9019412015614092 -- - - -0.4204795132692152 - - 0.907302032909044 - - - -0.4204795132692152 - - 0.907302032909044 -- - - -0.4090336419407468 - - 0.9125193037742757 - - - -0.4090336419407468 - - 0.9125193037742757 -- - - -0.3975230500839139 - - 0.9175921886393661 - - - -0.3975230500839139 - - 0.9175921886393661 -- - - -0.38594955899532896 - - 0.9225198848324686 - - - -0.38594955899532896 - - 0.9225198848324686 -- - - -0.3743149999240192 - - 0.9273016126546322 - - - -0.3743149999240192 - - 0.9273016126546322 -- - - -0.3626212137816673 - - 0.9319366155031737 - - - -0.3626212137816673 - - 0.9319366155031737 -- - - -0.35087005085133094 - - 0.9364241599913922 - - - -0.35087005085133094 - - 0.9364241599913922 -- - - -0.3390633704946757 - - 0.9407635360646108 - - - -0.3390633704946757 - - 0.9407635360646108 -- - - -0.3272030408577722 - - 0.9449540571125281 - - - -0.3272030408577722 - - 0.9449540571125281 -- - - -0.3152909385755031 - - 0.9489950600778585 - - - -0.3152909385755031 - - 0.9489950600778585 -- - - -0.3033289484746273 - - 0.9528859055612465 - - - -0.3033289484746273 - - 0.9528859055612465 -- - - -0.29131896327554796 - - 0.9566259779224375 - - - -0.29131896327554796 - - 0.9566259779224375 -- - - -0.2792628832928309 - - 0.9602146853776892 - - - -0.2792628832928309 - - 0.9602146853776892 -- - - -0.26716261613452225 - - 0.9636514600934084 - - - -0.26716261613452225 - - 0.9636514600934084 -- - - -0.25502007640031144 - - 0.9669357582759981 - - - -0.25502007640031144 - - 0.9669357582759981 -- - - -0.24283718537858734 - - 0.9700670602579007 - - - -0.24283718537858734 - - 0.9700670602579007 -- - - -0.23061587074244044 - - 0.9730448705798238 - - - -0.23061587074244044 - - 0.9730448705798238 -- - - -0.21835806624464577 - - 0.975868718069136 - - - -0.21835806624464577 - - 0.975868718069136 -- - - -0.20606571141169297 - - 0.9785381559144195 - - - -0.20606571141169297 - - 0.9785381559144195 -- - - -0.19374075123689813 - - 0.981052761736168 - - - -0.19374075123689813 - - 0.981052761736168 -- - - -0.18138513587265162 - - 0.9834121376536186 - - - -0.18138513587265162 - - 0.9834121376536186 -- - - -0.16900082032184968 - - 0.9856159103477083 - - - -0.16900082032184968 - - 0.9856159103477083 -- - - -0.15658976412855838 - - 0.9876637311201432 - - - -0.15658976412855838 - - 0.9876637311201432 -- - - -0.14415393106795907 - - 0.9895552759485718 - - - -0.14415393106795907 - - 0.9895552759485718 -- - - -0.13169528883562445 - - 0.9912902455378553 - - - -0.13169528883562445 - - 0.9912902455378553 -- - - -0.11921580873617425 - - 0.9928683653674237 - - - -0.11921580873617425 - - 0.9928683653674237 -- - - -0.10671746537135988 - - 0.9942893857347128 - - - -0.10671746537135988 - - 0.9942893857347128 -- - - -0.0942022363276273 - - 0.9955530817946745 - - - -0.0942022363276273 - - 0.9955530817946745 -- - - -0.08167210186320688 - - 0.9966592535953529 - - - -0.08167210186320688 - - 0.9966592535953529 -- - - -0.06912904459478485 - - 0.9976077261095226 - - - -0.06912904459478485 - - 0.9976077261095226 -- - - -0.056575049183792726 - - 0.998398349262383 - - - -0.056575049183792726 - - 0.998398349262383 -- - - -0.04401210202238211 - - 0.9990309979553044 - - - -0.04401210202238211 - - 0.9990309979553044 -- - - -0.031442190919121114 - - 0.9995055720856215 - - - -0.031442190919121114 - - 0.9995055720856215 -- - - -0.018867304784467676 - - 0.9998219965624732 - - - -0.018867304784467676 - - 0.9998219965624732 -- - - -0.006289433316068405 - - 0.9999802213186832 - - - -0.006289433316068405 - - 0.9999802213186832 -- - - 0.006289433316067026 - - 0.9999802213186832 - - - 0.006289433316067026 - - 0.9999802213186832 -- - - 0.0188673047844663 - - 0.9998219965624732 - - - 0.0188673047844663 - - 0.9998219965624732 -- - - 0.03144219091911974 - - 0.9995055720856215 - - - 0.03144219091911974 - - 0.9995055720856215 -- - - 0.04401210202238073 - - 0.9990309979553045 - - - 0.04401210202238073 - - 0.9990309979553045 -- - - 0.056575049183791346 - - 0.9983983492623831 - - - 0.056575049183791346 - - 0.9983983492623831 -- - - 0.06912904459478347 - - 0.9976077261095226 - - - 0.06912904459478347 - - 0.9976077261095226 -- - - 0.08167210186320639 - - 0.9966592535953529 - - - 0.08167210186320639 - - 0.9966592535953529 -- - - 0.09420223632762592 - - 0.9955530817946746 - - - 0.09420223632762592 - - 0.9955530817946746 -- - - 0.10671746537135851 - - 0.994289385734713 - - - 0.10671746537135851 - - 0.994289385734713 -- - - 0.11921580873617288 - - 0.9928683653674238 - - - 0.11921580873617288 - - 0.9928683653674238 -- - - 0.13169528883562306 - - 0.9912902455378555 - - - 0.13169528883562306 - - 0.9912902455378555 -- - - 0.14415393106795768 - - 0.9895552759485721 - - - 0.14415393106795768 - - 0.9895552759485721 -- - - 0.15658976412855702 - - 0.9876637311201434 - - - 0.15658976412855702 - - 0.9876637311201434 -- - - 0.16900082032184832 - - 0.9856159103477086 - - - 0.16900082032184832 - - 0.9856159103477086 -- - - 0.18138513587265026 - - 0.9834121376536189 - - - 0.18138513587265026 - - 0.9834121376536189 -- - - 0.19374075123689677 - - 0.9810527617361683 - - - 0.19374075123689677 - - 0.9810527617361683 -- - - 0.2060657114116916 - - 0.9785381559144198 - - - 0.2060657114116916 - - 0.9785381559144198 -- - - 0.21835806624464443 - - 0.9758687180691363 - - - 0.21835806624464443 - - 0.9758687180691363 -- - - 0.2306158707424391 - - 0.9730448705798241 - - - 0.2306158707424391 - - 0.9730448705798241 -- - - 0.24283718537858687 - - 0.9700670602579009 - - - 0.24283718537858687 - - 0.9700670602579009 -- - - 0.2550200764003101 - - 0.9669357582759984 - - - 0.2550200764003101 - - 0.9669357582759984 -- - - 0.2671626161345209 - - 0.9636514600934087 - - - 0.2671626161345209 - - 0.9636514600934087 -- - - 0.2792628832928296 - - 0.9602146853776896 - - - 0.2792628832928296 - - 0.9602146853776896 -- - - 0.2913189632755466 - - 0.956625977922438 - - - 0.2913189632755466 - - 0.956625977922438 -- - - 0.30332894847462605 - - 0.952885905561247 - - - 0.30332894847462605 - - 0.952885905561247 -- - - 0.3152909385755018 - - 0.9489950600778589 - - - 0.3152909385755018 - - 0.9489950600778589 -- - - 0.3272030408577709 - - 0.9449540571125286 - - - 0.3272030408577709 - - 0.9449540571125286 -- - - 0.33906337049467444 - - 0.9407635360646113 - - - 0.33906337049467444 - - 0.9407635360646113 -- - - 0.3508700508513296 - - 0.9364241599913926 - - - 0.3508700508513296 - - 0.9364241599913926 -- - - 0.36262121378166595 - - 0.9319366155031743 - - - 0.36262121378166595 - - 0.9319366155031743 -- - - 0.3743149999240179 - - 0.9273016126546327 - - - 0.3743149999240179 - - 0.9273016126546327 -- - - 0.3859495589953277 - - 0.9225198848324692 - - - 0.3859495589953277 - - 0.9225198848324692 -- - - 0.39752305008391264 - - 0.9175921886393666 - - - 0.39752305008391264 - - 0.9175921886393666 -- - - 0.40903364194074554 - - 0.9125193037742763 - - - 0.40903364194074554 - - 0.9125193037742763 -- - - 0.4204795132692139 - - 0.9073020329090445 - - - 0.4204795132692139 - - 0.9073020329090445 -- - - 0.4318588530132969 - - 0.9019412015614098 - - - 0.4318588530132969 - - 0.9019412015614098 -- - - 0.44316986064412556 - - 0.896437657964382 - - - 0.44316986064412556 - - 0.896437657964382 -- - - 0.45441074644487683 - - 0.8907922729320287 - - - 0.45441074644487683 - - 0.8907922729320287 -- - - 0.46557973179395645 - - 0.8850059397216877 - - - 0.46557973179395645 - - 0.8850059397216877 -- - - 0.47667504944642675 - - 0.8790795738926293 - - - 0.47667504944642675 - - 0.8790795738926293 -- - - 0.48769494381363376 - - 0.8730141131611886 - - - 0.48769494381363376 - - 0.8730141131611886 -- - - 0.4986376712409907 - - 0.8668105172523933 - - - 0.4986376712409907 - - 0.8668105172523933 -- - - 0.5095015002838723 - - 0.8604697677481082 - - - 0.5095015002838723 - - 0.8604697677481082 -- - - 0.520284711981578 - - 0.8539928679317214 - - - 0.520284711981578 - - 0.8539928679317214 -- - - 0.5309856001293194 - - 0.8473808426293968 - - - 0.5309856001293194 - - 0.8473808426293968 -- - - 0.5416024715481897 - - 0.8406347380479183 - - - 0.5416024715481897 - - 0.8406347380479183 -- - - 0.5521336463530699 - - 0.8337556216091518 - - - 0.5521336463530699 - - 0.8337556216091518 -- - - 0.5625774582184366 - - 0.8267445817811466 - - - 0.5625774582184366 - - 0.8267445817811466 -- - - 0.5729322546420195 - - 0.8196027279059118 - - - 0.5729322546420195 - - 0.8196027279059118 -- - - 0.5831963972062728 - - 0.8123311900238863 - - - 0.5831963972062728 - - 0.8123311900238863 -- - - 0.5933682618376198 - - 0.8049311186951352 - - - 0.5933682618376198 - - 0.8049311186951352 -- - - 0.6034462390634255 - - 0.7974036848172994 - - - 0.6034462390634255 - - 0.7974036848172994 -- - - 0.6134287342666611 - - 0.7897500794403265 - - - 0.6134287342666611 - - 0.7897500794403265 -- - - 0.6233141679382159 - - 0.7819715135780135 - - - 0.6233141679382159 - - 0.7819715135780135 -- - - 0.6331009759268206 - - 0.7740692180163913 - - - 0.6331009759268206 - - 0.7740692180163913 -- - - 0.6427876096865385 - - 0.7660444431189787 - - - 0.6427876096865385 - - 0.7660444431189787 -- - - 0.6523725365217901 - - 0.7578984586289417 - - - 0.6523725365217901 - - 0.7578984586289417 -- - - 0.6618542398298678 - - 0.7496325534681827 - - - 0.6618542398298678 - - 0.7496325534681827 -- - - 0.6712312193409025 - - 0.7412480355334005 - - - 0.6712312193409025 - - 0.7412480355334005 -- - - 0.6805019913552521 - - 0.7327462314891401 - - - 0.6805019913552521 - - 0.7327462314891401 -- - - 0.6896650889782615 - - 0.7241284865578805 - - - 0.6896650889782615 - - 0.7241284865578805 -- - - 0.698719062352367 - - 0.7153961643071823 - - - 0.698719062352367 - - 0.7153961643071823 -- - - 0.7076624788865039 - - 0.7065506464339328 - - - 0.7076624788865039 - - 0.7065506464339328 -- - - 0.7164939234827827 - - 0.6975933325457234 - - - 0.7164939234827827 - - 0.6975933325457234 -- - - 0.7252119987603968 - - 0.6885256399393928 - - - 0.7252119987603968 - - 0.6885256399393928 -- - - 0.7338153252767271 - - 0.6793490033767704 - - - 0.7338153252767271 - - 0.6793490033767704 -- - - 0.7423025417456087 - - 0.670064874857658 - - - 0.7423025417456087 - - 0.670064874857658 -- - - 0.7506723052527237 - - 0.6606747233900823 - - - 0.7506723052527237 - - 0.6606747233900823 -- - - 0.7589232914680881 - - 0.6511800347578566 - - - 0.7589232914680881 - - 0.6511800347578566 -- - - 0.767054194855598 - - 0.6415823112854891 - - - 0.767054194855598 - - 0.6415823112854891 -- - - 0.7750637288796014 - - 0.6318830716004724 - - - 0.7750637288796014 - - 0.6318830716004724 -- - - 0.7829506262084629 - - 0.6220838503929964 - - - 0.7829506262084629 - - 0.6220838503929964 -- - - 0.7907136389150935 - - 0.612186198173114 - - - 0.7907136389150935 - - 0.612186198173114 -- - - 0.7983515386744056 - - 0.60219168102541 - - - 0.7983515386744056 - - 0.60219168102541 -- - - 0.8058631169576688 - - 0.5921018803612016 - - - 0.8058631169576688 - - 0.5921018803612016 -- - - 0.8132471852237325 - - 0.5819183926683157 - - - 0.8132471852237325 - - 0.5819183926683157 -- - - 0.820502575107087 - - 0.5716428292584793 - - - 0.820502575107087 - - 0.5716428292584793 -- - - 0.8276281386027308 - - 0.5612768160123658 - - - 0.8276281386027308 - - 0.5612768160123658 -- - - 0.8346227482478168 - - 0.5508219931223347 - - - 0.8346227482478168 - - 0.5508219931223347 -- - - 0.8414852973000496 - - 0.5402800148329078 - - - 0.8414852973000496 - - 0.5402800148329078 -- - - 0.8482146999128017 - - 0.5296525491790214 - - - 0.8482146999128017 - - 0.5296525491790214 -- - - 0.8548098913069254 - - 0.5189412777220967 - - - 0.8548098913069254 - - 0.5189412777220967 -- - - 0.8612698279392301 - - 0.5081478952839703 - - - 0.8612698279392301 - - 0.5081478952839703 -- - - 0.8675934876676011 - - 0.49727410967872443 - - - 0.8675934876676011 - - 0.49727410967872443 -- - - 0.8737798699127283 - - 0.48632164144246715 - - - 0.8737798699127283 - - 0.48632164144246715 -- - - 0.8798279958164291 - - 0.4752922235610904 - - - 0.8798279958164291 - - 0.4752922235610904 -- - - 0.8857369083965291 - - 0.4641876011960666 - - - 0.8857369083965291 - - 0.4641876011960666 -- - - 0.8915056726982836 - - 0.4530095314083147 - - - 0.8915056726982836 - - 0.4530095314083147 -- - - 0.8971333759423138 - - 0.4417597828801838 - - - 0.8971333759423138 - - 0.4417597828801838 -- - - 0.9026191276690336 - - 0.43044013563559885 - - - 0.9026191276690336 - - 0.43044013563559885 -- - - 0.9079620598795458 - - 0.4190523807584107 - - - 0.9079620598795458 - - 0.4190523807584107 -- - - 0.9131613271729829 - - 0.4075983201089971 - - - 0.9131613271729829 - - 0.4075983201089971 -- - - 0.9182161068802737 - - 0.39607976603915773 - - - 0.9182161068802737 - - 0.39607976603915773 -- - - 0.9231255991943119 - - 0.3844985411053501 - - - 0.9231255991943119 - - 0.3844985411053501 -- - - 0.9278890272965089 - - 0.37285647778030967 - - - 0.9278890272965089 - - 0.37285647778030967 -- - - 0.932505637479707 - - 0.36115541816310226 - - - 0.932505637479707 - - 0.36115541816310226 -- - - 0.9369746992674379 - - 0.3493972136876513 - - - 0.9369746992674379 - - 0.3493972136876513 -- - - 0.9412955055295031 - - 0.3375837248297927 - - - 0.9412955055295031 - - 0.3375837248297927 -- - - 0.9454673725938633 - - 0.32571682081289105 - - - 0.9454673725938633 - - 0.32571682081289105 -- - - 0.9494896403548132 - - 0.3137983793120792 - - - 0.9494896403548132 - - 0.3137983793120792 -- - - 0.9533616723774291 - - 0.3018302861571574 - - - 0.9533616723774291 - - 0.3018302861571574 -- - - 0.9570828559982706 - - 0.2898144350342019 - - - 0.9570828559982706 - - 0.2898144350342019 -- - - 0.9606526024223209 - - 0.27775272718593136 - - - 0.9606526024223209 - - 0.27775272718593136 -- - - 0.9640703468161504 - - 0.26564707111087715 - - - 0.9640703468161504 - - 0.26564707111087715 -- - - 0.96733554839729 - - 0.25349938226140567 - - - 0.96733554839729 - - 0.25349938226140567 -- - - 0.9704476905197967 - - 0.24131158274064027 - - - 0.9704476905197967 - - 0.24131158274064027 -- - - 0.9734062807560024 - - 0.22908560099833106 - - - 0.9734062807560024 - - 0.22908560099833106 -- - - 0.9762108509744293 - - 0.21682337152572034 - - - 0.9762108509744293 - - 0.21682337152572034 -- - - 0.9788609574138614 - - 0.20452683454945125 - - - 0.9788609574138614 - - 0.20452683454945125 -- - - 0.9813561807535595 - - 0.1921979357245733 - - - 0.9813561807535595 - - 0.1921979357245733 -- - - 0.98369612617961 - - 0.17983862582668034 - - - 0.98369612617961 - - 0.17983862582668034 -- - - 0.9858804234473957 - - 0.1674508604432468 - - - 0.9858804234473957 - - 0.1674508604432468 -- - - 0.987908726940178 - - 0.15503659966419847 - - - 0.987908726940178 - - 0.15503659966419847 -- - - 0.9897807157237833 - - 0.14259780777177156 - - - 0.9897807157237833 - - 0.14259780777177156 -- - - 0.9914960935973847 - - 0.13013645292970846 - - - 0.9914960935973847 - - 0.13013645292970846 -- - - 0.9930545891403676 - - 0.11765450687183943 - - - 0.9930545891403676 - - 0.11765450687183943 -- - - 0.9944559557552775 - - 0.1051539445900992 - - - 0.9944559557552775 - - 0.1051539445900992 -- - - 0.9956999717068375 - - 0.09263674402202833 - - - 0.9956999717068375 - - 0.09263674402202833 -- - - 0.9967864401570342 - - 0.08010488573780816 - - - 0.9967864401570342 - - 0.08010488573780816 -- - - 0.9977151891962615 - - 0.06756035262687954 - - - 0.9977151891962615 - - 0.06756035262687954 -- - - 0.9984860718705224 - - 0.05500512958419429 - - - 0.9984860718705224 - - 0.05500512958419429 -- - - 0.9990989662046814 - - 0.042441203196148705 - - - 0.9990989662046814 - - 0.042441203196148705 -- - - 0.9995537752217638 - - 0.029870561426253633 - - - 0.9995537752217638 - - 0.029870561426253633 -- - - 0.9998504269583004 - - 0.01729519330057795 - - - 0.9998504269583004 - - 0.01729519330057795 -- - - 0.999988874475714 - - 0.004717088593032691 - - - 0.999988874475714 - - 0.004717088593032691 -- - - 0.999969095867747 - - -0.007861762489467534 - - - 0.999969095867747 - - -0.007861762489467534 -- - - 0.9997910942639262 - - -0.020439369621910786 - - - 0.9997910942639262 - - -0.020439369621910786 -- - - 0.9994548978290694 - - -0.03301374267611272 - - - 0.9994548978290694 - - -0.03301374267611272 -- - - 0.9989605597588275 - - -0.045582892035610355 - - - 0.9989605597588275 - - -0.045582892035610355 -- - - 0.9983081582712683 - - -0.058144828910474865 - - - 0.9983081582712683 - - -0.058144828910474865 -- - - 0.9974977965944998 - - -0.07069756565199363 - - - 0.9974977965944998 - - -0.07069756565199363 -- - - 0.9965296029503368 - - -0.08323911606717167 - - - 0.9965296029503368 - - -0.08323911606717167 -- - - 0.9954037305340127 - - -0.09576749573300279 - - - 0.9954037305340127 - - -0.09576749573300279 -- - - 0.9941203574899394 - - -0.1082807223104606 - - - 0.9941203574899394 - - -0.1082807223104606 -- - - 0.9926796868835203 - - -0.12077681585816072 - - - 0.9926796868835203 - - -0.12077681585816072 -- - - 0.9910819466690197 - - -0.1332537991456392 - - - 0.9910819466690197 - - -0.1332537991456392 -- - - 0.9893273896534936 - - -0.14570969796621086 - - - 0.9893273896534936 - - -0.14570969796621086 -- - - 0.9874162934567892 - - -0.1581425414493393 - - - 0.9874162934567892 - - -0.1581425414493393 -- - - 0.9853489604676167 - - -0.17055036237248902 - - - 0.9853489604676167 - - -0.17055036237248902 -- - - 0.9831257177957046 - - -0.18293119747238504 - - - 0.9831257177957046 - - -0.18293119747238504 -- - - 0.9807469172200398 - - -0.1952830877556692 - - - 0.9807469172200398 - - -0.1952830877556692 -- - - 0.9782129351332084 - - -0.2076040788088552 - - - 0.9782129351332084 - - -0.2076040788088552 -- - - 0.9755241724818389 - - -0.2198922211075767 - - - 0.9755241724818389 - - -0.2198922211075767 -- - - 0.9726810547031601 - - -0.23214557032506142 - - - 0.9726810547031601 - - -0.23214557032506142 -- - - 0.9696840316576879 - - -0.24436218763976586 - - - 0.9696840316576879 - - -0.24436218763976586 -- - - 0.9665335775580415 - - -0.25654014004216474 - - - 0.9665335775580415 - - -0.25654014004216474 -- - - 0.9632301908939129 - - -0.2686775006405933 - - - 0.9632301908939129 - - -0.2686775006405933 -- - - 0.9597743943531892 - - -0.2807723489661489 - - - 0.9597743943531892 - - -0.2807723489661489 -- - - 0.9561667347392514 - - -0.29282277127654904 - - - 0.9561667347392514 - - -0.29282277127654904 -- - - 0.9524077828844516 - - -0.3048268608589526 - - - 0.9524077828844516 - - -0.3048268608589526 -- - - 0.9484981335597957 - - -0.3167827183316413 - - - 0.9484981335597957 - - -0.3167827183316413 -- - - 0.9444384053808291 - - -0.32868845194456814 - - - 0.9444384053808291 - - -0.32868845194456814 -- - - 0.9402292407097596 - - -0.340542177878672 - - - 0.9402292407097596 - - -0.340542177878672 -- - - 0.9358713055538124 - - -0.3523420205439635 - - - 0.9358713055538124 - - -0.3523420205439635 -- - - 0.9313652894598542 - - -0.36408611287628373 - - - 0.9313652894598542 - - -0.36408611287628373 -- - - 0.9267119054052854 - - -0.37577259663273127 - - - 0.9267119054052854 - - -0.37577259663273127 -- - - 0.9219118896852252 - - -0.38739962268569283 - - - 0.9219118896852252 - - -0.38739962268569283 -- - - 0.9169660017960138 - - -0.3989653513154153 - - - 0.9169660017960138 - - -0.3989653513154153 -- - - 0.9118750243150339 - - -0.4104679525011135 - - - 0.9118750243150339 - - -0.4104679525011135 -- - - 0.9066397627768898 - - -0.4219056062105182 - - - 0.9066397627768898 - - -0.4219056062105182 -- - - 0.901261045545945 - - -0.4332765026878681 - - - 0.901261045545945 - - -0.4332765026878681 -- - - 0.895739723685256 - - -0.44457884274025133 - - - 0.895739723685256 - - -0.44457884274025133 -- - - 0.8900766708219062 - - -0.45581083802230066 - - - 0.8900766708219062 - - -0.45581083802230066 -- - - 0.8842727830087785 - - -0.46697071131914664 - - - 0.8842727830087785 - - -0.46697071131914664 -- - - 0.878328978582769 - - -0.47805669682763535 - - - 0.878328978582769 - - -0.47805669682763535 -- - - 0.8722461980194871 - - -0.48906704043571536 - - - 0.8722461980194871 - - -0.48906704043571536 -- - - 0.8660254037844392 - - -0.4999999999999992 - - - 0.8660254037844392 - - -0.4999999999999992 -- - - 0.8596675801807453 - - -0.5108538456214086 - - - 0.8596675801807453 - - -0.5108538456214086 -- - - 0.8531737331933934 - - -0.5216268599188969 - - - 0.8531737331933934 - - -0.5216268599188969 -- - - 0.8465448903300608 - - -0.5323173383011919 - - - 0.8465448903300608 - - -0.5323173383011919 -- - - 0.8397821004585404 - - -0.5429235892364983 - - - 0.8397821004585404 - - -0.5429235892364983 -- - - 0.8328864336407736 - - -0.5534439345201582 - - - 0.8328864336407736 - - -0.5534439345201582 -- - - 0.8258589809635439 - - -0.5638767095401768 - - - 0.8258589809635439 - - -0.5638767095401768 -- - - 0.8187008543658284 - - -0.5742202635406232 - - - 0.8187008543658284 - - -0.5742202635406232 -- - - 0.8114131864628666 - - -0.5844729598828138 - - - 0.8114131864628666 - - -0.5844729598828138 -- - - 0.803997130366941 - - -0.5946331763042861 - - - 0.803997130366941 - - -0.5946331763042861 -- - - 0.7964538595049301 - - -0.6046993051754741 - - - 0.7964538595049301 - - -0.6046993051754741 -- - - 0.7887845674326319 - - -0.6146697537540917 - - - 0.7887845674326319 - - -0.6146697537540917 -- - - 0.7809904676459185 - - -0.6245429444371375 - - - 0.7809904676459185 - - -0.6245429444371375 -- - - 0.7730727933887184 - - -0.6343173150105269 - - - 0.7730727933887184 - - -0.6343173150105269 -- - - 0.76503279745789 - - -0.6439913188962683 - - - 0.76503279745789 - - -0.6439913188962683 -- - - 0.7568717520049925 - - -0.6535634253971785 - - - 0.7568717520049925 - - -0.6535634253971785 -- - - 0.7485909483349908 - - -0.6630321199390865 - - - 0.7485909483349908 - - -0.6630321199390865 -- - - 0.7401916967019444 - - -0.6723959043104716 - - - 0.7401916967019444 - - -0.6723959043104716 -- - - 0.7316753261016786 - - -0.6816532968995326 - - - 0.7316753261016786 - - -0.6816532968995326 -- - - 0.7230431840615102 - - -0.69080283292861 - - - 0.7230431840615102 - - -0.69080283292861 -- - - 0.7142966364270213 - - -0.6998430646859649 - - - 0.7142966364270213 - - -0.6998430646859649 -- - - 0.7054370671459542 - - -0.7087725617548373 - - - 0.7054370671459542 - - -0.7087725617548373 -- - - 0.6964658780492222 - - -0.7175899112397874 - - - 0.6964658780492222 - - -0.7175899112397874 -- - - 0.6873844886291115 - - -0.7262937179902459 - - - 0.6873844886291115 - - -0.7262937179902459 -- - - 0.678194335814667 - - -0.7348826048212753 - - - 0.678194335814667 - - -0.7348826048212753 -- - - 0.6688968737443408 - - -0.7433552127314689 - - - 0.6688968737443408 - - -0.7433552127314689 -- - - 0.6594935735358967 - - -0.7517102011179926 - - - 0.6594935735358967 - - -0.7517102011179926 -- - - 0.6499859230536468 - - -0.7599462479886974 - - - 0.6499859230536468 - - -0.7599462479886974 -- - - 0.6403754266730268 - - -0.7680620501712988 - - - 0.6403754266730268 - - -0.7680620501712988 -- - - 0.6306636050425575 - - -0.7760563235195788 - - - 0.6306636050425575 - - -0.7760563235195788 -- - - 0.6208519948432446 - - -0.7839278031165648 - - - 0.6208519948432446 - - -0.7839278031165648 -- - - 0.6109421485454233 - - -0.7916752434746851 - - - 0.6109421485454233 - - -0.7916752434746851 -- - - 0.600935634163124 - - -0.7992974187328293 - - - 0.600935634163124 - - -0.7992974187328293 -- - - 0.5908340350059585 - - -0.8067931228503239 - - - 0.5908340350059585 - - -0.8067931228503239 -- - - 0.5806389494286068 - - -0.8141611697977519 - - - 0.5806389494286068 - - -0.8141611697977519 -- - - 0.570351990577902 - - -0.8214003937446248 - - - 0.570351990577902 - - -0.8214003937446248 -- - - 0.5599747861375968 - - -0.8285096492438412 - - - 0.5599747861375968 - - -0.8285096492438412 -- - - 0.5495089780708068 - - -0.8354878114129359 - - - 0.5495089780708068 - - -0.8354878114129359 -- - - 0.5389562223602165 - - -0.8423337761120617 - - - 0.5389562223602165 - - -0.8423337761120617 -- - - 0.5283181887460523 - - -0.8490464601186973 - - - 0.5283181887460523 - - -0.8490464601186973 -- - - 0.5175965604618786 - - -0.8556248012990465 - - - 0.5175965604618786 - - -0.8556248012990465 -- - - 0.5067930339682736 - - -0.8620677587760909 - - - 0.5067930339682736 - - -0.8620677587760909 -- - - 0.49590931868438975 - - -0.8683743130942925 - - - 0.49590931868438975 - - -0.8683743130942925 -- - - 0.4849471367174889 - - -0.8745434663808935 - - - 0.4849471367174889 - - -0.8745434663808935 -- - - 0.4739082225904436 - - -0.8805742425038144 - - - 0.4739082225904436 - - -0.8805742425038144 -- - - 0.4627943229673003 - - -0.886465687226098 - - - 0.4627943229673003 - - -0.886465687226098 -- - - 0.4516071963768956 - - -0.8922168683569035 - - - 0.4516071963768956 - - -0.8922168683569035 -- - - 0.44034861293462074 - - -0.8978268758989985 - - - 0.44034861293462074 - - -0.8978268758989985 -- - - 0.42902035406232714 - - -0.903294822192752 - - - 0.42902035406232714 - - -0.903294822192752 -- - - 0.4176242122064685 - - -0.9086198420565812 - - - 0.4176242122064685 - - -0.9086198420565812 -- - - 0.4061619905544733 - - -0.9138010929238529 - - - 0.4061619905544733 - - -0.9138010929238529 -- - - 0.3946355027494409 - - -0.918837754976196 - - - 0.3946355027494409 - - -0.918837754976196 -- - - 0.38304657260316866 - - -0.9237290312732221 - - - 0.38304657260316866 - - -0.9237290312732221 -- - - 0.37139703380756833 - - -0.9284741478786256 - - - 0.37139703380756833 - - -0.9284741478786256 -- - - 0.3596887296445368 - - -0.9330723539826369 - - - 0.3596887296445368 - - -0.9330723539826369 -- - - 0.34792351269428423 - - -0.9375229220208273 - - - 0.34792351269428423 - - -0.9375229220208273 -- - - 0.3361032445422173 - - -0.9418251477892244 - - - 0.3361032445422173 - - -0.9418251477892244 -- - - 0.3242297954843714 - - -0.9459783505557422 - - - 0.3242297954843714 - - -0.9459783505557422 -- - - 0.31230504423149086 - - -0.9499818731678866 - - - 0.31230504423149086 - - -0.9499818731678866 -- - - 0.3003308776117511 - - -0.9538350821567402 - - - 0.3003308776117511 - - -0.9538350821567402 -- - - 0.28830919027222335 - - -0.9575373678371905 - - - 0.28830919027222335 - - -0.9575373678371905 -- - - 0.27624188437907515 - - -0.9610881444044025 - - - 0.27624188437907515 - - -0.9610881444044025 -- - - 0.264130869316608 - - -0.9644868500265066 - - - 0.264130869316608 - - -0.9644868500265066 -- - - 0.2519780613851261 - - -0.9677329469334987 - - - 0.2519780613851261 - - -0.9677329469334987 -- - - 0.2397853834977361 - - -0.9708259215023276 - - - 0.2397853834977361 - - -0.9708259215023276 -- - - 0.22755476487608342 - - -0.9737652843381666 - - - 0.22755476487608342 - - -0.9737652843381666 -- - - 0.2152881407450906 - - -0.9765505703518492 - - - 0.2152881407450906 - - -0.9765505703518492 -- - - 0.20298745202676252 - - -0.9791813388334577 - - - 0.20298745202676252 - - -0.9791813388334577 -- - - 0.19065464503306495 - - -0.9816571735220581 - - - 0.19065464503306495 - - -0.9816571735220581 -- - - 0.17829167115797728 - - -0.9839776826715613 - - - 0.17829167115797728 - - -0.9839776826715613 -- - - 0.1659004865687139 - - -0.9861424991127113 - - - 0.1659004865687139 - - -0.9861424991127113 -- - - 0.15348305189621775 - - -0.9881512803111794 - - - 0.15348305189621775 - - -0.9881512803111794 -- - - 0.14104133192492 - - -0.9900037084217637 - - - 0.14104133192492 - - -0.9900037084217637 -- - - 0.12857729528187029 - - -0.9916994903386805 - - - 0.12857729528187029 - - -0.9916994903386805 -- - - 0.11609291412523105 - - -0.9932383577419429 - - - 0.11609291412523105 - - -0.9932383577419429 -- - - 0.10359016383224108 - - -0.9946200671398147 - - - 0.10359016383224108 - - -0.9946200671398147 -- - - 0.09107102268664179 - - -0.9958443999073395 - - - 0.09107102268664179 - - -0.9958443999073395 -- - - 0.07853747156566976 - - -0.996911162320932 - - - 0.07853747156566976 - - -0.996911162320932 -- - - 0.0659914936266216 - - -0.9978201855890306 - - - 0.0659914936266216 - - -0.9978201855890306 -- - - 0.05343507399305771 - - -0.9985713258788059 - - - 0.05343507399305771 - - -0.9985713258788059 -- - - 0.04087019944071283 - - -0.9991644643389177 - - - 0.04087019944071283 - - -0.9991644643389177 -- - - 0.028298858083118522 - - -0.9995995071183216 - - - 0.028298858083118522 - - -0.9995995071183216 -- - - 0.01572303905704239 - - -0.9998763853811183 - - - 0.01572303905704239 - - -0.9998763853811183 -- - - 0.003144732207736932 - - -0.9999950553174458 - - - 0.003144732207736932 - - -0.9999950553174458 -- - - -0.009434072225895224 - - -0.999955498150411 - - - -0.009434072225895224 - - -0.999955498150411 -- - - -0.02201138392622685 - - -0.9997577201390606 - - - -0.02201138392622685 - - -0.9997577201390606 -- - - -0.03458521281181564 - - -0.9994017525773914 - - - -0.03458521281181564 - - -0.9994017525773914 -- - - -0.04715356935230482 - - -0.9988876517893979 - - - -0.04715356935230482 - - -0.9988876517893979 -- - - -0.05971446488320808 - - -0.9982154991201609 - - - -0.05971446488320808 - - -0.9982154991201609 -- - - -0.07226591192058601 - - -0.9973854009229762 - - - -0.07226591192058601 - - -0.9973854009229762 -- - - -0.08480592447550901 - - -0.9963974885425265 - - - -0.08480592447550901 - - -0.9963974885425265 -- - - -0.0973325183683015 - - -0.9952519182940992 - - - -0.0973325183683015 - - -0.9952519182940992 -- - - -0.1098437115424997 - - -0.9939488714388522 - - - -0.1098437115424997 - - -0.9939488714388522 -- - - -0.12233752437845594 - - -0.9924885541551351 - - - -0.12233752437845594 - - -0.9924885541551351 -- - - -0.13481198000658376 - - -0.9908711975058637 - - - -0.13481198000658376 - - -0.9908711975058637 -- - - -0.14726510462013975 - - -0.9890970574019616 - - - -0.14726510462013975 - - -0.9890970574019616 -- - - -0.15969492778754882 - - -0.9871664145618658 - - - -0.15969492778754882 - - -0.9871664145618658 -- - - -0.17209948276416748 - - -0.9850795744671118 - - - -0.17209948276416748 - - -0.9850795744671118 -- - - -0.18447680680349163 - - -0.9828368673139949 - - - -0.18447680680349163 - - -0.9828368673139949 -- - - -0.19682494146770374 - - -0.9804386479613271 - - - -0.19682494146770374 - - -0.9804386479613271 -- - - -0.2091419329375665 - - -0.9778852958742853 - - - -0.2091419329375665 - - -0.9778852958742853 -- - - -0.22142583232155733 - - -0.9751772150643726 - - - -0.22142583232155733 - - -0.9751772150643726 -- - - -0.23367469596425144 - - -0.9723148340254892 - - - -0.23367469596425144 - - -0.9723148340254892 -- - - -0.24588658575385006 - - -0.9692986056661356 - - - -0.24588658575385006 - - -0.9692986056661356 -- - - -0.2580595694288491 - - -0.9661290072377483 - - - -0.2580595694288491 - - -0.9661290072377483 -- - - -0.2701917208837818 - - -0.9628065402591844 - - - -0.2701917208837818 - - -0.9628065402591844 -- - - -0.2822811204739704 - - -0.9593317304373705 - - - -0.2822811204739704 - - -0.9593317304373705 -- - - -0.29432585531928135 - - -0.9557051275841171 - - - -0.29432585531928135 - - -0.9557051275841171 -- - - -0.30632401960678207 - - -0.951927305529127 - - - -0.30632401960678207 - - -0.951927305529127 -- - - -0.31827371489230794 - - -0.9479988620291956 - - - -0.31827371489230794 - - -0.9479988620291956 -- - - -0.3301730504008353 - - -0.9439204186736335 - - - -0.3301730504008353 - - -0.9439204186736335 -- - - -0.342020143325668 - - -0.9396926207859086 - - - -0.342020143325668 - - -0.9396926207859086 -- - - -0.35381311912633706 - - -0.9353161373215435 - - - -0.35381311912633706 - - -0.9353161373215435 -- - - -0.3655501118252182 - - -0.9307916607622624 - - - -0.3655501118252182 - - -0.9307916607622624 -- - - -0.37722926430276815 - - -0.9261199070064267 - - - -0.37722926430276815 - - -0.9261199070064267 -- - - -0.3888487285913865 - - -0.9213016152557545 - - - -0.3888487285913865 - - -0.9213016152557545 -- - - -0.4004066661678036 - - -0.9163375478983632 - - - -0.4004066661678036 - - -0.9163375478983632 -- - - -0.4119012482439916 - - -0.9112284903881362 - - - -0.4119012482439916 - - -0.9112284903881362 -- - - -0.4233306560565341 - - -0.9059752511204401 - - - -0.4233306560565341 - - -0.9059752511204401 -- - - -0.4346930811543944 - - -0.9005786613042189 - - - -0.4346930811543944 - - -0.9005786613042189 -- - - -0.4459867256850755 - - -0.8950395748304681 - - - -0.4459867256850755 - - -0.8950395748304681 -- - - -0.4572098026790778 - - -0.8893588681371309 - - - -0.4572098026790778 - - -0.8893588681371309 -- - - -0.46836053633265995 - - -0.8835374400704156 - - - -0.46836053633265995 - - -0.8835374400704156 -- - - -0.47943716228880834 - - -0.8775762117425784 - - - -0.47943716228880834 - - -0.8775762117425784 -- - - -0.4904379279164198 - - -0.8714761263861728 - - - -0.4904379279164198 - - -0.8714761263861728 -- - - -0.5013610925876044 - - -0.8652381492048091 - - - -0.5013610925876044 - - -0.8652381492048091 -- - - -0.5122049279531135 - - -0.8588632672204265 - - - -0.5122049279531135 - - -0.8588632672204265 -- - - -0.5229677182158008 - - -0.852352489117125 - - - -0.5229677182158008 - - -0.852352489117125 -- - - -0.5336477604021214 - - -0.8457068450815567 - - - -0.5336477604021214 - - -0.8457068450815567 -- - - -0.5442433646315787 - - -0.8389273866399275 - - - -0.5442433646315787 - - -0.8389273866399275 -- - - -0.5547528543841161 - - -0.8320151864916143 - - - -0.5547528543841161 - - -0.8320151864916143 -- - - -0.5651745667653925 - - -0.8249713383394304 - - - -0.5651745667653925 - - -0.8249713383394304 -- - - -0.5755068527698889 - - -0.8177969567165786 - - - -0.5755068527698889 - - -0.8177969567165786 -- - - -0.5857480775418389 - - -0.8104931768102923 - - - -0.5857480775418389 - - -0.8104931768102923 -- - - -0.5958966206338965 - - -0.8030611542822266 - - - -0.5958966206338965 - - -0.8030611542822266 -- - - -0.6059508762635476 - - -0.7955020650855904 - - - -0.6059508762635476 - - -0.7955020650855904 -- - - -0.6159092535671783 - - -0.7878171052790878 - - - -0.6159092535671783 - - -0.7878171052790878 -- - - -0.6257701768518052 - - -0.7800074908376589 - - - -0.6257701768518052 - - -0.7800074908376589 -- - - -0.6355320858443827 - - -0.7720744574600873 - - - -0.6355320858443827 - - -0.7720744574600873 -- - - -0.6451934359386927 - - -0.76401926037347 - - - -0.6451934359386927 - - -0.76401926037347 -- - - -0.6547526984397336 - - -0.7558431741346133 - - - -0.6547526984397336 - - -0.7558431741346133 -- - - -0.6642083608056132 - - -0.7475474924283543 - - - -0.6642083608056132 - - -0.7475474924283543 -- - - -0.6735589268868657 - - -0.7391335278628713 - - - -0.6735589268868657 - - -0.7391335278628713 -- - - -0.6828029171631881 - - -0.7306026117619896 - - - -0.6828029171631881 - - -0.7306026117619896 -- - - -0.6919388689775459 - - -0.7219560939545248 - - - -0.6919388689775459 - - -0.7219560939545248 -- - - -0.7009653367675964 - - -0.7131953425607112 - - - -0.7009653367675964 - - -0.7131953425607112 -- - - -0.7098808922944282 - - -0.7043217437757168 - - - -0.7098808922944282 - - -0.7043217437757168 -- - - -0.7186841248685372 - - -0.695336701650319 - - - -0.7186841248685372 - - -0.695336701650319 -- - - -0.7273736415730482 - - -0.6862416378687342 - - - -0.7273736415730482 - - -0.6862416378687342 -- - - -0.7359480674841022 - - -0.6770379915236775 - - - -0.7359480674841022 - - -0.6770379915236775 -- - - -0.7444060458884184 - - -0.6677272188886492 - - - -0.7444060458884184 - - -0.6677272188886492 -- - - -0.7527462384979536 - - -0.6583107931875202 - - - -0.7527462384979536 - - -0.6583107931875202 -- - - -0.7609673256616669 - - -0.648790204361418 - - - -0.7609673256616669 - - -0.648790204361418 -- - - -0.7690680065743155 - - -0.6391669588329865 - - - -0.7690680065743155 - - -0.6391669588329865 -- - - -0.7770469994822877 - - -0.6294425792680167 - - - -0.7770469994822877 - - -0.6294425792680167 -- - - -0.7849030418864043 - - -0.619618604334529 - - - -0.7849030418864043 - - -0.619618604334529 -- - - -0.7926348907416839 - - -0.609696588459308 - - - -0.7926348907416839 - - -0.609696588459308 -- - - -0.8002413226540318 - - -0.5996781015819452 - - - -0.8002413226540318 - - -0.5996781015819452 -- - - -0.807721134073806 - - -0.5895647289064406 - - - -0.807721134073806 - - -0.5895647289064406 -- - - -0.8150731414862619 - - -0.5793580706503675 - - - -0.8150731414862619 - - -0.5793580706503675 -- - - -0.8222961815988086 - - -0.5690597417916851 - - - -0.8222961815988086 - - -0.5690597417916851 -- - - -0.8293891115250823 - - -0.5586713718131927 - - - -0.8293891115250823 - - -0.5586713718131927 -- - - -0.8363508089657752 - - -0.5481946044447112 - - - -0.8363508089657752 - - -0.5481946044447112 -- - - -0.8431801723862219 - - -0.537631097402988 - - - -0.8431801723862219 - - -0.537631097402988 -- - - -0.8498761211906855 - - -0.5269825221294112 - - - -0.8498761211906855 - - -0.5269825221294112 -- - - -0.8564375958933453 - - -0.5162505635255297 - - - -0.8564375958933453 - - -0.5162505635255297 -- - - -0.8628635582859301 - - -0.5054369196864662 - - - -0.8628635582859301 - - -0.5054369196864662 -- - - -0.8691529916019983 - - -0.49454330163221977 - - - -0.8691529916019983 - - -0.49454330163221977 -- - - -0.8753049006778127 - - -0.4835714330369447 - - - -0.8753049006778127 - - -0.4835714330369447 -- - - -0.8813183121098064 - - -0.4725230499562131 - - - -0.8813183121098064 - - -0.4725230499562131 -- - - -0.8871922744086038 - - -0.46139990055231767 - - - -0.8871922744086038 - - -0.46139990055231767 -- - - -0.8929258581495678 - - -0.4502037448176746 - - - -0.8929258581495678 - - -0.4502037448176746 -- - - -0.898518156119867 - - -0.43893635429633115 - - - -0.898518156119867 - - -0.43893635429633115 -- - - -0.9039682834620154 - - -0.42759951180367056 - - - -0.9039682834620154 - - -0.42759951180367056 -- - - -0.9092753778138881 - - -0.4161950111443084 - - - -0.9092753778138881 - - -0.4161950111443084 -- - - -0.914438599445165 - - -0.40472465682827513 - - - -0.914438599445165 - - -0.40472465682827513 -- - - -0.919457131390205 - - -0.39319026378547983 - - - -0.919457131390205 - - -0.39319026378547983 -- - - -0.9243301795773077 - - -0.38159365707855025 - - - -0.9243301795773077 - - -0.38159365707855025 -- - - -0.9290569729543624 - - -0.36993667161404425 - - - -0.9290569729543624 - - -0.36993667161404425 -- - - -0.9336367636108461 - - -0.3582211518521277 - - - -0.9336367636108461 - - -0.3582211518521277 -- - - -0.9380688268961654 - - -0.34644895151472466 - - - -0.9380688268961654 - - -0.34644895151472466 -- - - -0.9423524615343185 - - -0.3346219332922018 - - - -0.9423524615343185 - - -0.3346219332922018 -- - - -0.946486989734852 - - -0.32274196854865056 - - - -0.946486989734852 - - -0.32274196854865056 -- - - -0.9504717573001114 - - -0.31081093702577167 - - - -0.9504717573001114 - - -0.31081093702577167 -- - - -0.9543061337287484 - - -0.2988307265454612 - - - -0.9543061337287484 - - -0.2988307265454612 -- - - -0.9579895123154887 - - -0.2868032327110909 - - - -0.9579895123154887 - - -0.2868032327110909 -- - - -0.9615213102471251 - - -0.27473035860758444 - - - -0.9615213102471251 - - -0.27473035860758444 -- - - -0.9649009686947388 - - -0.2626140145002827 - - - -0.9649009686947388 - - -0.2626140145002827 -- - - -0.9681279529021183 - - -0.25045611753270025 - - - -0.9681279529021183 - - -0.25045611753270025 -- - - -0.9712017522703761 - - -0.23825859142316594 - - - -0.9712017522703761 - - -0.23825859142316594 -- - - -0.9741218804387358 - - -0.22602336616045093 - - - -0.9741218804387358 - - -0.22602336616045093 -- - - -0.9768878753614922 - - -0.21375237769837674 - - - -0.9768878753614922 - - -0.21375237769837674 -- - - -0.9794992993811164 - - -0.2014475676495055 - - - -0.9794992993811164 - - -0.2014475676495055 -- - - -0.9819557392975065 - - -0.18911088297791753 - - - -0.9819557392975065 - - -0.18911088297791753 -- - - -0.9842568064333685 - - -0.17674427569114207 - - - -0.9842568064333685 - - -0.17674427569114207 -- - - -0.9864021366957143 - - -0.1643497025313075 - - - -0.9864021366957143 - - -0.1643497025313075 -- - - -0.9883913906334727 - - -0.1519291246655162 - - - -0.9883913906334727 - - -0.1519291246655162 -- - - -0.9902242534911982 - - -0.1394845073755471 - - - -0.9902242534911982 - - -0.1394845073755471 -- - - -0.9919004352588768 - - -0.12701781974687945 - - - -0.9919004352588768 - - -0.12701781974687945 -- - - -0.9934196707178105 - - -0.11453103435714257 - - - -0.9934196707178105 - - -0.11453103435714257 -- - - -0.9947817194825852 - - -0.10202612696398496 - - - -0.9947817194825852 - - -0.10202612696398496 -- - - -0.9959863660391042 - - -0.08950507619246842 - - - -0.9959863660391042 - - -0.08950507619246842 -- - - -0.9970334197786901 - - -0.07696986322198038 - - - -0.9970334197786901 - - -0.07696986322198038 -- - - -0.9979227150282431 - - -0.0644224714727701 - - - -0.9979227150282431 - - -0.0644224714727701 -- - - -0.9986541110764564 - - -0.051864886292102175 - - - -0.9986541110764564 - - -0.051864886292102175 -- - - -0.9992274921960794 - - -0.03929909464013164 - - - -0.9992274921960794 - - -0.03929909464013164 -- - - -0.9996427676622299 - - -0.026727084775506123 - - - -0.9996427676622299 - - -0.026727084775506123 -- - - -0.9998998717667489 - - -0.014150845940762564 - - - -0.9998998717667489 - - -0.014150845940762564 -- - - -0.9999987638285974 - - -0.001572368047586014 - - - -0.9999987638285974 - - -0.001572368047586014 -- - - -0.9999394282002937 - - 0.0110063586380641 - - - -0.9999394282002937 - - 0.0110063586380641 -- - - -0.9997218742703887 - - 0.02358334381085534 - - - -0.9997218742703887 - - 0.02358334381085534 -- - - -0.9993461364619809 - - 0.036156597441018276 - - - -0.9993461364619809 - - 0.036156597441018276 -- - - -0.9988122742272693 - - 0.04872413008921046 - - - -0.9988122742272693 - - 0.04872413008921046 -- - - -0.9981203720381463 - - 0.06128395322131545 - - - -0.9981203720381463 - - 0.06128395322131545 -- - - -0.9972705393728328 - - 0.0738340795230701 - - - -0.9972705393728328 - - 0.0738340795230701 -- - - -0.9962629106985544 - - 0.08637252321452737 - - - -0.9962629106985544 - - 0.08637252321452737 -- - - -0.9950976454502662 - - 0.09889730036424782 - - - -0.9950976454502662 - - 0.09889730036424782 -- - - -0.9937749280054243 - - 0.11140642920322712 - - - -0.9937749280054243 - - 0.11140642920322712 -- - - -0.9922949676548137 - - 0.12389793043845473 - - - -0.9922949676548137 - - 0.12389793043845473 -- - - -0.9906579985694319 - - 0.1363698275660986 - - - -0.9906579985694319 - - 0.1363698275660986 -- - - -0.9888642797634358 - - 0.14882014718424852 - - - -0.9888642797634358 - - 0.14882014718424852 -- - - -0.9869140950531602 - - 0.16124691930515087 - - - -0.9869140950531602 - - 0.16124691930515087 -- - - -0.9848077530122081 - - 0.17364817766692972 - - - -0.9848077530122081 - - 0.17364817766692972 -- - - -0.9825455869226281 - - 0.18602196004469043 - - - -0.9825455869226281 - - 0.18602196004469043 -- - - -0.9801279547221767 - - 0.19836630856101212 - - - -0.9801279547221767 - - 0.19836630856101212 -- - - -0.9775552389476866 - - 0.21067926999572462 - - - -0.9775552389476866 - - 0.21067926999572462 -- - - -0.9748278466745344 - - 0.2229588960949763 - - - -0.9748278466745344 - - 0.2229588960949763 -- - - -0.9719462094522341 - - 0.23520324387948816 - - - -0.9719462094522341 - - 0.23520324387948816 -- - - -0.9689107832361499 - - 0.24741037595200138 - - - -0.9689107832361499 - - 0.24741037595200138 -- - - -0.9657220483153551 - - 0.25957836080381363 - - - -0.9657220483153551 - - 0.25957836080381363 -- - - -0.9623805092366339 - - 0.27170527312041143 - - - -0.9623805092366339 - - 0.27170527312041143 -- - - -0.9588866947246498 - - 0.2837891940860965 - - - -0.9588866947246498 - - 0.2837891940860965 -- - - -0.9552411575982872 - - 0.29582821168760115 - - - -0.9552411575982872 - - 0.29582821168760115 -- - - -0.9514444746831768 - - 0.30782042101662727 - - - -0.9514444746831768 - - 0.30782042101662727 -- - - -0.9474972467204302 - - 0.31976392457124386 - - - -0.9474972467204302 - - 0.31976392457124386 -- - - -0.9434000982715814 - - 0.3316568325561384 - - - -0.9434000982715814 - - 0.3316568325561384 -- - - -0.9391536776197683 - - 0.3434972631816217 - - - -0.9391536776197683 - - 0.3434972631816217 -- - - -0.9347586566671513 - - 0.35528334296139286 - - - -0.9347586566671513 - - 0.35528334296139286 -- - - -0.9302157308286049 - - 0.3670132070089637 - - - -0.9302157308286049 - - 0.3670132070089637 -- - - -0.9255256189216783 - - 0.3786849993327492 - - - -0.9255256189216783 - - 0.3786849993327492 -- - - -0.9206890630528639 - - 0.3902968731297237 - - - -0.9206890630528639 - - 0.3902968731297237 -- - - -0.9157068285001696 - - 0.40184699107765015 - - - -0.9157068285001696 - - 0.40184699107765015 -- - - -0.9105797035920364 - - 0.41333352562578207 - - - -0.9105797035920364 - - 0.41333352562578207 -- - - -0.9053084995825972 - - 0.4247546592840467 - - - -0.9053084995825972 - - 0.4247546592840467 -- - - -0.8998940505233184 - - 0.4361085849106107 - - - -0.8998940505233184 - - 0.4361085849106107 -- - - -0.8943372131310279 - - 0.4473935059978257 - - - -0.8943372131310279 - - 0.4473935059978257 -- - - -0.8886388666523561 - - 0.45860763695649037 - - - -0.8886388666523561 - - 0.45860763695649037 -- - - -0.8827999127246203 - - 0.4697492033983695 - - - -0.8827999127246203 - - 0.4697492033983695 -- - - -0.8768212752331539 - - 0.48081644241696414 - - - -0.8768212752331539 - - 0.48081644241696414 -- - - -0.8707039001651283 - - 0.49180760286644026 - - - -0.8707039001651283 - - 0.49180760286644026 -- - - -0.8644487554598653 - - 0.502720945638721 - - - -0.8644487554598653 - - 0.502720945638721 -- - - -0.8580568308556884 - - 0.5135547439386501 - - - -0.8580568308556884 - - 0.5135547439386501 -- - - -0.8515291377333118 - - 0.5243072835572309 - - - -0.8515291377333118 - - 0.5243072835572309 -- - - -0.8448667089558188 - - 0.53497686314285 - - - -0.8448667089558188 - - 0.53497686314285 -- - - -0.838070598705227 - - 0.5455617944704909 - - - -0.838070598705227 - - 0.5455617944704909 -- - - -0.8311418823156947 - - 0.5560604027088458 - - - -0.8311418823156947 - - 0.5560604027088458 -- - - -0.8240816561033651 - - 0.5664710266853329 - - - -0.8240816561033651 - - 0.5664710266853329 -- - - -0.8168910371929057 - - 0.5767920191489293 - - - -0.8168910371929057 - - 0.5767920191489293 -- - - -0.8095711633407447 - - 0.5870217470308176 - - - -0.8095711633407447 - - 0.5870217470308176 -- - - -0.8021231927550442 - - 0.5971585917027857 - - - -0.8021231927550442 - - 0.5971585917027857 -- - - -0.7945483039124446 - - 0.6072009492333305 - - - -0.7945483039124446 - - 0.6072009492333305 -- - - -0.7868476953715905 - - 0.6171472306414546 - - - -0.7868476953715905 - - 0.6171472306414546 -- - - -0.7790225855834922 - - 0.6269958621480771 - - - -0.7790225855834922 - - 0.6269958621480771 -- - - -0.7710742126987252 - - 0.6367452854250599 - - - -0.7710742126987252 - - 0.6367452854250599 -- - - -0.7630038343715285 - - 0.6463939578417678 - - - -0.7630038343715285 - - 0.6463939578417678 -- - - -0.7548127275607995 - - 0.6559403527091668 - - - -0.7548127275607995 - - 0.6559403527091668 -- - - -0.7465021883280534 - - 0.6653829595213779 - - - -0.7465021883280534 - - 0.6653829595213779 -- - - -0.7380735316323398 - - 0.6747202841946918 - - - -0.7380735316323398 - - 0.6747202841946918 -- - - -0.7295280911221899 - - 0.6839508493039641 - - - -0.7295280911221899 - - 0.6839508493039641 -- - - -0.7208672189245859 - - 0.6930731943163961 - - - -0.7208672189245859 - - 0.6930731943163961 -- - - -0.7120922854310258 - - 0.7020858758226223 - - - -0.7120922854310258 - - 0.7020858758226223 -- - - -0.703204679080685 - - 0.7109874677651012 - - - -0.703204679080685 - - 0.7109874677651012 -- - - -0.694205806140723 - - 0.719776561663763 - - - -0.694205806140723 - - 0.719776561663763 -- - - -0.685097090483782 - - 0.7284517668388598 - - - -0.685097090483782 - - 0.7284517668388598 -- - - -0.6758799733626797 - - 0.7370117106310208 - - - -0.6758799733626797 - - 0.7370117106310208 -- - - -0.6665559131823733 - - 0.745455038618435 - - - -0.6665559131823733 - - 0.745455038618435 -- - - -0.6571263852691893 - - 0.7537804148311689 - - - -0.6571263852691893 - - 0.7537804148311689 -- - - -0.6475928816373955 - - 0.7619865219625438 - - - -0.6475928816373955 - - 0.7619865219625438 -- - - -0.6379569107531127 - - 0.7700720615775806 - - - -0.6379569107531127 - - 0.7700720615775806 -- - - -0.6282199972956439 - - 0.7780357543184383 - - - -0.6282199972956439 - - 0.7780357543184383 -- - - -0.6183836819162163 - - 0.7858763401068541 - - - -0.6183836819162163 - - 0.7858763401068541 -- - - -0.6084495209942188 - - 0.7935925783435136 - - - -0.6084495209942188 - - 0.7935925783435136 -- - - -0.5984190863909279 - - 0.8011832481043567 - - - -0.5984190863909279 - - 0.8011832481043567 -- - - -0.5882939652008056 - - 0.8086471483337546 - - - -0.5882939652008056 - - 0.8086471483337546 -- - - -0.5780757595003719 - - 0.8159830980345537 - - - -0.5780757595003719 - - 0.8159830980345537 -- - - -0.5677660860947084 - - 0.8231899364549449 - - - -0.5677660860947084 - - 0.8231899364549449 -- - - -0.5573665762616435 - - 0.8302665232721198 - - - -0.5573665762616435 - - 0.8302665232721198 -- - - -0.546878875493628 - - 0.8372117387727103 - - - -0.546878875493628 - - 0.8372117387727103 -- - - -0.5363046432373839 - - 0.8440244840299495 - - - -0.5363046432373839 - - 0.8440244840299495 -- - - -0.5256455526313215 - - 0.850703681077561 - - - -0.5256455526313215 - - 0.850703681077561 -- - - -0.5149032902408143 - - 0.8572482730803158 - - - -0.5149032902408143 - - 0.8572482730803158 -- - - -0.5040795557913256 - - 0.86365722450126 - - - -0.5040795557913256 - - 0.86365722450126 -- - - -0.49317606189947616 - - 0.8699295212655587 - - - -0.49317606189947616 - - 0.8699295212655587 -- - - -0.4821945338020488 - - 0.8760641709209576 - - - -0.4821945338020488 - - 0.8760641709209576 -- - - -0.4711367090830182 - - 0.8820602027948112 - - - -0.4711367090830182 - - 0.8820602027948112 -- - - -0.46000433739861224 - - 0.8879166681476723 - - - -0.46000433739861224 - - 0.8879166681476723 -- - - -0.44879918020046267 - - 0.893632640323412 - - - -0.44879918020046267 - - 0.893632640323412 -- - - -0.43752301045690567 - - 0.8992072148958361 - - - -0.43752301045690567 - - 0.8992072148958361 -- - - -0.4261776123724359 - - 0.9046395098117977 - - - -0.4261776123724359 - - 0.9046395098117977 -- - - -0.4147647811054085 - - 0.909928665530756 - - - -0.4147647811054085 - - 0.909928665530756 -- - - -0.403286322483982 - - 0.9150738451607857 - - - -0.403286322483982 - - 0.9150738451607857 -- - - -0.39174405272039897 - - 0.9200742345909907 - - - -0.39174405272039897 - - 0.9200742345909907 -- - - -0.3801397981235976 - - 0.9249290426203247 - - - -0.3801397981235976 - - 0.9249290426203247 -- - - -0.3684753948102517 - - 0.9296375010827764 - - - -0.3684753948102517 - - 0.9296375010827764 -- - - -0.3567526884142328 - - 0.9341988649689195 - - - -0.3567526884142328 - - 0.9341988649689195 -- - - -0.34497353379459245 - - 0.9386124125437886 - - - -0.34497353379459245 - - 0.9386124125437886 -- - - -0.33313979474205874 - - 0.9428774454610838 - - - -0.33313979474205874 - - 0.9428774454610838 -- - - -0.3212533436841441 - - 0.9469932888736632 - - - -0.3212533436841441 - - 0.9469932888736632 -- - - -0.30931606138887024 - - 0.9509592915403249 - - - -0.30931606138887024 - - 0.9509592915403249 -- - - -0.2973298366671729 - - 0.9547748259288534 - - - -0.2973298366671729 - - 0.9547748259288534 -- - - -0.28529656607405124 - - 0.9584392883153082 - - - -0.28529656607405124 - - 0.9584392883153082 -- - - -0.2732181536084666 - - 0.9619520988795546 - - - -0.2732181536084666 - - 0.9619520988795546 -- - - -0.26109651041208987 - - 0.9653127017970029 - - - -0.26109651041208987 - - 0.9653127017970029 -- - - -0.24893355446689247 - - 0.9685205653265596 - - - -0.24893355446689247 - - 0.9685205653265596 -- - - -0.2367312102916815 - - 0.9715751818947599 - - - -0.2367312102916815 - - 0.9715751818947599 -- - - -0.22449140863757358 - - 0.974476068176083 - - - -0.22449140863757358 - - 0.974476068176083 -- - - -0.2122160861825098 - - 0.9772227651694252 - - - -0.2122160861825098 - - 0.9772227651694252 -- - - -0.19990718522480572 - - 0.9798148382707292 - - - -0.19990718522480572 - - 0.9798148382707292 -- - - -0.1875666533758392 - - 0.9822518773417477 - - - -0.1875666533758392 - - 0.9822518773417477 -- - - -0.17519644325187023 - - 0.9845334967749417 - - - -0.17519644325187023 - - 0.9845334967749417 -- - - -0.16279851216509478 - - 0.9866593355544919 - - - -0.16279851216509478 - - 0.9866593355544919 -- - - -0.1503748218139381 - - 0.9886290573134224 - - - -0.1503748218139381 - - 0.9886290573134224 -- - - -0.1379273379726542 - - 0.9904423503868245 - - - -0.1379273379726542 - - 0.9904423503868245 -- - - -0.12545803018029758 - - 0.9920989278611683 - - - -0.12545803018029758 - - 0.9920989278611683 -- - - -0.11296887142907358 - - 0.9935985276197029 - - - -0.11296887142907358 - - 0.9935985276197029 -- - - -0.10046183785216964 - - 0.9949409123839287 - - - -0.10046183785216964 - - 0.9949409123839287 -- - - -0.08793890841106214 - - 0.9961258697511428 - - - -0.08793890841106214 - - 0.9961258697511428 -- - - -0.07540206458240344 - - 0.9971532122280462 - - - -0.07540206458240344 - - 0.9971532122280462 -- - - -0.06285329004448297 - - 0.9980227772604111 - - - -0.06285329004448297 - - 0.9980227772604111 -- - - -0.05029457036336817 - - 0.9987344272588005 - - - -0.05029457036336817 - - 0.9987344272588005 -- - - -0.037727892678718344 - - 0.99928804962034 - - - -0.037727892678718344 - - 0.99928804962034 -- - - -0.025155245389377974 - - 0.9996835567465338 - - - -0.025155245389377974 - - 0.9996835567465338 -- - - -0.012578617838742366 - - 0.9999208860571255 - - - -0.012578617838742366 - - 0.9999208860571255 -- - - -4.898587196589413e-16 - - 1.0 - - - -4.898587196589413e-16 - - 1.0 -init_spikes: -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -- - 0.0 - - 0.0 -n_neurons: 2 diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 53d4a398..83a7f7cf 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -1,13 +1,12 @@ from typing import Union -import jax.numpy as jnp import jax +import jax.numpy as jnp import pytest from numpy.typing import NDArray -from neurostatslib.base_class import BaseRegressor, _Base - import neurostatslib as nsl +from neurostatslib.base_class import BaseRegressor, _Base @pytest.fixture From e43991c5483ac00a3810abe1b8b44a44acaa6595 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 12:21:32 -0400 Subject: [PATCH 113/250] requirement updated --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17a0297d..1cf71706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,9 +62,10 @@ docs = [ "mkdocstrings[python]", # Python-specific plugin for mkdocs "mkdocs-section-index", # Plugin for generating a section index in mkdocs "mkdocs-gen-files", # Plugin for generating additional files in mkdocs - "mkdocs-literate-nav", # Plugin for literate-style navigation in mkdocs + "mkdocs-literate-nav>=6.1", # Plugin for literate-style navigation in mkdocs "mkdocs-gallery", # Plugin for adding image galleries to mkdocs - "mkdocs-material" + "mkdocs-material", + "mkdocs-autorefs>=0.5" ] From e336d9ecae0bf50a233fb07accf7af58b8a6bbbc Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 12:35:54 -0400 Subject: [PATCH 114/250] bugfix version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1cf71706..6f47475b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ docs = [ "mkdocstrings[python]", # Python-specific plugin for mkdocs "mkdocs-section-index", # Plugin for generating a section index in mkdocs "mkdocs-gen-files", # Plugin for generating additional files in mkdocs - "mkdocs-literate-nav>=6.1", # Plugin for literate-style navigation in mkdocs + "mkdocs-literate-nav>=0.6.1", # Plugin for literate-style navigation in mkdocs "mkdocs-gallery", # Plugin for adding image galleries to mkdocs "mkdocs-material", "mkdocs-autorefs>=0.5" From a4eb0eb55cf49e69e2bd050a5eead948177d9a0b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 15:42:53 -0400 Subject: [PATCH 115/250] coverage over src --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6f47475b..4981a5a9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,7 +100,7 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] testpaths = ["tests"] # Specify the directory where test files are located -addopts = "--cov=neurostatslib" +addopts = "--cov=src" [tool.coverage.report] exclude_lines = [ From c868f68f16a8c1c683c8fef764932db332cf75ec Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 17:00:32 -0400 Subject: [PATCH 116/250] changed function naming --- src/neurostatslib/base_class.py | 4 ++-- src/neurostatslib/basis.py | 28 ++++++++++++++-------------- src/neurostatslib/utils.py | 7 ++++--- tests/conftest.py | 15 --------------- 4 files changed, 20 insertions(+), 34 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index c3577838..2f335264 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -11,7 +11,7 @@ from jax._src.lib import xla_client from numpy.typing import ArrayLike, NDArray -from .utils import has_local_device, is_sequence +from .utils import has_local_device, is_list_like class _Base: @@ -340,7 +340,7 @@ def _check_and_convert_params( It ensures that the parameters and data are compatible for the model. """ - if not is_sequence(params): + if not is_list_like(params): raise TypeError("Initial parameters must be array-like!") if len(params) != 2: diff --git a/src/neurostatslib/basis.py b/src/neurostatslib/basis.py index ee7ff73e..0da14e82 100644 --- a/src/neurostatslib/basis.py +++ b/src/neurostatslib/basis.py @@ -64,7 +64,7 @@ def _evaluate(self, *xi: NDArray) -> NDArray: pass @staticmethod - def _get_samples(*n_samples: int) -> Generator[NDArray, None, None]: + def _get_samples(*n_samples: int) -> Generator[NDArray]: """Get equi-spaced samples for all the input dimensions. This will be used to evaluate the basis on a grid of @@ -520,12 +520,11 @@ class MSplineBasis(SplineBasis): at each interior knot. The higher this number, the smoother the basis representation will be. - References ---------- - .. [1] Ramsay, J. O. (1988). Monotone regression splines in action. - Statistical science, 3(4), 425-441. - + [^1]: + Ramsay, J. O. (1988). Monotone regression splines in action. + Statistical science, 3(4), 425-441. """ def __init__(self, n_basis_funcs: int, order: int = 2) -> None: @@ -581,8 +580,9 @@ class BSplineBasis(SplineBasis): References ---------- - ..[2] Prautzsch, H., Boehm, W., Paluszny, M. (2002). B-spline representation. In: Bézier and B-Spline Techniques. - Mathematics and Visualization. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-662-04919-8_5 + [^2]: + Prautzsch, H., Boehm, W., Paluszny, M. (2002). B-spline representation. In: Bézier and B-Spline Techniques. + Mathematics and Visualization. Springer, Berlin, Heidelberg. https://doi.org/10.1007/978-3-662-04919-8_5 """ @@ -613,7 +613,6 @@ def _evaluate(self, sample_pts: NDArray) -> NDArray: The evaluation is performed by looping over each element and using `splev` from SciPy to compute the basis values. """ - # add knots knot_locs = self._generate_knots(sample_pts, 0.0, 1.0) @@ -677,7 +676,6 @@ def _evaluate(self, sample_pts: NDArray) -> NDArray: The evaluation is performed by looping over each element and using `splev` from SciPy to compute the basis values. """ - knot_locs = self._generate_knots(sample_pts, 0.0, 1.0, is_cyclic=True) # for cyclic, do not repeat knots @@ -766,10 +764,11 @@ def _evaluate(self, sample_pts: NDArray) -> NDArray: class RaisedCosineBasisLinear(RaisedCosineBasis): - """Linearly-spaced raised cosine basis functions used by Pillow et al. [2]_. + """Linearly-spaced raised cosine basis functions used by Pillow et al. These are "cosine bumps" that uniformly tile the space. + Parameters ---------- n_basis_funcs @@ -777,10 +776,11 @@ class RaisedCosineBasisLinear(RaisedCosineBasis): References ---------- - .. [2] Pillow, J. W., Paninski, L., Uzzel, V. J., Simoncelli, E. P., & J., - C. E. (2005). Prediction and decoding of retinal ganglion cell responses - with a probabilistic spiking model. Journal of Neuroscience, 25(47), - 11003–11013. http://dx.doi.org/10.1523/jneurosci.3305-05.2005 + [^3]: + Pillow, J. W., Paninski, L., Uzzel, V. J., Simoncelli, E. P., & J., + C. E. (2005). Prediction and decoding of retinal ganglion cell responses + with a probabilistic spiking model. Journal of Neuroscience, 25(47), + 11003–11013. http://dx.doi.org/10.1523/jneurosci.3305-05.2005 """ diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index 1c8ade07..a033d806 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -394,7 +394,7 @@ def has_local_device(device_type: str) -> bool: Parameters ---------- device_type: - The the device type in lower-case, e.g. `gpu`, `tpu`... + The device type in lower-case, e.g. `gpu`, `tpu`... Returns ------- @@ -407,8 +407,9 @@ def has_local_device(device_type: str) -> bool: ) -def is_sequence(obj) -> bool: - """Check if an object is a sequence.""" +def is_list_like(obj) -> bool: + """Check if the object is an iterable (not including strings or bytes) + that supports item retrieval by index but isn't a dictionary.""" return ( hasattr(obj, "__iter__") and hasattr(obj, "__getitem__") diff --git a/tests/conftest.py b/tests/conftest.py index 4a61912a..f4f0d670 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,21 +4,6 @@ This module contains test fixtures required to set up and verify the functionality of the modules of the `neurostatslib` library. -Dependencies: - - jax: Used for efficient numerical computing. - - jax.numpy: JAX's version of NumPy, used for matrix operations. - - numpy: Standard Python numerical computing library. - - pytest: Testing framework. - - json: For parsing and loading json configuration files. - -Functions: - - poissonGLM_model_instantiation: Sets up a Poisson GLM, instantiating its parameters - with random values and returning a set of test data and expected output. - - - poissonGLM_coupled_model_config_simulate: Reads from a json configuration file, - sets up a Poisson GLM with predefined parameters, and returns the initialized model - along with other related parameters. - Note: This module primarily serves as a utility for test configurations, setting up initial conditions, and loading predefined parameters for testing various functionalities of the `neurostatslib` library. From e434236da5f300122c9bf6d4629c3f9545b2807d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 17:40:50 -0400 Subject: [PATCH 117/250] refractored name to Base --- docs/developers_notes/02-base_class.md | 18 +++++++++--------- src/neurostatslib/base_class.py | 4 ++-- src/neurostatslib/noise_model.py | 4 ++-- src/neurostatslib/solver.py | 4 ++-- tests/test_base_class.py | 4 ++-- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 6ad242bf..d3fb631b 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -2,16 +2,16 @@ ## Introduction -The `base_class` module introduces the `_Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `_Base`. +The `base_class` module introduces the `Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `Base`. -The `_Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, noise models, solvers etc.). In contrast, abstract classes derived from `_Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `noise_model.NoiseModel` is the building block for the Poisson noise, Gamma noise, ... etc.). +The `Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, noise models, solvers etc.). In contrast, abstract classes derived from `Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `noise_model.NoiseModel` is the building block for the Poisson noise, Gamma noise, ... etc.). Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. Below a scheme of how we envision the architecture of the `neurostatslib` models. ``` -Abstract Class _Base +Abstract Class Base │ ├─ Abstract Subclass BaseRegressor │ │ @@ -41,13 +41,13 @@ Abstract Class _Base ``` !!! Example - The current package version includes a concrete class named `neurostatslib.glm.GLM`. This class inherits from `BaseRegressor`, which in turn inherits `_Base`, since it falls under the " GLM regression" category. + The current package version includes a concrete class named `neurostatslib.glm.GLM`. This class inherits from `BaseRegressor`, which in turn inherits `Base`, since it falls under the " GLM regression" category. As any `BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. -## The Class `model_base._Base` +## The Class `model_base.Base` -The `_Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. dditionally, the class provides auxiliary helper methods to identify available computational devices (such as GPUs and TPUs) and to facilitate data transfer to these devices. +The `Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. dditionally, the class provides auxiliary helper methods to identify available computational devices (such as GPUs and TPUs) and to facilitate data transfer to these devices. For a detailed understanding, consult the [`scikit-learn` API Reference](https://scikit-learn.org/stable/modules/classes.html) and [`BaseEstimator`](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html). @@ -63,7 +63,7 @@ For a detailed understanding, consult the [`scikit-learn` API Reference](https:/ ## The Abstract Class `model_base.BaseRegressor` -`BaseRegressor` is an abstract class that inherits from `_Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. +`BaseRegressor` is an abstract class that inherits from `Base`, stipulating the implementation of abstract methods: `fit`, `predict`, `score`, and `simulate`. This ensures seamless assimilation with `scikit-learn` pipelines and cross-validation procedures. ### Abstract Methods @@ -87,7 +87,7 @@ Moreover, `BaseRegressor` incorporates auxiliary methods such as `_convert_to_jn and a number of other methods for checking input consistency. !!! Tip - Deciding between concrete and abstract methods in a superclass can be nuanced. As a general guideline: any method that's expected in all subclasses and isn't subclass-specific should be concretely implemented in the superclass. Conversely, methods essential for a subclass's expected behavior, but vary based on the subclass, should be abstract in the superclass. For instance, compatibility with the `sklearn.cross_validation` module demands `score`, `fit`, `get_params`, and `set_params` methods. Given their specificity to individual models, `score` and `fit` are abstract in `BaseRegressor`. Conversely, as `get_params` and `set_params` are consistent across model classes, they're inherited from `_Base`. This approach typifies our general implementation strategy. However, it's important to note that while these are sound guidelines, exceptions exist based on various factors like future extensibility, clarity, and maintainability. + Deciding between concrete and abstract methods in a superclass can be nuanced. As a general guideline: any method that's expected in all subclasses and isn't subclass-specific should be concretely implemented in the superclass. Conversely, methods essential for a subclass's expected behavior, but vary based on the subclass, should be abstract in the superclass. For instance, compatibility with the `sklearn.cross_validation` module demands `score`, `fit`, `get_params`, and `set_params` methods. Given their specificity to individual models, `score` and `fit` are abstract in `BaseRegressor`. Conversely, as `get_params` and `set_params` are consistent across model classes, they're inherited from `Base`. This approach typifies our general implementation strategy. However, it's important to note that while these are sound guidelines, exceptions exist based on various factors like future extensibility, clarity, and maintainability. ## Contributor Guidelines @@ -98,5 +98,5 @@ When devising a new model subclass based on the `BaseRegressor` abstract class, - **Must** inherit the `BaseRegressor` abstract superclass. - **Must** realize the abstract methods: `fit`, `predict`, `score`, and `simulate`. -- **Should not** overwrite the `get_params` and `set_params` methods, inherited from `_Base`. +- **Should not** overwrite the `get_params` and `set_params` methods, inherited from `Base`. - **May** introduce auxiliary methods such as `_convert_to_jnp_ndarray` for added utility. diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 2f335264..87c53821 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -14,7 +14,7 @@ from .utils import has_local_device, is_list_like -class _Base: +class Base: """Base class for neurostatslib estimators. A base class for estimators with utilities for getting and setting parameters, @@ -237,7 +237,7 @@ def _get_param_names(cls): return sorted(parameters) -class BaseRegressor(_Base, abc.ABC): +class BaseRegressor(Base, abc.ABC): """Abstract base class for GLM regression models. This class encapsulates the common functionality for Generalized Linear Models (GLM) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index ccc5535e..8be1be24 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -6,7 +6,7 @@ import jax import jax.numpy as jnp -from .base_class import _Base +from .base_class import Base KeyArray = Union[jnp.ndarray, jax.random.PRNGKeyArray] @@ -17,7 +17,7 @@ def __dir__(): return __all__ -class NoiseModel(_Base, abc.ABC): +class NoiseModel(Base, abc.ABC): """ Abstract noise model class for neural data processing. diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 6681e22e..37a913cd 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -13,7 +13,7 @@ import jaxopt from numpy.typing import NDArray -from .base_class import _Base +from .base_class import Base from .proximal_operator import prox_group_lasso __all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] @@ -23,7 +23,7 @@ def __dir__() -> list[str]: return __all__ -class Solver(_Base, abc.ABC): +class Solver(Base, abc.ABC): """ Abstract base class for optimization solvers. diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 83a7f7cf..f01e052b 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -6,7 +6,7 @@ from numpy.typing import NDArray import neurostatslib as nsl -from neurostatslib.base_class import BaseRegressor, _Base +from neurostatslib.base_class import BaseRegressor, Base @pytest.fixture @@ -65,7 +65,7 @@ def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) pass -class BadEstimator(_Base): +class BadEstimator(Base): def __init__(self, param1, *args): super().__init__() pass From 6c39aa384d71820e8214c7ba21b3ae7d9744e503 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 17:55:13 -0400 Subject: [PATCH 118/250] removed hidden attributes --- src/neurostatslib/base_class.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 87c53821..1b3ca41e 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -29,11 +29,6 @@ class Base: **kwargs : dict Arbitrary keyword arguments. - Attributes - ---------- - _kwargs_keys : list - List of keyword arguments provided during the initialization. - Notes ----- The class provides helper methods mimicking scikit-learn's get_params and set_params. From 60202902cbdc62505443479654016ba54323987b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 18:15:12 -0400 Subject: [PATCH 119/250] refer to jax --- src/neurostatslib/base_class.py | 36 ++------------------------------- src/neurostatslib/glm.py | 2 +- tests/test_base_class.py | 27 +++++++++---------------- 3 files changed, 12 insertions(+), 53 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 1b3ca41e..15ca69c8 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -131,40 +131,8 @@ def set_params(self, **params: Any): return self @staticmethod - def select_target_device(device: Literal["cpu", "tpu", "gpu"]) -> xla_client.Device: - """Select a device. - - Parameters - ---------- - device - A device between "cpu", "gpu" or "tpu". Rolls back to "cpu" if device is not found. - - Returns - ------- - The selected device. - - Raises - ------ - ValueError - If the an invalid device name is provided. - """ - if device in ["cpu", "gpu", "tpu"]: - if has_local_device(device): - target_device = jax.devices(device)[0] - else: - raise RuntimeError( - f"Unknown backend: '{device}' requested, but no " - f"platforms that are instances of {device} are present." - ) - - else: - raise ValueError( - f"Invalid device specification: {device}. Choose `cpu`, `gpu` or `tpu`." - ) - return target_device - def device_put( - self, *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] + *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] ) -> Union[Any, jnp.ndarray]: """Send arrays to device. @@ -183,7 +151,7 @@ def device_put( : The arrays on the desired device. """ - device_obj = self.select_target_device(device) + device_obj = jax.devices(device)[0] return tuple( jax.device_put(arg, device_obj) if arg.device_buffer.device() != device_obj diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 134adae3..f30d31b3 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -561,7 +561,7 @@ def scan_fn( activity, chunk = data # Convolve the neural activity with the coupling basis matrix - conv_act = convolve_1d_trials(coupling_basis_matrix, activity[None, :, :])[ + conv_act = convolve_1d_trials(coupling_basis_matrix, activity[None])[ 0 ] diff --git a/tests/test_base_class.py b/tests/test_base_class.py index f01e052b..1a40587a 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Literal, Union import jax import jax.numpy as jnp @@ -297,22 +297,9 @@ def test_empty_set(mock_regressor): """ assert mock_regressor.set_params() is mock_regressor -@pytest.mark.parametrize("device_name", [1, "none"]) -def test_target_device_invalid_device_name(device_name, mock_regressor): - with pytest.raises(ValueError, match="Invalid device specification"): - mock_regressor.select_target_device(device_name) -@pytest.mark.parametrize("device_name", ["cpu", "gpu", "tpu"]) -def test_target_device_availability(device_name, mock_regressor): - raise_exception = not nsl.utils.has_local_device(device_name) - if raise_exception: - with pytest.raises(RuntimeError, match=f"Unknown backend: '{device_name}' requested, but no "): - mock_regressor.select_target_device(device_name) - else: - mock_regressor.select_target_device(device_name) - -@pytest.mark.parametrize("device_name", ["cpu", "gpu", "tpu"]) -def test_target_device_put(device_name, mock_regressor): +@pytest.mark.parametrize("device_name", ["cpu", "gpu", "tpu", "unknown"]) +def test_target_device_put(device_name: Literal["cpu", "gpu", "tpu"], mock_regressor): """Test that put works. Put array to device and checks that the device is matched after put, if device is found. @@ -321,8 +308,12 @@ def test_target_device_put(device_name, mock_regressor): raise_exception = not nsl.utils.has_local_device(device_name) x = jnp.array([1]) if raise_exception: - with pytest.raises(RuntimeError, match=f"Unknown backend: '{device_name}' requested, but no "): - mock_regressor.device_put(x, device=device_name) + if device_name != "tpu": + with pytest.raises(RuntimeError, match=f"Unknown backend"): + mock_regressor.device_put(x, device=device_name) + else: + with pytest.raises(RuntimeError, match=f"Backend '{device_name}' failed to initialize: "): + mock_regressor.device_put(x, device=device_name) else: x, = mock_regressor.device_put(x, device=device_name) assert x.device().device_kind == device_name From 10af2fe85ebfe9891c95506f5d517e2ecf04c83b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 18:46:29 -0400 Subject: [PATCH 120/250] moved conversion to utils.py --- src/neurostatslib/__init__.py | 2 -- src/neurostatslib/base_class.py | 32 +++++--------------------------- src/neurostatslib/glm.py | 8 ++++---- src/neurostatslib/utils.py | 23 ++++++++++++++++++++++- tests/test_base_class.py | 3 ++- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index b6f82569..892ab3ea 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 from . import ( - base_class, basis, - exceptions, glm, noise_model, sample_points, diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 15ca69c8..277e6fdc 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -8,10 +8,9 @@ import jax import jax.numpy as jnp -from jax._src.lib import xla_client from numpy.typing import ArrayLike, NDArray -from .utils import has_local_device, is_list_like +from .utils import convert_to_jnp_ndarray, is_list_like class Base: @@ -222,7 +221,7 @@ class BaseRegressor(Base, abc.ABC): - [`GLMRecurrent`](../glm/#neurostatslib.glm.GLMRecurrent): A recurrent GLM implementation. """ - FLOAT_EPS = jnp.finfo(jnp.float32).eps + FLOAT_EPS = jnp.finfo(float).eps @abc.abstractmethod def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): @@ -254,27 +253,6 @@ def simulate( """Simulate neural activity in response to a feed-forward input and recurrent activity.""" pass - @staticmethod - def _convert_to_jnp_ndarray( - *args: Union[NDArray, jnp.ndarray], data_type: Optional[jnp.dtype] = None - ) -> Tuple[jnp.ndarray, ...]: - """Convert provided arrays to jnp.ndarray of specified type. - - Parameters - ---------- - *args : - Input arrays to convert. - data_type : - Data type to convert to. Default is None, which means that the data-type - is inferred from the input. - - Returns - ------- - : - Converted arrays. - """ - return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) - @staticmethod def _has_invalid_entry(array: jnp.ndarray) -> bool: """Check if the array has nans or infs. @@ -448,7 +426,7 @@ def preprocess_fit( ValueError If there are inconsistencies in the input shapes or if NaNs or Infs are detected. """ - X, y = self._convert_to_jnp_ndarray(X, y) + X, y = convert_to_jnp_ndarray(X, y) # check input dimensionality self._check_input_dimensionality(X, y) @@ -517,7 +495,7 @@ def preprocess_simulate( If the feedforward_input contains NaNs or Infs. If the dimensionality or consistency checks fail for the provided data and parameters. """ - (feedforward_input,) = self._convert_to_jnp_ndarray(feedforward_input) + (feedforward_input,) = convert_to_jnp_ndarray(feedforward_input) self._check_input_dimensionality(X=feedforward_input) self._check_input_and_params_consistency(params_f, X=feedforward_input) @@ -531,7 +509,7 @@ def preprocess_simulate( ) # If both are provided, perform checks and conversions elif init_y is not None and params_r is not None: - init_y = self._convert_to_jnp_ndarray(init_y)[ + init_y = convert_to_jnp_ndarray(init_y)[ 0 ] # Assume this method returns a tuple self._check_input_dimensionality(y=init_y) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index f30d31b3..dd8ee8d2 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -9,7 +9,7 @@ from . import solver as slv from .base_class import BaseRegressor from .exceptions import NotFittedError -from .utils import convolve_1d_trials +from .utils import convert_to_jnp_ndarray, convolve_1d_trials class GLM(BaseRegressor): @@ -154,7 +154,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - (X,) = self._convert_to_jnp_ndarray(X) + (X,) = convert_to_jnp_ndarray(X) # check input dimensionality self._check_input_dimensionality(X=X) @@ -272,7 +272,7 @@ def score( Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - X, y = self._convert_to_jnp_ndarray(X, y) + X, y = convert_to_jnp_ndarray(X, y) self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) @@ -517,7 +517,7 @@ def simulate_recurrent( self._check_is_fit() # convert to jnp.ndarray - (coupling_basis_matrix,) = self._convert_to_jnp_ndarray(coupling_basis_matrix) + (coupling_basis_matrix,) = convert_to_jnp_ndarray(coupling_basis_matrix) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index a033d806..d70c7cad 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import partial -from typing import Iterable, List, Literal, Optional, Union +from typing import Iterable, List, Literal, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -415,3 +415,24 @@ def is_list_like(obj) -> bool: and hasattr(obj, "__getitem__") and not isinstance(obj, (str, bytes, dict)) ) + + +def convert_to_jnp_ndarray( + *args: Union[NDArray, jnp.ndarray], data_type: Optional[jnp.dtype] = None +) -> Tuple[jnp.ndarray, ...]: + """Convert provided arrays to jnp.ndarray of specified type. + + Parameters + ---------- + *args : + Input arrays to convert. + data_type : + Data type to convert to. Default is None, which means that the data-type + is inferred from the input. + + Returns + ------- + : + Converted arrays. + """ + return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 1a40587a..f17a5a29 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -7,6 +7,7 @@ import neurostatslib as nsl from neurostatslib.base_class import BaseRegressor, Base +from neurostatslib.utils import convert_to_jnp_ndarray @pytest.fixture @@ -114,7 +115,7 @@ def test_get_param_names(): def test_convert_to_jnp_ndarray(): """Test data conversion to JAX NumPy arrays.""" data = [1, 2, 3] - jnp_data, = BaseRegressor._convert_to_jnp_ndarray(data) + jnp_data, = convert_to_jnp_ndarray(data) assert isinstance(jnp_data, jnp.ndarray) assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) From 379ae4c1c6b70869dcf351ca6d027de6f4c8fdc9 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 30 Oct 2023 20:01:08 -0400 Subject: [PATCH 121/250] moved check invalid entry to utils --- src/neurostatslib/base_class.py | 24 ++++-------------------- src/neurostatslib/utils.py | 16 ++++++++++++++++ tests/test_base_class.py | 6 +++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 277e6fdc..377642bc 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -10,7 +10,7 @@ import jax.numpy as jnp from numpy.typing import ArrayLike, NDArray -from .utils import convert_to_jnp_ndarray, is_list_like +from .utils import convert_to_jnp_ndarray, has_invalid_entry, is_list_like class Base: @@ -253,22 +253,6 @@ def simulate( """Simulate neural activity in response to a feed-forward input and recurrent activity.""" pass - @staticmethod - def _has_invalid_entry(array: jnp.ndarray) -> bool: - """Check if the array has nans or infs. - - Parameters - ---------- - array: - The array to be checked. - - Returns - ------- - True if a nan or an inf is present, False otherwise - - """ - return (jnp.isinf(array) | jnp.isnan(array)).any() - @staticmethod def _check_and_convert_params( params: Tuple[ArrayLike, ArrayLike], data_type: Optional[jnp.dtype] = None @@ -432,9 +416,9 @@ def preprocess_fit( self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) - if self._has_invalid_entry(X): + if has_invalid_entry(X): raise ValueError("Input X contains a NaNs or Infs!") - if self._has_invalid_entry(y): + if has_invalid_entry(y): raise ValueError("Input y contains a NaNs or Infs!") _, n_neurons = y.shape @@ -499,7 +483,7 @@ def preprocess_simulate( self._check_input_dimensionality(X=feedforward_input) self._check_input_and_params_consistency(params_f, X=feedforward_input) - if self._has_invalid_entry(feedforward_input): + if has_invalid_entry(feedforward_input): raise ValueError("feedforward_input contains a NaNs or Infs!") # Ensure that both or neither of `init_y` and `params_r` are provided diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index d70c7cad..bfdb5de7 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -436,3 +436,19 @@ def convert_to_jnp_ndarray( Converted arrays. """ return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) + + +def has_invalid_entry(array: jnp.ndarray) -> jnp.ndarray: + """Check if the array has nans or infs. + + Parameters + ---------- + array: + The array to be checked. + + Returns + ------- + True if a nan or an inf is present, False otherwise + + """ + return jnp.any(jnp.isinf(array) | jnp.isnan(array)) diff --git a/tests/test_base_class.py b/tests/test_base_class.py index f17a5a29..92a5949a 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -7,7 +7,7 @@ import neurostatslib as nsl from neurostatslib.base_class import BaseRegressor, Base -from neurostatslib.utils import convert_to_jnp_ndarray +from neurostatslib.utils import convert_to_jnp_ndarray, has_invalid_entry @pytest.fixture @@ -124,8 +124,8 @@ def test_has_invalid_entry(): """Test validation of data arrays.""" valid_data = jnp.array([1, 2, 3]) invalid_data = jnp.array([1, 2, jnp.nan]) - assert not BaseRegressor._has_invalid_entry(valid_data) - assert BaseRegressor._has_invalid_entry(invalid_data) + assert not has_invalid_entry(valid_data) + assert has_invalid_entry(invalid_data) # To ensure abstract methods aren't callable From 3229c0587ac3470f8d0edc7084d819017980cd70 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Mon, 30 Oct 2023 20:01:48 -0400 Subject: [PATCH 122/250] Update src/neurostatslib/base_class.py Co-authored-by: William F. Broderick --- src/neurostatslib/base_class.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index c3577838..35cd8d09 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -347,9 +347,8 @@ def _check_and_convert_params( raise ValueError("Params needs to be array-like of length two.") try: - params = jnp.asarray(params[0], dtype=data_type), jnp.asarray( - params[1], dtype=data_type - ) + params = (jnp.asarray(params[0], dtype=data_type), + jnp.asarray(params[1], dtype=data_type)) except (ValueError, TypeError): raise TypeError( "Initial parameters must be array-like of array-like objects" From 255d969d0c449eeba3275250531cd9a9ffcf74f5 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Mon, 30 Oct 2023 20:02:26 -0400 Subject: [PATCH 123/250] Update src/neurostatslib/glm.py Co-authored-by: William F. Broderick --- src/neurostatslib/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 134adae3..8ee3e24c 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -565,7 +565,7 @@ def scan_fn( 0 ] - # Extract the corresponding slice of the feedforward input for the current time step + # Extract the slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( feed_forward_contrib, (chunk, 0), From 89df69c7578cb661b31605edacea0ad415325e82 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:18:53 -0400 Subject: [PATCH 124/250] changed mock tests and changed get_params --- src/neurostatslib/base_class.py | 9 ---- tests/test_base_class.py | 85 ++++++++++++++++++++------------- 2 files changed, 51 insertions(+), 43 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index cc57559b..7d340965 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -33,12 +33,6 @@ class Base: The class provides helper methods mimicking scikit-learn's get_params and set_params. Additionally, it has methods for selecting target devices and sending arrays to them. """ - - def __init__(self, **kwargs): - self._kwargs_keys = list(kwargs.keys()) - for key in kwargs: - setattr(self, key, kwargs[key]) - def get_params(self, deep=True): """ From scikit-learn, get parameters by inspecting init. @@ -60,9 +54,6 @@ def get_params(self, deep=True): deep_items = value.get_params().items() out.update((key + "__" + k, val) for k, val in deep_items) out[key] = value - # add kwargs - for key in self._kwargs_keys: - out[key] = getattr(self, key) return out def set_params(self, **params: Any): diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 92a5949a..e239f98a 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -6,7 +6,7 @@ from numpy.typing import NDArray import neurostatslib as nsl -from neurostatslib.base_class import BaseRegressor, Base +from neurostatslib.base_class import Base, BaseRegressor from neurostatslib.utils import convert_to_jnp_ndarray, has_invalid_entry @@ -21,10 +21,11 @@ class MockBaseRegressor(BaseRegressor): Mock implementation of the BaseRegressor abstract class for testing purposes. Implements all required abstract methods as empty methods. """ - def __init__(self, std_param: int = 0, **kwargs): + + def __init__(self, std_param: int = 0): """Initialize a MockBaseRegressor instance with optional standard parameters.""" self.std_param = std_param - super().__init__(**kwargs) + super().__init__() def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): pass @@ -41,28 +42,36 @@ def score( pass def simulate( - self, - random_key: jax.random.PRNGKeyArray, - feed_forward_input: Union[NDArray, jnp.ndarray], - **kwargs, + self, + random_key: jax.random.PRNGKeyArray, + feed_forward_input: Union[NDArray, jnp.ndarray], + **kwargs, ): pass -class MockBaseRegressor_Invalid(BaseRegressor): +class MockRegressorNested(MockBaseRegressor): + def __init__(self, other_param: int, std_param: int = 0): + super().__init__(std_param=std_param) + self.other_param = MockBaseRegressor(std_param=other_param) + + +class MockBaseRegressorInvalid(BaseRegressor): """ Mock model that intentionally doesn't implement all the required abstract methods. Used for testing the instantiation of incomplete concrete classes. """ - def __init__(self, std_param: int = 0, **kwargs): + def __init__(self, std_param: int = 0): self.std_param = std_param - super().__init__(**kwargs) + super().__init__() def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: pass - def score(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: + def score( + self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray] + ) -> jnp.ndarray: pass @@ -74,34 +83,31 @@ def __init__(self, param1, *args): def test_init(): """Test the initialization of the MockBaseRegressor class.""" - model = MockBaseRegressor(param1="test", param2=2) - assert model.param1 == "test" - assert model.param2 == 2 - assert model.std_param == 0 + model = MockBaseRegressor(std_param=2) + assert model.std_param == 2 def test_get_params(): """Test the get_params method.""" - model = MockBaseRegressor(param1="test", param2=2) + model = MockRegressorNested(other_param=1, std_param=2) params = model.get_params(deep=True) - assert params["param1"] == "test" - assert params["param2"] == 2 - assert params["std_param"] == 0 + assert params["std_param"] == 2 + assert params["other_param__std_param"] == 1 def set_params(): """Test the set_params method.""" - model = MockBaseRegressor(param1="init_param") - model.set_params(param1="changed") + model = MockBaseRegressor() model.set_params(std_param=1) - assert model.param1 == "changed" assert model.std_param == 1 def test_invalid_set_params(): """Test invalid parameter setting using the set_params method.""" model = MockBaseRegressor() - with pytest.raises(ValueError, match="Invalid parameter 'invalid_param' for estimator"): + with pytest.raises( + ValueError, match="Invalid parameter 'invalid_param' for estimator" + ): model.set_params(invalid_param="invalid") @@ -115,7 +121,7 @@ def test_get_param_names(): def test_convert_to_jnp_ndarray(): """Test data conversion to JAX NumPy arrays.""" data = [1, 2, 3] - jnp_data, = convert_to_jnp_ndarray(data) + (jnp_data,) = convert_to_jnp_ndarray(data) assert isinstance(jnp_data, jnp.ndarray) assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) @@ -138,7 +144,7 @@ def test_abstract_class(): def test_invalid_concrete_class(): """Ensure that classes missing implementation of required abstract methods raise errors.""" with pytest.raises(TypeError, match="Can't instantiate abstract"): - model = MockBaseRegressor_Invalid() + model = MockBaseRegressorInvalid() def test_preprocess_fit(mock_data, mock_regressor): @@ -188,6 +194,7 @@ def test_preprocess_fit_with_inf_in_X(mock_regressor): with pytest.raises(ValueError, match="Input X contains a NaNs or Infs"): mock_regressor.preprocess_fit(X, y) + def test_preprocess_fit_with_nan_in_y(mock_regressor): """Test behavior with NaN values in data.""" X = jnp.array([[[1, 2], [2, 4]]]) @@ -249,7 +256,10 @@ def test_preprocess_simulate_invalid_datatypes(mock_regressor): """Test behavior with invalid feedforward_input datatype.""" feedforward_input = "invalid_data_type" params_f = (jnp.array([[]]),) - with pytest.raises(TypeError, match="Value 'invalid_data_type' with dtype .+ is not a valid JAX array type."): + with pytest.raises( + TypeError, + match="Value 'invalid_data_type' with dtype .+ is not a valid JAX array type.", + ): mock_regressor.preprocess_simulate(feedforward_input, params_f) @@ -284,18 +294,21 @@ def test_preprocess_simulate_invalid_init_y(mock_regressor): init_y = jnp.array([[[1]]]) params_r = (jnp.array([[1]]),) with pytest.raises(ValueError, match="y must be two-dimensional"): - mock_regressor.preprocess_simulate(feedforward_input, params_f, init_y, params_r) + mock_regressor.preprocess_simulate( + feedforward_input, params_f, init_y, params_r + ) + def test_preprocess_simulate_feedforward(mock_regressor): """Test that the preprocessing works.""" feedforward_input = jnp.array([[[1]]]) params_f = (jnp.array([[1]]), jnp.array([1])) - ff, = mock_regressor.preprocess_simulate(feedforward_input, params_f) - assert(jnp.all(ff == feedforward_input)) + (ff,) = mock_regressor.preprocess_simulate(feedforward_input, params_f) + assert jnp.all(ff == feedforward_input) + def test_empty_set(mock_regressor): - """Check that an empty set_params returns self. - """ + """Check that an empty set_params returns self.""" assert mock_regressor.set_params() is mock_regressor @@ -313,15 +326,19 @@ def test_target_device_put(device_name: Literal["cpu", "gpu", "tpu"], mock_regre with pytest.raises(RuntimeError, match=f"Unknown backend"): mock_regressor.device_put(x, device=device_name) else: - with pytest.raises(RuntimeError, match=f"Backend '{device_name}' failed to initialize: "): + with pytest.raises( + RuntimeError, match=f"Backend '{device_name}' failed to initialize: " + ): mock_regressor.device_put(x, device=device_name) else: - x, = mock_regressor.device_put(x, device=device_name) + (x,) = mock_regressor.device_put(x, device=device_name) assert x.device().device_kind == device_name def test_glm_varargs_error(): """Test that variable number of argument in __init__ is not allowed.""" bad_estimator = BadEstimator(1) - with pytest.raises(RuntimeError, match="GLM estimators should always specify their parameters"): + with pytest.raises( + RuntimeError, match="GLM estimators should always specify their parameters" + ): bad_estimator._get_param_names() From 914ce1b797e60169011f8c6ad531ac1fc44f9737 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:26:15 -0400 Subject: [PATCH 125/250] fixed typo --- src/neurostatslib/base_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 7d340965..2b7b147d 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -312,7 +312,7 @@ def _check_input_and_params_consistency( Raises ------ ValueError - - if the number of neurons is consistent across the model parameters (`params`) and + - if the number of neurons is inconsistent across the model parameters (`params`) and any additional inputs (`X` or `y` when provided). - if the number of features is inconsistent between params[1] and X (when provided). From 23310cf0cdbfc5346139ae36ac02c9482f328424 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:40:07 -0400 Subject: [PATCH 126/250] create a check array function --- src/neurostatslib/base_class.py | 11 ++++------- src/neurostatslib/utils.py | 13 ++++++++----- tests/test_base_class.py | 26 +++++++++++++++----------- 3 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 2b7b147d..db69240d 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -10,7 +10,7 @@ import jax.numpy as jnp from numpy.typing import ArrayLike, NDArray -from .utils import convert_to_jnp_ndarray, has_invalid_entry, is_list_like +from .utils import convert_to_jnp_ndarray, check_invalid_entry, is_list_like class Base: @@ -408,10 +408,8 @@ def preprocess_fit( self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) - if has_invalid_entry(X): - raise ValueError("Input X contains a NaNs or Infs!") - if has_invalid_entry(y): - raise ValueError("Input y contains a NaNs or Infs!") + check_invalid_entry(X) + check_invalid_entry(y) _, n_neurons = y.shape n_features = X.shape[2] @@ -475,8 +473,7 @@ def preprocess_simulate( self._check_input_dimensionality(X=feedforward_input) self._check_input_and_params_consistency(params_f, X=feedforward_input) - if has_invalid_entry(feedforward_input): - raise ValueError("feedforward_input contains a NaNs or Infs!") + check_invalid_entry(feedforward_input) # Ensure that both or neither of `init_y` and `params_r` are provided if (init_y is None) != (params_r is None): diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index bfdb5de7..d42ad563 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -438,7 +438,7 @@ def convert_to_jnp_ndarray( return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) -def has_invalid_entry(array: jnp.ndarray) -> jnp.ndarray: +def check_invalid_entry(array: jnp.ndarray) -> None: """Check if the array has nans or infs. Parameters @@ -446,9 +446,12 @@ def has_invalid_entry(array: jnp.ndarray) -> jnp.ndarray: array: The array to be checked. - Returns - ------- - True if a nan or an inf is present, False otherwise + Raises + ------ + - ValueError: If any entry of `array` is either NaN or inf. """ - return jnp.any(jnp.isinf(array) | jnp.isnan(array)) + if jnp.any(jnp.isinf(array)): + raise ValueError("Input array contains Infs!") + elif jnp.any(jnp.isnan(array)): + raise ValueError("Input array contains NaNs!") diff --git a/tests/test_base_class.py b/tests/test_base_class.py index e239f98a..024ddb44 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -7,7 +7,7 @@ import neurostatslib as nsl from neurostatslib.base_class import Base, BaseRegressor -from neurostatslib.utils import convert_to_jnp_ndarray, has_invalid_entry +from neurostatslib.utils import check_invalid_entry, convert_to_jnp_ndarray @pytest.fixture @@ -126,12 +126,16 @@ def test_convert_to_jnp_ndarray(): assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) -def test_has_invalid_entry(): +def test_check_invalid_entry(): """Test validation of data arrays.""" valid_data = jnp.array([1, 2, 3]) - invalid_data = jnp.array([1, 2, jnp.nan]) - assert not has_invalid_entry(valid_data) - assert has_invalid_entry(invalid_data) + invalid_data_nan = jnp.array([1, 2, jnp.nan]) + invalid_data_inf = jnp.array([1, jnp.inf, 2]) + check_invalid_entry(valid_data) + with pytest.raises(ValueError, match="Input array contains NaN"): + check_invalid_entry(invalid_data_nan) + with pytest.raises(ValueError, match="Input array contains Inf"): + check_invalid_entry(invalid_data_inf) # To ensure abstract methods aren't callable @@ -183,7 +187,7 @@ def test_preprocess_fit_with_nan_in_X(mock_regressor): """Test behavior with NaN values in data.""" X = jnp.array([[[1, 2], [jnp.nan, 4]]]) y = jnp.array([[1, 2]]) - with pytest.raises(ValueError, match="Input X contains a NaNs or Infs"): + with pytest.raises(ValueError, match="Input array contains"): mock_regressor.preprocess_fit(X, y) @@ -191,7 +195,7 @@ def test_preprocess_fit_with_inf_in_X(mock_regressor): """Test behavior with inf values in data.""" X = jnp.array([[[1, 2], [jnp.inf, 4]]]) y = jnp.array([[1, 2]]) - with pytest.raises(ValueError, match="Input X contains a NaNs or Infs"): + with pytest.raises(ValueError, match="Input array contains"): mock_regressor.preprocess_fit(X, y) @@ -199,7 +203,7 @@ def test_preprocess_fit_with_nan_in_y(mock_regressor): """Test behavior with NaN values in data.""" X = jnp.array([[[1, 2], [2, 4]]]) y = jnp.array([[1, jnp.nan]]) - with pytest.raises(ValueError, match="Input y contains a NaNs or Infs"): + with pytest.raises(ValueError, match="Input array contains"): mock_regressor.preprocess_fit(X, y) @@ -207,7 +211,7 @@ def test_preprocess_fit_with_inf_in_y(mock_regressor): """Test behavior with inf values in data.""" X = jnp.array([[[1, 2], [2, 4]]]) y = jnp.array([[1, jnp.inf]]) - with pytest.raises(ValueError, match="Input y contains a NaNs or Infs"): + with pytest.raises(ValueError, match="Input array contains"): mock_regressor.preprocess_fit(X, y) @@ -267,7 +271,7 @@ def test_preprocess_simulate_with_nan(mock_regressor): """Test behavior with NaN values in feedforward_input.""" feedforward_input = jnp.array([[[jnp.nan]]]) params_f = (jnp.array([[1]]), jnp.array([1])) - with pytest.raises(ValueError, match="feedforward_input contains a NaNs or Infs!"): + with pytest.raises(ValueError, match="Input array contains"): mock_regressor.preprocess_simulate(feedforward_input, params_f) @@ -275,7 +279,7 @@ def test_preprocess_simulate_with_inf(mock_regressor): """Test behavior with infinite values in feedforward_input.""" feedforward_input = jnp.array([[[jnp.inf]]]) params_f = (jnp.array([[1]]), jnp.array([1])) - with pytest.raises(ValueError, match="feedforward_input contains a NaNs or Infs!"): + with pytest.raises(ValueError, match="Input array contains"): mock_regressor.preprocess_simulate(feedforward_input, params_f) From 94d5523af075b4d22b22eed1a6a7523c35de04ee Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:42:55 -0400 Subject: [PATCH 127/250] preprocess func private --- src/neurostatslib/base_class.py | 4 ++-- src/neurostatslib/glm.py | 6 +++--- tests/test_base_class.py | 38 ++++++++++++++++----------------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index db69240d..69f4a649 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -359,7 +359,7 @@ def _check_input_n_timepoints(X: jnp.ndarray, y: jnp.ndarray): f"y has {y.shape[0]} instead!" ) - def preprocess_fit( + def _preprocess_fit( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], @@ -431,7 +431,7 @@ def preprocess_fit( return X, y, init_params - def preprocess_simulate( + def _preprocess_simulate( self, feedforward_input: Union[NDArray, jnp.ndarray], params_f: Tuple[jnp.ndarray, jnp.ndarray], diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 56eae04d..eb8418d6 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -321,7 +321,7 @@ def fit( - If `init_params[i]` cannot be converted to jnp.ndarray for all i """ # convert to jnp.ndarray & perform checks - X, y, init_params = self.preprocess_fit(X, y, init_params) + X, y, init_params = self._preprocess_fit(X, y, init_params) # Run optimization runner = self.solver.instantiate_solver(self._score) @@ -387,7 +387,7 @@ def simulate( # check if the model is fit self._check_is_fit() Ws, bs = self.basis_coeff_, self.baseline_link_fr_ - (feedforward_input,) = self.preprocess_simulate( + (feedforward_input,) = self._preprocess_simulate( feedforward_input, params_f=(Ws, bs) ) predicted_rate = self._predict((Ws, bs), feedforward_input) @@ -529,7 +529,7 @@ def simulate_recurrent( Wr = self.basis_coeff_[:, : n_basis_coupling * n_neurons] bs = self.baseline_link_fr_ - feedforward_input, init_y = self.preprocess_simulate( + feedforward_input, init_y = self._preprocess_simulate( feedforward_input, params_f=(Wf, bs), init_y=init_y, params_r=(Wr, bs) ) diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 024ddb44..1a983500 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -153,7 +153,7 @@ def test_invalid_concrete_class(): def test_preprocess_fit(mock_data, mock_regressor): X, y = mock_data - X_out, y_out, params_out = mock_regressor.preprocess_fit(X, y) + X_out, y_out, params_out = mock_regressor._preprocess_fit(X, y) assert X_out.shape == X.shape assert y_out.shape == y.shape assert params_out[0].shape == (2, 2) # Mock data shapes @@ -164,7 +164,7 @@ def test_preprocess_fit_empty_data(mock_regressor): """Test behavior with empty data input.""" X, y = jnp.array([[]]), jnp.array([]) with pytest.raises(ValueError): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_mismatched_shapes(mock_regressor): @@ -172,7 +172,7 @@ def test_preprocess_fit_mismatched_shapes(mock_regressor): X = jnp.array([[1, 2], [3, 4]]) y = jnp.array([1, 2, 3]) with pytest.raises(ValueError): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_invalid_datatypes(mock_regressor): @@ -180,7 +180,7 @@ def test_preprocess_fit_invalid_datatypes(mock_regressor): X = "invalid_data_type" y = "invalid_data_type" with pytest.raises(TypeError): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_with_nan_in_X(mock_regressor): @@ -188,7 +188,7 @@ def test_preprocess_fit_with_nan_in_X(mock_regressor): X = jnp.array([[[1, 2], [jnp.nan, 4]]]) y = jnp.array([[1, 2]]) with pytest.raises(ValueError, match="Input array contains"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_with_inf_in_X(mock_regressor): @@ -196,7 +196,7 @@ def test_preprocess_fit_with_inf_in_X(mock_regressor): X = jnp.array([[[1, 2], [jnp.inf, 4]]]) y = jnp.array([[1, 2]]) with pytest.raises(ValueError, match="Input array contains"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_with_nan_in_y(mock_regressor): @@ -204,7 +204,7 @@ def test_preprocess_fit_with_nan_in_y(mock_regressor): X = jnp.array([[[1, 2], [2, 4]]]) y = jnp.array([[1, jnp.nan]]) with pytest.raises(ValueError, match="Input array contains"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_with_inf_in_y(mock_regressor): @@ -212,7 +212,7 @@ def test_preprocess_fit_with_inf_in_y(mock_regressor): X = jnp.array([[[1, 2], [2, 4]]]) y = jnp.array([[1, jnp.inf]]) with pytest.raises(ValueError, match="Input array contains"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_higher_dimensional_data_X(mock_regressor): @@ -220,7 +220,7 @@ def test_preprocess_fit_higher_dimensional_data_X(mock_regressor): X = jnp.array([[[[1, 2], [3, 4]]]]) y = jnp.array([[1, 2]]) with pytest.raises(ValueError, match="X must be three-dimensional"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_higher_dimensional_data_y(mock_regressor): @@ -228,7 +228,7 @@ def test_preprocess_fit_higher_dimensional_data_y(mock_regressor): X = jnp.array([[[[1, 2], [3, 4]]]]) y = jnp.array([[[1, 2]]]) with pytest.raises(ValueError, match="y must be two-dimensional"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_lower_dimensional_data_X(mock_regressor): @@ -236,7 +236,7 @@ def test_preprocess_fit_lower_dimensional_data_X(mock_regressor): X = jnp.array([[1, 2], [3, 4]]) y = jnp.array([[1, 2]]) with pytest.raises(ValueError, match="X must be three-dimensional"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) def test_preprocess_fit_lower_dimensional_data_y(mock_regressor): @@ -244,7 +244,7 @@ def test_preprocess_fit_lower_dimensional_data_y(mock_regressor): X = jnp.array([[[[1, 2], [3, 4]]]]) y = jnp.array([1, 2]) with pytest.raises(ValueError, match="y must be two-dimensional"): - mock_regressor.preprocess_fit(X, y) + mock_regressor._preprocess_fit(X, y) # Preprocess Simulate Tests @@ -253,7 +253,7 @@ def test_preprocess_simulate_empty_data(mock_regressor): feedforward_input = jnp.array([[[]]]) params_f = (jnp.array([[]]), jnp.array([])) with pytest.raises(ValueError, match="Model parameters have inconsistent shapes."): - mock_regressor.preprocess_simulate(feedforward_input, params_f) + mock_regressor._preprocess_simulate(feedforward_input, params_f) def test_preprocess_simulate_invalid_datatypes(mock_regressor): @@ -264,7 +264,7 @@ def test_preprocess_simulate_invalid_datatypes(mock_regressor): TypeError, match="Value 'invalid_data_type' with dtype .+ is not a valid JAX array type.", ): - mock_regressor.preprocess_simulate(feedforward_input, params_f) + mock_regressor._preprocess_simulate(feedforward_input, params_f) def test_preprocess_simulate_with_nan(mock_regressor): @@ -272,7 +272,7 @@ def test_preprocess_simulate_with_nan(mock_regressor): feedforward_input = jnp.array([[[jnp.nan]]]) params_f = (jnp.array([[1]]), jnp.array([1])) with pytest.raises(ValueError, match="Input array contains"): - mock_regressor.preprocess_simulate(feedforward_input, params_f) + mock_regressor._preprocess_simulate(feedforward_input, params_f) def test_preprocess_simulate_with_inf(mock_regressor): @@ -280,7 +280,7 @@ def test_preprocess_simulate_with_inf(mock_regressor): feedforward_input = jnp.array([[[jnp.inf]]]) params_f = (jnp.array([[1]]), jnp.array([1])) with pytest.raises(ValueError, match="Input array contains"): - mock_regressor.preprocess_simulate(feedforward_input, params_f) + mock_regressor._preprocess_simulate(feedforward_input, params_f) def test_preprocess_simulate_higher_dimensional_data(mock_regressor): @@ -288,7 +288,7 @@ def test_preprocess_simulate_higher_dimensional_data(mock_regressor): feedforward_input = jnp.array([[[[1]]]]) params_f = (jnp.array([[1]]), jnp.array([1])) with pytest.raises(ValueError, match="X must be three-dimensional"): - mock_regressor.preprocess_simulate(feedforward_input, params_f) + mock_regressor._preprocess_simulate(feedforward_input, params_f) def test_preprocess_simulate_invalid_init_y(mock_regressor): @@ -298,7 +298,7 @@ def test_preprocess_simulate_invalid_init_y(mock_regressor): init_y = jnp.array([[[1]]]) params_r = (jnp.array([[1]]),) with pytest.raises(ValueError, match="y must be two-dimensional"): - mock_regressor.preprocess_simulate( + mock_regressor._preprocess_simulate( feedforward_input, params_f, init_y, params_r ) @@ -307,7 +307,7 @@ def test_preprocess_simulate_feedforward(mock_regressor): """Test that the preprocessing works.""" feedforward_input = jnp.array([[[1]]]) params_f = (jnp.array([[1]]), jnp.array([1])) - (ff,) = mock_regressor.preprocess_simulate(feedforward_input, params_f) + (ff,) = mock_regressor._preprocess_simulate(feedforward_input, params_f) assert jnp.all(ff == feedforward_input) From b50017ebf8a83b4b8ce93046fcf4071391a0fc26 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:44:07 -0400 Subject: [PATCH 128/250] fixed tests --- tests/test_glm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index b9f50c83..b53b52e2 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -91,11 +91,11 @@ def test_fit_param_length(self, n_params, error, match_str, poissonGLM_model_ins @pytest.mark.parametrize("add_entry, add_to, error, match_str", [ (0, "X", None, None), - (np.nan, "X", ValueError, "Input X contains a NaNs or Infs"), - (np.inf, "X", ValueError, "Input X contains a NaNs or Infs"), + (np.nan, "X", ValueError, "Input array contains"), + (np.inf, "X", ValueError, "Input array contains"), (0, "y", None, None), - (np.nan, "y", ValueError, "Input y contains a NaNs or Infs"), - (np.inf, "y", ValueError, "Input y contains a NaNs or Infs"), + (np.nan, "y", ValueError, "Input array contains"), + (np.inf, "y", ValueError, "Input array contains"), ]) def test_fit_param_values(self, add_entry, add_to, error, match_str, poissonGLM_model_instantiation): """ From 759a23de90cce24c52b33c218a285411e5cb9651 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:47:33 -0400 Subject: [PATCH 129/250] renamed paramters in simulate preproc --- src/neurostatslib/base_class.py | 16 ++++++++-------- src/neurostatslib/glm.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 69f4a649..7fd2a388 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -434,9 +434,9 @@ def _preprocess_fit( def _preprocess_simulate( self, feedforward_input: Union[NDArray, jnp.ndarray], - params_f: Tuple[jnp.ndarray, jnp.ndarray], + params_feedforward: Tuple[jnp.ndarray, jnp.ndarray], init_y: Optional[Union[NDArray, jnp.ndarray]] = None, - params_r: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, + params_recurrent: Optional[Tuple[jnp.ndarray, jnp.ndarray]] = None, ) -> Tuple[jnp.ndarray, ...]: """ Preprocess the input data and parameters for simulation. @@ -449,12 +449,12 @@ def _preprocess_simulate( ---------- feedforward_input : Input data for the feedforward process. Expected shape: (n_timesteps, n_neurons, n_basis_input). - params_f : + params_feedforward : Parameters corresponding to the feedforward input. Expected shape: (n_neurons, n_basis_input). init_y : Initial values for the feedback process. If provided, its dimensionality and consistency with params_r will be checked. Expected shape if provided: (window_size, n_neurons). - params_r : + params_recurrent : Parameters corresponding to the feedback input (init_y). Required if init_y is provided. Expected shape if provided: (window_size, n_basis_coupling) @@ -471,22 +471,22 @@ def _preprocess_simulate( """ (feedforward_input,) = convert_to_jnp_ndarray(feedforward_input) self._check_input_dimensionality(X=feedforward_input) - self._check_input_and_params_consistency(params_f, X=feedforward_input) + self._check_input_and_params_consistency(params_feedforward, X=feedforward_input) check_invalid_entry(feedforward_input) # Ensure that both or neither of `init_y` and `params_r` are provided - if (init_y is None) != (params_r is None): + if (init_y is None) != (params_recurrent is None): raise ValueError( "Both `init_y` and `params_r` should be provided, or neither should be provided." ) # If both are provided, perform checks and conversions - elif init_y is not None and params_r is not None: + elif init_y is not None and params_recurrent is not None: init_y = convert_to_jnp_ndarray(init_y)[ 0 ] # Assume this method returns a tuple self._check_input_dimensionality(y=init_y) - self._check_input_and_params_consistency(params_r, y=init_y) + self._check_input_and_params_consistency(params_recurrent, y=init_y) return feedforward_input, init_y return (feedforward_input,) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index eb8418d6..9dbe2952 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -388,7 +388,7 @@ def simulate( self._check_is_fit() Ws, bs = self.basis_coeff_, self.baseline_link_fr_ (feedforward_input,) = self._preprocess_simulate( - feedforward_input, params_f=(Ws, bs) + feedforward_input, params_feedforward=(Ws, bs) ) predicted_rate = self._predict((Ws, bs), feedforward_input) return ( @@ -530,7 +530,7 @@ def simulate_recurrent( bs = self.baseline_link_fr_ feedforward_input, init_y = self._preprocess_simulate( - feedforward_input, params_f=(Wf, bs), init_y=init_y, params_r=(Wr, bs) + feedforward_input, params_feedforward=(Wf, bs), init_y=init_y, params_recurrent=(Wr, bs) ) self._check_input_and_params_consistency( From 9fef6c8e2f21254b3f8e61218e343e64dd81c507 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 15:48:39 -0400 Subject: [PATCH 130/250] linted --- src/neurostatslib/base_class.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index 7fd2a388..ecf7ccf4 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -482,9 +482,7 @@ def _preprocess_simulate( ) # If both are provided, perform checks and conversions elif init_y is not None and params_recurrent is not None: - init_y = convert_to_jnp_ndarray(init_y)[ - 0 - ] # Assume this method returns a tuple + init_y = convert_to_jnp_ndarray(init_y)[0] self._check_input_dimensionality(y=init_y) self._check_input_and_params_consistency(params_recurrent, y=init_y) return feedforward_input, init_y From f10e4e92137ce3a323ab04eb0797c8f1330ee7ba Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 16:25:02 -0400 Subject: [PATCH 131/250] linted --- src/neurostatslib/base_class.py | 5 ++- src/neurostatslib/glm.py | 5 ++- src/neurostatslib/noise_model.py | 52 +++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 10 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index ecf7ccf4..afeb6b7a 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -33,6 +33,7 @@ class Base: The class provides helper methods mimicking scikit-learn's get_params and set_params. Additionally, it has methods for selecting target devices and sending arrays to them. """ + def get_params(self, deep=True): """ From scikit-learn, get parameters by inspecting init. @@ -471,7 +472,9 @@ def _preprocess_simulate( """ (feedforward_input,) = convert_to_jnp_ndarray(feedforward_input) self._check_input_dimensionality(X=feedforward_input) - self._check_input_and_params_consistency(params_feedforward, X=feedforward_input) + self._check_input_and_params_consistency( + params_feedforward, X=feedforward_input + ) check_invalid_entry(feedforward_input) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 9dbe2952..6b87e8c8 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -530,7 +530,10 @@ def simulate_recurrent( bs = self.baseline_link_fr_ feedforward_input, init_y = self._preprocess_simulate( - feedforward_input, params_feedforward=(Wf, bs), init_y=init_y, params_recurrent=(Wr, bs) + feedforward_input, + params_feedforward=(Wf, bs), + init_y=init_y, + params_recurrent=(Wr, bs), ) self._check_input_and_params_consistency( diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 8be1be24..831e4c10 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -39,7 +39,7 @@ class NoiseModel(Base, abc.ABC): noise model using the Poisson distribution. """ - FLOAT_EPS = jnp.finfo(jnp.float32).eps + FLOAT_EPS = jnp.finfo(float).eps def __init__(self, inverse_link_function: Callable, **kwargs): super().__init__(**kwargs) @@ -71,17 +71,53 @@ def scale(self, value: Union[int, float]): self._scale = value @staticmethod - def _check_inverse_link_function(inverse_link_function): + def _check_inverse_link_function(inverse_link_function: Callable): + """ + Check if the provided inverse_link_function is usable. + + This function verifies if the inverse link function: + 1. Is callable + 2. Returns a jax.numpy.ndarray + 3. Is differentiable (via jax) + + Parameters + ---------- + inverse_link_function : + The function to be checked. + + Raises + ------ + TypeError + If the function is not callable, does not return a jax.numpy.ndarray, + or is not differentiable. + """ + + # check that it's callable if not callable(inverse_link_function): raise TypeError("The `inverse_link_function` function must be a Callable!") - # check that the callable is in the jax namespace - if not hasattr(inverse_link_function, "__module__"): + + # check if the function returns a jax array for a 1D array + array_out = inverse_link_function(jnp.array([1, 2, 3])) + if not isinstance(array_out, jnp.ndarray): + raise TypeError( + "The `inverse_link_function` must return a jax.numpy.ndarray!" + ) + + # Optionally: Check for scalar input + scalar_out = inverse_link_function(1.0) + if not isinstance(scalar_out, (jnp.ndarray, float, int)): raise TypeError( - "The `inverse_link_function` must be from the `jax` namespace!" + "The `inverse_link_function` must handle scalar inputs correctly and return a scalar or a " + "jax.numpy.ndarray!" ) - elif not getattr(inverse_link_function, "__module__").startswith("jax"): + + # check for autodiff + try: + gradient_fn = jax.grad(inverse_link_function) + gradient_fn(jnp.array([1, 2, 3])) + except Exception as e: raise TypeError( - "The `inverse_link_function` must be from the `jax` namespace!" + f"The `inverse_link_function` function cannot be differentiated. Error: {e}" ) @abc.abstractmethod @@ -101,7 +137,7 @@ def negative_log_likelihood(self, predicted_rate, y): Returns ------- : - The Poisson negative log-likehood. Shape (1,). + The negative log-likehood. Shape (1,). """ pass From 315206560776fbaf55922d1d77a3295d2620a70a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 16:50:19 -0400 Subject: [PATCH 132/250] added test for noise model link --- src/neurostatslib/noise_model.py | 4 ++-- tests/test_noise_model.py | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 831e4c10..7744fb17 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -97,7 +97,7 @@ def _check_inverse_link_function(inverse_link_function: Callable): raise TypeError("The `inverse_link_function` function must be a Callable!") # check if the function returns a jax array for a 1D array - array_out = inverse_link_function(jnp.array([1, 2, 3])) + array_out = inverse_link_function(jnp.array([1., 2., 3.])) if not isinstance(array_out, jnp.ndarray): raise TypeError( "The `inverse_link_function` must return a jax.numpy.ndarray!" @@ -114,7 +114,7 @@ def _check_inverse_link_function(inverse_link_function: Callable): # check for autodiff try: gradient_fn = jax.grad(inverse_link_function) - gradient_fn(jnp.array([1, 2, 3])) + gradient_fn(1.0) except Exception as e: raise TypeError( f"The `inverse_link_function` function cannot be differentiated. Error: {e}" diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index 458c3952..bc464e19 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -21,13 +21,12 @@ def test_initialization_link_is_callable(self, link_function): else: self.cls(link_function) - @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x:x, sm.families.links.log]) + @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x:x, sm.families.links.log()]) def test_initialization_link_is_jax(self, link_function): """Check that the noise model initializes when a callable is passed.""" - raise_exception = (not hasattr(link_function, "__module__")) or \ - (not getattr(link_function, "__module__").startswith("jax")) + raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` must be from the `jax` namespace"): + with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray"): self.cls(link_function) else: self.cls(link_function) @@ -43,14 +42,27 @@ def test_initialization_link_is_callable_set_params(self, link_function): else: noise_model.set_params(inverse_link_function=link_function) - @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log]) + @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log()]) def test_initialization_link_is_jax_set_params(self, link_function): """Check that the noise model initializes when a callable is passed.""" - raise_exception = (not hasattr(link_function, "__module__")) or \ - (not getattr(link_function, "__module__").startswith("jax")) + raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) noise_model = self.cls() if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` must be from the `jax` namespace"): + with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray!"): + noise_model.set_params(inverse_link_function=link_function) + else: + noise_model.set_params(inverse_link_function=link_function) + + @pytest.mark.parametrize("link_function", [ + jnp.exp, + lambda x: jnp.exp(x) if isinstance(x, jnp.ndarray) else "not a number" + ]) + def test_initialization_link_returns_scalar(self, link_function): + """Check that the noise model initializes when a callable is passed.""" + raise_exception = not isinstance(link_function(1.), (jnp.ndarray, float)) + noise_model = self.cls() + if raise_exception: + with pytest.raises(TypeError, match="The `inverse_link_function` must handle scalar inputs correctly"): noise_model.set_params(inverse_link_function=link_function) else: noise_model.set_params(inverse_link_function=link_function) From 9b55807be3db846ae89c31af42fe14fb35723a33 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Tue, 31 Oct 2023 18:16:08 -0400 Subject: [PATCH 133/250] Update src/neurostatslib/noise_model.py Co-authored-by: William F. Broderick --- src/neurostatslib/noise_model.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 8be1be24..7310d468 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -124,9 +124,8 @@ def emission_probability( Returns ------- - jnp.ndarray - Random numbers generated from the desired distribution based on the `predicted_rate` scale parameter - if needed. + : + Random numbers generated from the noise model with `predicted_rate`. """ pass From bbe8213904f32e24ce44131ae6e633c695e72cf1 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Tue, 31 Oct 2023 18:18:16 -0400 Subject: [PATCH 134/250] Update src/neurostatslib/noise_model.py Co-authored-by: William F. Broderick --- src/neurostatslib/noise_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 7310d468..6401b54a 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -131,7 +131,7 @@ def emission_probability( @abc.abstractmethod def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray): - r"""Compute the residual deviance for a Poisson model. + r"""Compute the residual deviance for the noise model. Parameters ---------- From 0f7d67bedde4ffa42c237c49a835f9ae7d87cd99 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 18:21:16 -0400 Subject: [PATCH 135/250] linted --- src/neurostatslib/base_class.py | 2 +- src/neurostatslib/noise_model.py | 49 ++++++++++++++++++++++++++------ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/src/neurostatslib/base_class.py b/src/neurostatslib/base_class.py index afeb6b7a..7182276a 100644 --- a/src/neurostatslib/base_class.py +++ b/src/neurostatslib/base_class.py @@ -10,7 +10,7 @@ import jax.numpy as jnp from numpy.typing import ArrayLike, NDArray -from .utils import convert_to_jnp_ndarray, check_invalid_entry, is_list_like +from .utils import check_invalid_entry, convert_to_jnp_ndarray, is_list_like class Base: diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 7744fb17..0c184aff 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -43,9 +43,8 @@ class NoiseModel(Base, abc.ABC): def __init__(self, inverse_link_function: Callable, **kwargs): super().__init__(**kwargs) - self._check_inverse_link_function(inverse_link_function) - self._inverse_link_function = inverse_link_function - self._scale = 1.0 + self.inverse_link_function = inverse_link_function + self.scale = 1.0 @property def inverse_link_function(self): @@ -97,7 +96,7 @@ def _check_inverse_link_function(inverse_link_function: Callable): raise TypeError("The `inverse_link_function` function must be a Callable!") # check if the function returns a jax array for a 1D array - array_out = inverse_link_function(jnp.array([1., 2., 3.])) + array_out = inverse_link_function(jnp.array([1.0, 2.0, 3.0])) if not isinstance(array_out, jnp.ndarray): raise TypeError( "The `inverse_link_function` must return a jax.numpy.ndarray!" @@ -185,8 +184,26 @@ def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarr pass @abc.abstractmethod - def estimate_scale(self, predicted_rate: jnp.ndarray) -> float: - """Estimate the scale parameter for the model.""" + def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: + r"""Estimate the scale parameter for the model. + + This method estimates the scale parameter, often denoted as $\phi$, which determines the dispersion + of an exponential family distribution. The probability density function (pdf) for such a distribution + is generally expressed as + $f(x; \theta, \phi) \propto \exp \left(a(\phi)\left( y\theta - \mathcal{k}(\theta) \right)\right)$. + + The relationship between variance and the scale parameter is given by: + $$ + \text{var}(Y) = \frac{V(\mu)}{a(\phi)}. + $$ + + The scale parameter, $\phi$, is necessary for capturing the variance of the data accurately. + + Parameters + ---------- + predicted_rate : + The predicted rate values. + """ pass def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): @@ -351,6 +368,22 @@ def residual_deviance( ) return resid_dev - def estimate_scale(self, predicted_rate: jnp.ndarray): - """Assign 1 to the scale parameter of the Poisson model.""" + def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: + r""" + Assign 1 to the scale parameter of the Poisson model. + + For the Poisson exponential family distribution, the scale parameter $\phi$ is always 1. + This property is consistent with the fact that the variance equals the mean in a Poisson distribution. + As given in the general exponential family expression: + $$ + \text{var}(Y) = \frac{V(\mu)}{a(\phi)}, + $$ + for the Poisson family, it simplifies to $\text{var}(Y) = \mu$ since $a(\phi) = 1$ and $V(\mu) = \mu$. + + Parameters + ---------- + predicted_rate : + The predicted rate values. This is not used in the Poisson model for estimating scale, + but is retained for compatibility with the abstract method signature. + """ self.scale = 1.0 From 6cc48222d4d65234a257f557c988d1c27fae0672 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 31 Oct 2023 18:28:36 -0400 Subject: [PATCH 136/250] renamed emission_probability --- src/neurostatslib/glm.py | 4 ++-- src/neurostatslib/noise_model.py | 8 ++++---- tests/test_noise_model.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 6b87e8c8..abc856c0 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -392,7 +392,7 @@ def simulate( ) predicted_rate = self._predict((Ws, bs), feedforward_input) return ( - self.noise_model.emission_probability( + self.noise_model.sample_generator( key=random_key, predicted_rate=predicted_rate ), predicted_rate, @@ -586,7 +586,7 @@ def scan_fn( ) # Simulate activity based on the predicted firing rate - new_act = self.noise_model.emission_probability(key, firing_rate) + new_act = self.noise_model.sample_generator(key, firing_rate) # Prepare the spikes for the next iteration (keeping the most recent spikes) concat_act = jnp.row_stack((activity[1:], new_act)), chunk + 1 diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 1daf2232..ced5e7ce 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -141,11 +141,11 @@ def negative_log_likelihood(self, predicted_rate, y): pass @abc.abstractmethod - def emission_probability( + def sample_generator( self, key: KeyArray, predicted_rate: jnp.ndarray ) -> jnp.ndarray: """ - Calculate the emission of the noise model. + Sample from the estimated distribution. This method generates random numbers from the desired distribution based on the given `predicted_rate`. @@ -304,11 +304,11 @@ def negative_log_likelihood( # see above for derivation of this. return jnp.mean(predicted_firing_rates - x) - def emission_probability( + def sample_generator( self, key: KeyArray, predicted_rate: jnp.ndarray ) -> jnp.ndarray: """ - Calculate the emission probability using a Poisson distribution. + Sample from the Poisson distribution. This method generates random numbers from a Poisson distribution based on the given `predicted_rate`. diff --git a/tests/test_noise_model.py b/tests/test_noise_model.py index bc464e19..dc91fb7f 100644 --- a/tests/test_noise_model.py +++ b/tests/test_noise_model.py @@ -118,6 +118,6 @@ def test_emission_probability(selfself, poissonGLM_model_instantiation): """ _, _, model, _, _ = poissonGLM_model_instantiation key_array = jax.random.PRNGKey(123) - counts = model.noise_model.emission_probability(key_array, np.arange(1, 11)) + counts = model.noise_model.sample_generator(key_array, np.arange(1, 11)) if not jnp.all(counts == jax.random.poisson(key_array, np.arange(1, 11))): raise ValueError("The emission probability should output the results of a call to jax.random.poisson.") From 2aae9ef585aba83d61f30e05f220d8d249543e2c Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 10:44:45 -0400 Subject: [PATCH 137/250] Update src/neurostatslib/noise_model.py Co-authored-by: William F. Broderick --- src/neurostatslib/noise_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index 1daf2232..2095867a 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -280,7 +280,7 @@ def negative_log_likelihood( \end{aligned} $$ - Because $\Gamma(k+1)=k!$, see [wikipedia](https://en.wikipedia.org/wiki/Gamma_function) for example. + Because $\Gamma(k+1)=k!$, see [wikipedia](https://en.wikipedia.org/wiki/Gamma_function) for explanation. Parameters ---------- From 19456b1b33347477caeccfccceeef7876f3f0c68 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 10:48:14 -0400 Subject: [PATCH 138/250] Update src/neurostatslib/glm.py Co-authored-by: William F. Broderick --- src/neurostatslib/glm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 6b87e8c8..90cdbe17 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -37,9 +37,9 @@ class GLM(BaseRegressor): solver : Solver Solver being used. baseline_link_fr_ : jnp.ndarray or None - Model baseline link firing rate parameters after fitting. + Model baseline link firing rate parameters. basis_coeff_ : jnp.ndarray or None - Basis coefficients for the model after fitting. + Basis coefficients for the model. solver_state : Any State of the solver after fitting. May include details like optimization error. From aa454fbff7ffe66936bacd2fafe495bc50c48767 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 12:00:29 -0400 Subject: [PATCH 139/250] Update src/neurostatslib/glm.py Co-authored-by: William F. Broderick --- src/neurostatslib/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 90cdbe17..bb63a68a 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -162,7 +162,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: self._check_input_and_params_consistency((Ws, bs), X=X) return self._predict((Ws, bs), X) - def _score( # call _negative_log_likelihood + def _score( self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, From f74d3abdd099e1f32946024122b2381a044d01df Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 12:04:10 -0400 Subject: [PATCH 140/250] Update src/neurostatslib/glm.py Co-authored-by: William F. Broderick --- src/neurostatslib/glm.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index bb63a68a..1f110486 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -410,17 +410,12 @@ class GLMRecurrent(GLM): Parameters ---------- - noise_model : nsm.NoiseModel, default=nsm.PoissonNoiseModel() + noise_model : The noise model to use for the GLM. This defines how neural activity is generated based on the underlying firing rate. Common choices include Poisson and Gaussian models. - - solver : slv.Solver, default=slv.RidgeSolver() + solver : The optimization solver to use for fitting the GLM parameters. - data_type : {jnp.float32, jnp.float64}, optional - The numerical data type for internal calculations. If not provided, it will be inferred - from the data during fitting. - See Also -------- [GLM](../glm/#neurostatslib.glm.GLM) : Base class for the generalized linear model. From b29b6d7800f1d12218c788735ae605c431a87830 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 12:06:08 -0400 Subject: [PATCH 141/250] Update src/neurostatslib/solver.py Co-authored-by: William F. Broderick --- src/neurostatslib/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 37a913cd..178c9770 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -382,7 +382,7 @@ def __init__( mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): super().__init__(solver_name, solver_kwargs=solver_kwargs) - self._mask = mask + self.mask = mask self.regularizer_strength = regularizer_strength @property From dbdca8698d16bfe83aa4f88215a40b463eb7c5f2 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 12:10:30 -0400 Subject: [PATCH 142/250] Update src/neurostatslib/glm.py Co-authored-by: William F. Broderick --- src/neurostatslib/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 1f110486..583f8636 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -418,7 +418,7 @@ class GLMRecurrent(GLM): See Also -------- - [GLM](../glm/#neurostatslib.glm.GLM) : Base class for the generalized linear model. + [GLM](./#neurostatslib.glm.GLM) : Base class for the generalized linear model. Notes ----- From 881d503d48d5c15b57a616fbcf0a5407dc9c1e7e Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 1 Nov 2023 12:15:10 -0400 Subject: [PATCH 143/250] Update src/neurostatslib/solver.py Co-authored-by: William F. Broderick --- src/neurostatslib/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 178c9770..05b043f5 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -541,7 +541,7 @@ class GroupLassoSolver(ProxGradientSolver): Attributes ---------- mask : Union[jnp.ndarray, NDArray] - A mask array indicating groups of features for regularization. + A 2d mask array indicating groups of features for regularization. Each row represents a group of features. Each column corresponds to a feature, where a value of 1 indicates that the feature belongs to the group, and a value of 0 indicates it doesn't. From c340dd2ddded7a4c06355eecd8e14f82c6bdc091 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 1 Nov 2023 12:18:42 -0400 Subject: [PATCH 144/250] linted --- src/neurostatslib/glm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 8462eb3e..03cdbd60 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -410,10 +410,10 @@ class GLMRecurrent(GLM): Parameters ---------- - noise_model : + noise_model : The noise model to use for the GLM. This defines how neural activity is generated based on the underlying firing rate. Common choices include Poisson and Gaussian models. - solver : + solver : The optimization solver to use for fitting the GLM parameters. See Also From 448d3705133af600ebc3e14068a4d8573223562f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 1 Nov 2023 12:35:36 -0400 Subject: [PATCH 145/250] removed mask from proximal operator --- src/neurostatslib/solver.py | 117 +++++++++++++++++------------------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 05b043f5..d3f8ac74 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -368,8 +368,6 @@ class ProxGradientSolver(Solver, abc.ABC): ---------- allowed_optimizers : List[...,str] A list of optimizer names that are allowed to be used with this solver. - mask : Optional[Union[NDArray, jnp.ndarray]] - An optional mask array for element-wise operations. Shape (n_groups, n_features) """ allowed_optimizers = ["ProximalGradient"] @@ -379,65 +377,11 @@ def __init__( solver_name: str, solver_kwargs: Optional[dict] = None, regularizer_strength: float = 1.0, - mask: Optional[Union[NDArray, jnp.ndarray]] = None, + **kwargs ): super().__init__(solver_name, solver_kwargs=solver_kwargs) - self.mask = mask self.regularizer_strength = regularizer_strength - @property - def mask(self): - return self._mask - - @mask.setter - def mask(self, mask: jnp.ndarray): - self._check_mask(mask) - self._mask = mask - - @staticmethod - def _check_mask(mask: jnp.ndarray): - """ - Validate the mask array. - - This method ensures the mask adheres to requirements: - - It should be 2-dimensional. - - Each element must be either 0 or 1. - - Each feature should belong to only one group. - - The mask should not be empty. - - The mask is an array of float type. - - Raises - ------ - ValueError - If any of the above conditions are not met. - """ - if mask.ndim != 2: - raise ValueError( - "`mask` must be 2-dimensional. " - f"{mask.ndim} dimensional mask provided instead!" - ) - - if mask.shape[0] == 0: - raise ValueError(f"Empty mask provided! Mask has shape {mask.shape}.") - - if jnp.any((mask != 1) & (mask != 0)): - raise ValueError("Mask elements be 0s and 1s!") - - if mask.sum() == 0: - raise ValueError("Empty mask provided!") - - if jnp.any(mask.sum(axis=0) > 1): - raise ValueError( - "Incorrect group assignment. Some of the features are assigned " - "to more then one group." - ) - - if not jnp.issubdtype(mask.dtype, jnp.floating): - raise ValueError( - "Mask should be a floating point jnp.ndarray. " - f"Data type {mask.dtype} provided instead!" - ) - @abc.abstractmethod def get_prox_operator( self, @@ -499,12 +443,10 @@ def __init__( solver_name: str = "ProximalGradient", solver_kwargs: Optional[dict] = None, regularizer_strength: float = 1.0, - mask: Optional[Union[NDArray, jnp.ndarray]] = None, ): super().__init__( solver_name, - solver_kwargs=solver_kwargs, - mask=mask, + solver_kwargs=solver_kwargs ) self.regularizer_strength = regularizer_strength @@ -564,11 +506,62 @@ def __init__( super().__init__( solver_name, solver_kwargs=solver_kwargs, - mask=mask, ) self.regularizer_strength = regularizer_strength - mask = jnp.asarray(mask) + self.mask = jnp.asarray(mask) + + @property + def mask(self): + return self._mask + + @mask.setter + def mask(self, mask: jnp.ndarray): self._check_mask(mask) + self._mask = mask + + @staticmethod + def _check_mask(mask: jnp.ndarray): + """ + Validate the mask array. + + This method ensures the mask adheres to requirements: + - It should be 2-dimensional. + - Each element must be either 0 or 1. + - Each feature should belong to only one group. + - The mask should not be empty. + - The mask is an array of float type. + + Raises + ------ + ValueError + If any of the above conditions are not met. + """ + if mask.ndim != 2: + raise ValueError( + "`mask` must be 2-dimensional. " + f"{mask.ndim} dimensional mask provided instead!" + ) + + if mask.shape[0] == 0: + raise ValueError(f"Empty mask provided! Mask has shape {mask.shape}.") + + if jnp.any((mask != 1) & (mask != 0)): + raise ValueError("Mask elements be 0s and 1s!") + + if mask.sum() == 0: + raise ValueError("Empty mask provided!") + + if jnp.any(mask.sum(axis=0) > 1): + raise ValueError( + "Incorrect group assignment. Some of the features are assigned " + "to more then one group." + ) + + if not jnp.issubdtype(mask.dtype, jnp.floating): + raise ValueError( + "Mask should be a floating point jnp.ndarray. " + f"Data type {mask.dtype} provided instead!" + ) def get_prox_operator( self, From 61a2511d13ea414cbb2ad0a0d4da436e6446652b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 1 Nov 2023 12:38:11 -0400 Subject: [PATCH 146/250] fixed solver mask --- src/neurostatslib/solver.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index d3f8ac74..78ee12ea 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -377,7 +377,7 @@ def __init__( solver_name: str, solver_kwargs: Optional[dict] = None, regularizer_strength: float = 1.0, - **kwargs + **kwargs, ): super().__init__(solver_name, solver_kwargs=solver_kwargs) self.regularizer_strength = regularizer_strength @@ -444,10 +444,7 @@ def __init__( solver_kwargs: Optional[dict] = None, regularizer_strength: float = 1.0, ): - super().__init__( - solver_name, - solver_kwargs=solver_kwargs - ) + super().__init__(solver_name, solver_kwargs=solver_kwargs) self.regularizer_strength = regularizer_strength def get_prox_operator( From 0183d9eb34d84fb8460df74227331dc229979613 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 1 Nov 2023 16:13:34 -0400 Subject: [PATCH 147/250] removed the renaming --- src/neurostatslib/glm.py | 30 +++++++------ src/neurostatslib/noise_model.py | 77 ++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 03cdbd60..3216e43a 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -204,19 +204,6 @@ def score( This computes the GLM pseudo-$R^2$ or the mean log-likelihood, thus the higher the number the better. - The pseudo-$R^2$ can be computed as follows, - - $$ - \begin{aligned} - R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ - &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) - - \log \text{LL}(\bar{\lambda}| y)}, - \end{aligned} - $$ - - where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is - the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model - predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate. See [1]. Parameters ---------- @@ -252,12 +239,27 @@ def score( of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. + The pseudo-$R^2$ can be computed as follows, + + $$ + \begin{aligned} + R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ + &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) + - \log \text{LL}(\bar{\lambda}| y)}, + \end{aligned} + $$ + + where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is + the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model + predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate, + see references[$^1$](#--references). + Refer to the `nsl.noise_model.NoiseModel` concrete subclasses for the specific likelihood equations. References ---------- - [1] Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. + 1. Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. Routledge, 2013. """ diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/noise_model.py index aae31eb0..fda8f378 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/noise_model.py @@ -206,10 +206,13 @@ def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: pass def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): - r"""Pseudo-R^2 calculation for a GLM. + r"""Pseudo-$R^2$ calculation for a GLM. - The Pseudo-R^2 metric gives a sense of how well the model fits the data, - relative to a null (or baseline) model. + Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002)[$^1$](#--references). + + This metric evaluates the goodness-of-fit of the model relative to a null (baseline) model that assumes a + constant mean for the observations. While the pseudo-$R^2$ is bounded between 0 and 1 for the training set, + it can yield negative values on out-of-sample data, indicating potential overfitting. Parameters ---------- @@ -224,6 +227,31 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, whereas a value closer to 0 suggests that the model doesn't improve much over the null model. + Notes + ----- + The pseudo-$R^2$ score is calculated as follows, + + $$ + \begin{aligned} + R_{\text{pseudo}}^2 &= \frac{LL(\bm{y}| \bm{\hat{\mu}}) - LL(\bm{y}| \bm{\mu_0})}{LL(\bm{y}| \bm{y}) - + LL(\bm{y}| \bm{\mu_0})}\\ + &= \frac{D(\bm{y}; \bm{\mu_0}) - D(\bm{y}; \bm{\hat{\mu}})}{D(\bm{y}; \bm{\mu_0})}, + \end{aligned} + $$ + + where $\bm{y}=[y_1,\dots, y_T]$, $\bm{\hat{\mu}} = \left[\hat{\mu}_1, \dots, \hat{\mu}_T \right]$ and, + $\bm{\mu_0} = \left[\mu_0, \dots, \mu_0 \right]$ are the counts, the model predicted rate and the average + firing rates respectively, $LL$ is the log-likelihood averaged over the samples, and + $D(\cdot\; ;\cdot)$ is the deviance averaged over samples, + $$ + D(\bm{y}; \bm{\mu}) = 2 \left( LL(\bm{y}| \bm{y}) - LL(\bm{y}| \bm{\mu}) \right). + $$ + + References + ---------- + 1. Jacob Cohen, Patricia Cohen, Steven G. West, Leona S. Aiken. + *Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences*. + 3rd edition. Routledge, 2002. p.502. ISBN 978-0-8058-2223-6. (May 2012) """ res_dev_t = self.residual_deviance(predicted_rate, y) resid_deviance = jnp.sum(res_dev_t**2) @@ -255,7 +283,7 @@ class PoissonNoiseModel(NoiseModel): def __init__(self, inverse_link_function=jnp.exp): super().__init__(inverse_link_function=inverse_link_function) - self._scale = 1 + self.scale = 1 def negative_log_likelihood( self, @@ -267,6 +295,21 @@ def negative_log_likelihood( This computes the Poisson negative log-likelihood of the predicted rates for the observed spike counts up to a constant. + Parameters + ---------- + predicted_rate : + The predicted rate of the current model. Shape (n_time_bins, n_neurons). + y : + The target spikes to compare against. Shape (n_time_bins, n_neurons). + + Returns + ------- + : + The Poisson negative log-likehood. Shape (1,). + + Notes + ----- + The formula for the Poisson mean log-likelihood is the following, $$ @@ -282,27 +325,13 @@ def negative_log_likelihood( Because $\Gamma(k+1)=k!$, see [wikipedia](https://en.wikipedia.org/wiki/Gamma_function) for explanation. - Parameters - ---------- - predicted_rate : - The predicted rate of the current model. Shape (n_time_bins, n_neurons). - y : - The target spikes to compare against. Shape (n_time_bins, n_neurons). - - Returns - ------- - : - The Poisson negative log-likehood. Shape (1,). - - Notes - ----- The $\log({y\_{tn}!})$ term is not a function of the parameters and can be disregarded when computing the loss-function. This is why we incorporated it into the `const` term. """ - predicted_firing_rates = jnp.clip(predicted_rate, a_min=self.FLOAT_EPS) - x = y * jnp.log(predicted_firing_rates) + predicted_rate = jnp.clip(predicted_rate, a_min=self.FLOAT_EPS) + x = y * jnp.log(predicted_rate) # see above for derivation of this. - return jnp.mean(predicted_firing_rates - x) + return jnp.mean(predicted_rate - x) def sample_generator( self, key: KeyArray, predicted_rate: jnp.ndarray @@ -346,19 +375,19 @@ def residual_deviance( Notes ----- - Deviance is a measure of the goodness of fit of a statistical model. + The deviance is a measure of the goodness of fit of a statistical model. For a Poisson model, the residual deviance is computed as: $$ \begin{aligned} D(y\_{tn}, \hat{y}\_{tn}) &= 2 \left[ y\_{tn} \log\left(\frac{y\_{tn}}{\hat{y}\_{tn}}\right) - (y\_{tn} - \hat{y}\_{tn}) \right]\\\ - &= -2 \left( \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right) - \text{LL}\left(y\_{tn} | y\_{tn}\right)\right) + &= 2 \left( \text{LL}\left(y\_{tn} | y\_{tn}\right) - \text{LL}\left(y\_{tn} | \hat{y}\_{tn}\right)\right) \end{aligned} $$ + where $ y $ is the observed data, $ \hat{y} $ is the predicted data, and $\text{LL}$ is the model log-likelihood. Lower values of deviance indicate a better fit. - """ # this takes care of 0s in the log ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) From ab3cd99b926c2f525f14aec52cd3a1409e1a255c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 1 Nov 2023 17:14:35 -0400 Subject: [PATCH 148/250] removed the any reference to noise --- docs/developers_notes/02-base_class.md | 8 +-- ...oise_model.md => 03-observation_models.md} | 26 +++---- docs/developers_notes/05-glm.md | 18 ++--- docs/examples/plot_glm_demo.py | 26 +++---- src/neurostatslib/__init__.py | 2 +- src/neurostatslib/glm.py | 67 ++++++++++--------- .../{noise_model.py => observation_models.py} | 34 +++++----- tests/conftest.py | 16 ++--- tests/test_glm.py | 18 ++--- ...se_model.py => test_observation_models.py} | 42 ++++++------ 10 files changed, 129 insertions(+), 128 deletions(-) rename docs/developers_notes/{03-noise_model.md => 03-observation_models.md} (52%) rename src/neurostatslib/{noise_model.py => observation_models.py} (92%) rename tests/{test_noise_model.py => test_observation_models.py} (75%) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index d3fb631b..3fedf458 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -4,7 +4,7 @@ The `base_class` module introduces the `Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `Base`. -The `Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, noise models, solvers etc.). In contrast, abstract classes derived from `Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `noise_model.NoiseModel` is the building block for the Poisson noise, Gamma noise, ... etc.). +The `Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, observation models, solvers etc.). In contrast, abstract classes derived from `Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `observation_models.Observations` is the building block for the Poisson observations, Gamma observations, ... etc.). Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. @@ -30,11 +30,11 @@ Abstract Class Base │ ├─ Concrete Subclass RidgeSolver │ ... │ -├─ Abstract Subclass NoiseModel +├─ Abstract Subclass Observations │ │ -│ ├─ Concrete Subclass PoissonNoiseModel +│ ├─ Concrete Subclass PoissonObservations │ │ -│ ├─ Concrete Subclass GammaNoiseModel *(not implemented yet) +│ ├─ Concrete Subclass GammaObservations *(not implemented yet) │ ... │ ... diff --git a/docs/developers_notes/03-noise_model.md b/docs/developers_notes/03-observation_models.md similarity index 52% rename from docs/developers_notes/03-noise_model.md rename to docs/developers_notes/03-observation_models.md index aa6b6eb8..9b01babc 100644 --- a/docs/developers_notes/03-noise_model.md +++ b/docs/developers_notes/03-observation_models.md @@ -1,22 +1,22 @@ -# The `noise_model` Module +# The `observation_models` Module ## Introduction -The `noise_model` module provides objects representing the observation noise of GLM-like models. +The `observation_models` module provides objects representing the observations of GLM-like models. -The abstract class `NoiseModel` defines the structure of the subclasses which specify observation noise types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`neurostatslib.glm.GLM`](../03-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. +The abstract class `Observations` defines the structure of the subclasses which specify observation types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`neurostatslib.glm.GLM`](../03-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. -## The Abstract class `NoiseModel` +## The Abstract class `Observations` -The abstract class `NoiseModel` is the backbone of any noise model. Any class inheriting `NoiseModel` must reimplement the `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `estimate_scale` methods. +The abstract class `Observations` is the backbone of any observation model. Any class inheriting `Observations` must reimplement the `negative_log_likelihood`, `sample_generator`, `residual_deviance`, and `estimate_scale` methods. ### Abstract Methods -For subclasses derived from `NoiseModel` to function correctly, they must implement the following: +For subclasses derived from `Observations` to function correctly, they must implement the following: - **negative_log_likelihood**: Computes the negative-log likelihood of the model up to a normalization constant. This method is usually part of the objective function used to learn GLM parameters. -- **emission_probability**: Returns the random emission probability function. This typically invokes `jax.random` emission probability, provided some sufficient statistics[^1]. For distributions in the exponential family, the sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is entirely specified by the model's weights, while the scale is either fixed (i.e., Poisson) or needs to be estimated (i.e., Gamma). +- **sample_generator**: Returns the random emission probability function. This typically invokes `jax.random` emission probability, provided some sufficient statistics[^1]. For distributions in the exponential family, the sufficient statistics are the canonical parameter and the scale. In GLMs, the canonical parameter is entirely specified by the model's weights, while the scale is either fixed (i.e., Poisson) or needs to be estimated (i.e., Gamma). - **residual_deviance**: Computes the residual deviance based on the model's estimated rates and observations. @@ -31,15 +31,15 @@ For subclasses derived from `NoiseModel` to function correctly, they must implem - **_check_inverse_link_function**: Check that the provided link function is a `Callable` of the `jax` namespace. -## Concrete `PoissonNoiseModel` class +## Concrete `PoissonObservations` class -The `PoissonNoiseModel` class extends the abstract `NoiseModel` class to provide functionalities specific to the Poisson noise model. It is designed for modeling observed spike counts based on a Poisson distribution with a given rate. +The `PoissonObservations` class extends the abstract `Observations` class to provide functionalities specific to the Poisson observation model. It is designed for modeling observed spike counts based on a Poisson distribution with a given rate. ### Overridden Methods - **negative_log_likelihood**: This method computes the Poisson negative log-likelihood of the predicted rates for the observed spike counts. -- **emission_probability**: Generates random numbers from a Poisson distribution based on the given `predicted_rate`. +- **sample_generator**: Generates random numbers from a Poisson distribution based on the given `predicted_rate`. - **residual_deviance**: Calculates the residual deviance for a Poisson model. @@ -47,11 +47,11 @@ The `PoissonNoiseModel` class extends the abstract `NoiseModel` class to provide ## Contributor Guidelines -To implement a noise model class you +To implement an observation model class you -- **Must** inherit from `NoiseModel` +- **Must** inherit from `Observations` -- **Must** provide a concrete implementation of `negative_log_likelihood`, `emission_probability`, `residual_deviance`, and `estimate_scale`. +- **Must** provide a concrete implementation of `negative_log_likelihood`, `sample_generator`, `residual_deviance`, and `estimate_scale`. - **Should not** reimplement the `pseudo_r2` method as well as the `_check_inverse_link_function` auxiliary method. diff --git a/docs/developers_notes/05-glm.md b/docs/developers_notes/05-glm.md index 1711cb0a..2b66c601 100644 --- a/docs/developers_notes/05-glm.md +++ b/docs/developers_notes/05-glm.md @@ -15,7 +15,7 @@ Our design aligns with the `scikit-learn` API, facilitating seamless integration The classes provided here are modular by design offering a standard foundation for any GLM variant. -Instantiating a specific GLM simply requires providing an observation noise model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`neurostatslib.noise_model.NoiseModel`](../04-noise_model/#the-abstract-class-noisemodel) and [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) objects, respectively. +Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`neurostatslib.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) and [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) objects, respectively.
@@ -36,21 +36,21 @@ The `GLM` class provides a direct implementation of the GLM model and is designe ### Attributes - **`solver`**: Refers to the optimization solver - an object of the [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. -- **`noise_model`**: Represents the GLM noise model, which is an object of the [`neurostatslib.noise_model.NoiseModel`](../04-noise_model/#the-abstract-class-noisemodel) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. +- **`observation_models`**: Represents the GLM observation model, which is an object of the [`neurostatslib.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. - **`basis_coeff_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`baseline_link_fr_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`solver_state`**: Indicates the solver's state. For specific solver states, refer to the [`jaxopt` documentation](https://jaxopt.github.io/stable/index.html#). ### Public Methods -- **`predict`**: Validates input and computes the mean rates of the `GLM` by invoking the inverse-link function of the `noise_model` attribute. -- **`score`**: Validates input and assesses the Poisson GLM using either log-likelihood or pseudo-$R^2$. This method uses the `noise_model` to determine log-likelihood or pseudo-$R^2$. -- **`fit`**: Validates input and aligns the Poisson GLM with spike train data. It leverages the `noise_model` and `solver` to define the model's loss function and instantiate the solver. -- **`simulate`**: Simulates spike trains using the GLM as a feedforward network, invoking the `noise_model.emission_probability` method for emission probability. +- **`predict`**: Validates input and computes the mean rates of the `GLM` by invoking the inverse-link function of the `observation_models` attribute. +- **`score`**: Validates input and assesses the Poisson GLM using either log-likelihood or pseudo-$R^2$. This method uses the `observation_models` to determine log-likelihood or pseudo-$R^2$. +- **`fit`**: Validates input and aligns the Poisson GLM with spike train data. It leverages the `observation_models` and `solver` to define the model's loss function and instantiate the solver. +- **`simulate`**: Simulates spike trains using the GLM as a feedforward network, invoking the `observation_models.sample_generator` method for emission probability. ### Private Methods -- **`_predict`**: Forecasts rates based on current model parameters and the inverse-link function of the `noise_model`. +- **`_predict`**: Forecasts rates based on current model parameters and the inverse-link function of the `observation_models`. - **`_score`**: Determines the Poisson negative log-likelihood, excluding normalization constants. - **`_check_is_fit`**: Validates whether the model has been appropriately fit by ensuring model parameters are set. If not, a `NotFittedError` is raised. @@ -61,7 +61,7 @@ The `RecurrentGLM` class is an extension of the `GLM`, designed to simulate mode ### Overridden Methods -- **`simulate`**: This method simulates spike trains, treating the GLM as a recurrent neural network. It utilizes the `noise_model.emission_probability` method to determine the emission probability. +- **`simulate`**: This method simulates spike trains, treating the GLM as a recurrent neural network. It utilizes the `observation_models.sample_generator` method to determine the emission probability. ## Contributor Guidelines @@ -71,6 +71,6 @@ When crafting a functional (i.e., concrete) GLM class: - **Must** inherit from `BaseRegressor` or one of its derivatives. - **Must** realize the `predict`, `fit`, `score`, and `simulate` methods, either directly or through inheritance. -- **Should** incorporate a `noise_model` attribute of type `neurostatslib.noise_model.NoiseModel` to specify the link-function, emission probability, and likelihood. +- **Should** incorporate a `observation_models` attribute of type `neurostatslib.observation_models.Observations` to specify the link-function, emission probability, and likelihood. - **Should** include a `solver` attribute of type `neurostatslib.solver.Solver` to establish the solver based on penalization type. - **May** embed additional parameter and input checks if required by the specific GLM subclass. diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index f8dd96d5..f7b7a4cd 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -53,20 +53,20 @@ # The class implementing the feed-forward GLM is `neurostatslib.glm.GLM`. # In order to define the class, one **must** provide: # -# - **Noise Model**: The noise model for the GLM, e.g. an object of the class of type -# `neurostatslib.noise_model.NoiseModel`. So far, only the `PoissonNoiseModel` noise +# - **Observation Model**: The observation model for the GLM, e.g. an object of the class of type +# `neurostatslib.observation_models.Observations`. So far, only the `PoissonObservations` # model has been implemented. # - **Solver**: The desired solver, e.g. an object of the `neurostatslib.solver.Solver` class. # Currently, we implemented the un-regulrized, Ridge, Lasso, and Group-Lasso solver. # -# The default for the GLM class is the `PoissonNoiseModel` with log-link function with a Ridge solver. +# The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge solver. # Here is how to define the model. -# default Poisson GLM with Ridge solver and Poisson noise model. +# default Poisson GLM with Ridge solver and Poisson observation model. model = nsl.glm.GLM() print("Solver type: ", type(model.solver)) -print("Noise model type:",type(model.noise_model)) +print("Observation model:", type(model.observation_model)) # %% # ### Model Configuration @@ -89,10 +89,10 @@ # These parameters can be configured at initialization and/or # set after the model is initialized with the following syntax: -# Poisson noise model with soft-plus NL -noise_model = nsl.noise_model.PoissonNoiseModel(jax.nn.softplus) +# Poisson observation model with soft-plus NL +observation_models = nsl.observation_models.PoissonObservations(jax.nn.softplus) -# Observation noise +# Observation model solver = nsl.solver.RidgeSolver( solver_name="LBFGS", regularizer_strength=0.1, @@ -101,23 +101,23 @@ # define the GLM model = nsl.glm.GLM( - noise_model=noise_model, + observation_model=observation_models, solver=solver, ) -print("Solver type: ", type(model.solver)) -print("Noise model type:",type(model.noise_model)) +print("Solver type: ", type(model.solver)) +print("Observation model:", type(model.observation_model)) # %% # Hyperparameters can be set at any moment via the `set_params` method. model.set_params( solver=nsl.solver.LassoSolver(), - noise_model__inverse_link_function=jax.numpy.exp + observation_model__inverse_link_function=jax.numpy.exp ) print("Updated solver: ", model.solver) -print("Updated NL: ", model.noise_model.inverse_link_function) +print("Updated NL: ", model.observation_model.inverse_link_function) # %% # !!! warning diff --git a/src/neurostatslib/__init__.py b/src/neurostatslib/__init__.py index c2a0954d..999ad7b0 100644 --- a/src/neurostatslib/__init__.py +++ b/src/neurostatslib/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -from . import basis, glm, noise_model, sample_points, solver, utils +from . import basis, glm, observation_models, sample_points, solver, utils diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 3216e43a..24aafb8a 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -5,7 +5,7 @@ import jax.numpy as jnp from numpy.typing import NDArray -from . import noise_model as nsm +from . import observation_models as nsm from . import solver as slv from .base_class import BaseRegressor from .exceptions import NotFittedError @@ -17,49 +17,49 @@ class GLM(BaseRegressor): Generalized Linear Model (GLM) for neural activity data. This GLM implementation allows users to model neural activity based on a combination of exogenous inputs - (like convolved currents or light intensities) and a choice of noise model. It is suitable for scenarios where + (like convolved currents or light intensities) and a choice of observation model. It is suitable for scenarios where the relationship between predictors and the response variable might be non-linear, and the residuals don't follow a normal distribution. Parameters ---------- - noise_model : NoiseModel - Noise model to use. The model describes the noise distribution of the neural activity. - Default is Poisson noise model. - solver : Solver + observation_model : + Observation model to use. The model describes the distribution of the neural activity. + Default is the Poisson model. + solver : Solver to use for model optimization. Defines the optimization algorithm and related parameters. Default is Ridge regression with gradient descent. Attributes ---------- - noise_model : NoiseModel - Noise model being used. - solver : Solver + observation_model : + Observation model being used. + solver : Solver being used. - baseline_link_fr_ : jnp.ndarray or None + baseline_link_fr_ : Model baseline link firing rate parameters. - basis_coeff_ : jnp.ndarray or None + basis_coeff_ : Basis coefficients for the model. - solver_state : Any + solver_state : State of the solver after fitting. May include details like optimization error. Raises ------ TypeError - If provided `solver` or `noise_model` are not valid or implemented in `neurostatslib.solver` and - `neurostatslib.noise_model` respectively. + If provided `solver` or `observation_model` are not valid or implemented in `neurostatslib.solver` and + `neurostatslib.observation_models` respectively. Notes ----- The GLM aims to model the relationship between several predictor variables and a response variable. In this neural context, the predictors might represent external inputs or other neurons' activity, while - the response variable is the neuron's activity being modeled. The noise model captures the statistical properties - of the neural activity, while the solver determines how the model parameters are estimated. + the response variable is the neuron's activity being modeled. The observation model captures the statistical + properties of the neural activity, while the solver determines how the model parameters are estimated. """ def __init__( self, - noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), + observation_model: nsm.Observations = nsm.PoissonObservations(), solver: slv.Solver = slv.RidgeSolver("GradientDescent"), ): super().__init__() @@ -70,13 +70,14 @@ def __init__( f"Available options are: {slv.__all__}." ) - if noise_model.__class__.__name__ not in nsm.__all__: + if observation_model.__class__.__name__ not in nsm.__all__: raise TypeError( - "The provided `noise_model` should be one of the implemented models in `neurostatslib.noise_model`. " + "The provided `observation_model` should be one of the implemented models in " + "`neurostatslib.observation_models`. " f"Available options are: {nsm.__all__}." ) - self.noise_model = noise_model + self.observation_model = observation_model self.solver = solver # initialize to None fit output @@ -110,7 +111,7 @@ def _predict( The predicted rates. Shape (n_time_bins, n_neurons). """ Ws, bs = params - return self.noise_model.inverse_link_function( + return self.observation_model.inverse_link_function( jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :] ) @@ -191,7 +192,7 @@ def _score( """ predicted_rate = self._predict(params, X) - return self.noise_model.negative_log_likelihood(predicted_rate, y) + return self.observation_model.negative_log_likelihood(predicted_rate, y) def score( self, @@ -254,7 +255,7 @@ def score( predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate, see references[$^1$](#--references). - Refer to the `nsl.noise_model.NoiseModel` concrete subclasses for the specific likelihood equations. + Refer to the `nsl.observation_models.Observations` concrete subclasses for the specific likelihood equations. References @@ -283,7 +284,7 @@ def score( norm_constant = jax.scipy.special.gammaln(y + 1).mean() score = -self._score((Ws, bs), X, y) - norm_constant else: - score = self.noise_model.pseudo_r2(self._predict((Ws, bs), X), y) + score = self.observation_model.pseudo_r2(self._predict((Ws, bs), X), y) return score @@ -328,9 +329,9 @@ def fit( # Run optimization runner = self.solver.instantiate_solver(self._score) params, state = runner(init_params, X, y) - # if any noise model other than Poisson are used + # if any observation model other than Poisson are used # one should set the scale parameter too. - # self.noise_model.set_scale(params) + # self.observation_model.set_scale(params) if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): raise ValueError( @@ -394,7 +395,7 @@ def simulate( ) predicted_rate = self._predict((Ws, bs), feedforward_input) return ( - self.noise_model.sample_generator( + self.observation_model.sample_generator( key=random_key, predicted_rate=predicted_rate ), predicted_rate, @@ -412,8 +413,8 @@ class GLMRecurrent(GLM): Parameters ---------- - noise_model : - The noise model to use for the GLM. This defines how neural activity is generated + observation_model : + The observation model to use for the GLM. This defines how neural activity is generated based on the underlying firing rate. Common choices include Poisson and Gaussian models. solver : The optimization solver to use for fitting the GLM parameters. @@ -441,10 +442,10 @@ class GLMRecurrent(GLM): def __init__( self, - noise_model: nsm.NoiseModel = nsm.PoissonNoiseModel(), + observation_model: nsm.Observations = nsm.PoissonObservations(), solver: slv.Solver = slv.RidgeSolver(), ): - super().__init__(noise_model=noise_model, solver=solver) + super().__init__(observation_model=observation_model, solver=solver) def simulate_recurrent( self, @@ -578,12 +579,12 @@ def scan_fn( # Predict the firing rate using the model coefficients # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input - firing_rate = self.noise_model.inverse_link_function( + firing_rate = self.observation_model.inverse_link_function( jnp.einsum("ik,tik->ti", Wr, conv_act) + input_slice + bs[None, :] ) # Simulate activity based on the predicted firing rate - new_act = self.noise_model.sample_generator(key, firing_rate) + new_act = self.observation_model.sample_generator(key, firing_rate) # Prepare the spikes for the next iteration (keeping the most recent spikes) concat_act = jnp.row_stack((activity[1:], new_act)), chunk + 1 diff --git a/src/neurostatslib/noise_model.py b/src/neurostatslib/observation_models.py similarity index 92% rename from src/neurostatslib/noise_model.py rename to src/neurostatslib/observation_models.py index fda8f378..99906336 100644 --- a/src/neurostatslib/noise_model.py +++ b/src/neurostatslib/observation_models.py @@ -1,4 +1,4 @@ -"""Noise model classes for GLMs.""" +"""Observation model classes for GLMs.""" import abc from typing import Callable, Union @@ -10,19 +10,19 @@ KeyArray = Union[jnp.ndarray, jax.random.PRNGKeyArray] -__all__ = ["PoissonNoiseModel"] +__all__ = ["PoissonObservations"] def __dir__(): return __all__ -class NoiseModel(Base, abc.ABC): +class Observations(Base, abc.ABC): """ - Abstract noise model class for neural data processing. + Abstract observation model class for neural data processing. - This is an abstract base class used to implement noise models for neural data. - Specific noise models that inherit from this class should define their versions + This is an abstract base class used to implement observation models for neural data. + Specific observation models that inherit from this class should define their versions of the abstract methods: negative_log_likelihood, emission_probability, and residual_deviance. @@ -35,8 +35,8 @@ class NoiseModel(Base, abc.ABC): See Also -------- - [PoissonNoiseModel](./#neurostatslib.noise_model.PoissonNoiseModel) : A specific implementation of a - noise model using the Poisson distribution. + [PoissonObservations](./#neurostatslib.observation_models.PoissonObservations) : A specific implementation of a + observation model using the Poisson distribution. """ FLOAT_EPS = jnp.finfo(float).eps @@ -121,7 +121,7 @@ def _check_inverse_link_function(inverse_link_function: Callable): @abc.abstractmethod def negative_log_likelihood(self, predicted_rate, y): - r"""Compute the noise model negative log-likelihood. + r"""Compute the observation model negative log-likelihood. This computes the negative log-likelihood of the predicted rates for the observed neural activity up to a constant. @@ -160,13 +160,13 @@ def sample_generator( Returns ------- : - Random numbers generated from the noise model with `predicted_rate`. + Random numbers generated from the observation model with `predicted_rate`. """ pass @abc.abstractmethod def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray): - r"""Compute the residual deviance for the noise model. + r"""Compute the residual deviance for the observation model. Parameters ---------- @@ -263,13 +263,13 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): return (null_deviance - resid_deviance) / null_deviance -class PoissonNoiseModel(NoiseModel): +class PoissonObservations(Observations): """ - Poisson Noise Model class for spike count data. + Model observations as Poisson random variables. - The PoissonNoiseModel is designed to model the observed spike counts based on a Poisson distribution - with a given rate. It provides methods for computing the negative log-likelihood, emission probability, - and residual deviance for the given spike count data. + The PoissonObservations is designed to model the observed spike counts based on a Poisson distribution + with a given rate. It provides methods for computing the negative log-likelihood, generating samples, + and computing the residual deviance for the given spike count data. Attributes ---------- @@ -278,7 +278,7 @@ class PoissonNoiseModel(NoiseModel): See Also -------- - [NoiseModel](./#neurostatslib.noise_model.NoiseModel) : Base class for noise models. + [Observations](./#neurostatslib.observation_models.Observations) : Base class for observation models. """ def __init__(self, inverse_link_function=jnp.exp): diff --git a/tests/conftest.py b/tests/conftest.py index f4f0d670..39509a0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,9 +40,9 @@ def poissonGLM_model_instantiation(): X = np.random.normal(size=(100, 1, 5)) b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) - noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) + observation_model = nsl.observation_models.PoissonObservations(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) - model = nsl.glm.GLM(noise_model, solver) + model = nsl.glm.GLM(observation_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -68,9 +68,9 @@ def poissonGLM_coupled_model_config_simulate(): "simulate_coupled_neurons_params.json"), "r") as fh: config_dict = json.load(fh) - noise = nsl.noise_model.PoissonNoiseModel(jnp.exp) + observations = nsl.observation_models.PoissonObservations(jnp.exp) solver = nsl.solver.RidgeSolver("BFGS", regularizer_strength=0.1) - model = nsl.glm.GLMRecurrent(noise_model=noise, solver=solver) + model = nsl.glm.GLMRecurrent(observation_model=observations, solver=solver) model.basis_coeff_ = jnp.asarray(config_dict["basis_coeff_"]) model.baseline_link_fr_ = jnp.asarray(config_dict["baseline_link_fr_"]) coupling_basis = jnp.asarray(config_dict["coupling_basis"]) @@ -116,9 +116,9 @@ def group_sparse_poisson_glm_model_instantiation(): mask = np.zeros((2, 5)) mask[0, 1:4] = 1 mask[1, [0,4]] = 1 - noise_model = nsl.noise_model.PoissonNoiseModel(jnp.exp) + observation_model = nsl.observation_models.PoissonObservations(jnp.exp) solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) - model = nsl.glm.GLM(noise_model, solver) + model = nsl.glm.GLM(observation_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask @@ -136,8 +136,8 @@ def example_data_prox_operator(): return params, regularizer_strength, mask, scaling @pytest.fixture -def poisson_noise_model(): - return nsl.noise_model.PoissonNoiseModel(jnp.exp) +def poisson_observation_model(): + return nsl.observation_models.PoissonObservations(jnp.exp) @pytest.fixture diff --git a/tests/test_glm.py b/tests/test_glm.py index b53b52e2..567e7e06 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -41,27 +41,27 @@ class TestGLM: (1, TypeError, "The provided `solver` should be one of the implemented") ] ) - def test_init_solver_type(self, solver, error, match_str, poisson_noise_model): + def test_init_solver_type(self, solver, error, match_str, poisson_observation_model): """ Test initialization with different solver names. Check if an appropriate exception is raised when the solver name is not present in jaxopt. """ - _test_class_initialization(self.cls, {'solver': solver, 'noise_model': poisson_noise_model}, error, match_str) + _test_class_initialization(self.cls, {'solver': solver, 'observation_model': poisson_observation_model}, error, match_str) @pytest.mark.parametrize( - "noise, error, match_str", + "observation, error, match_str", [ - (nsl.noise_model.PoissonNoiseModel(), None, None), - (nsl.solver.Solver, TypeError, "The provided `noise_model` should be one of the implemented"), - (1, TypeError, "The provided `noise_model` should be one of the implemented") + (nsl.observation_models.PoissonObservations(), None, None), + (nsl.solver.Solver, TypeError, "The provided `observation_model` should be one of the implemented"), + (1, TypeError, "The provided `observation_model` should be one of the implemented") ] ) - def test_init_noise_type(self, noise, error, match_str, ridge_solver): + def test_init_observation_type(self, observation, error, match_str, ridge_solver): """ Test initialization with different solver names. Check if an appropriate exception is raised when the solver name is not present in jaxopt. """ - _test_class_initialization(self.cls, {'solver': ridge_solver, 'noise_model': noise}, error, match_str) + _test_class_initialization(self.cls, {'solver': ridge_solver, 'observation_model': observation}, error, match_str) ####################### # Test model.fit @@ -1013,7 +1013,7 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): model.baseline_link_fr_ = true_params[1] # get the rate dev = sm.families.Poisson().deviance(y, firing_rate) - dev_model = model.noise_model.residual_deviance(firing_rate, y).sum() + dev_model = model.observation_model.residual_deviance(firing_rate, y).sum() if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") diff --git a/tests/test_noise_model.py b/tests/test_observation_models.py similarity index 75% rename from tests/test_noise_model.py rename to tests/test_observation_models.py index dc91fb7f..e857631e 100644 --- a/tests/test_noise_model.py +++ b/tests/test_observation_models.py @@ -8,12 +8,12 @@ import neurostatslib as nsl -class TestPoissonNoiseModel: - cls = nsl.noise_model.PoissonNoiseModel +class TestPoissonObservations: + cls = nsl.observation_models.PoissonObservations @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) def test_initialization_link_is_callable(self, link_function): - """Check that the noise model initializes when a callable is passed.""" + """Check that the observation model initializes when a callable is passed.""" raise_exception = not callable(link_function) if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): @@ -23,7 +23,7 @@ def test_initialization_link_is_callable(self, link_function): @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x:x, sm.families.links.log()]) def test_initialization_link_is_jax(self, link_function): - """Check that the noise model initializes when a callable is passed.""" + """Check that the observation model initializes when a callable is passed.""" raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray"): @@ -33,39 +33,39 @@ def test_initialization_link_is_jax(self, link_function): @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) def test_initialization_link_is_callable_set_params(self, link_function): - """Check that the noise model initializes when a callable is passed.""" - noise_model = self.cls() + """Check that the observation model initializes when a callable is passed.""" + observation_model = self.cls() raise_exception = not callable(link_function) if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): - noise_model.set_params(inverse_link_function=link_function) + observation_model.set_params(inverse_link_function=link_function) else: - noise_model.set_params(inverse_link_function=link_function) + observation_model.set_params(inverse_link_function=link_function) @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log()]) def test_initialization_link_is_jax_set_params(self, link_function): - """Check that the noise model initializes when a callable is passed.""" + """Check that the observation model initializes when a callable is passed.""" raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) - noise_model = self.cls() + observation_model = self.cls() if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray!"): - noise_model.set_params(inverse_link_function=link_function) + observation_model.set_params(inverse_link_function=link_function) else: - noise_model.set_params(inverse_link_function=link_function) + observation_model.set_params(inverse_link_function=link_function) @pytest.mark.parametrize("link_function", [ jnp.exp, lambda x: jnp.exp(x) if isinstance(x, jnp.ndarray) else "not a number" ]) def test_initialization_link_returns_scalar(self, link_function): - """Check that the noise model initializes when a callable is passed.""" + """Check that the observation model initializes when a callable is passed.""" raise_exception = not isinstance(link_function(1.), (jnp.ndarray, float)) - noise_model = self.cls() + observation_model = self.cls() if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` must handle scalar inputs correctly"): - noise_model.set_params(inverse_link_function=link_function) + observation_model.set_params(inverse_link_function=link_function) else: - noise_model.set_params(inverse_link_function=link_function) + observation_model.set_params(inverse_link_function=link_function) def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): """ @@ -74,7 +74,7 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): """ _, y, model, _, firing_rate = poissonGLM_model_instantiation dev = sm.families.Poisson().deviance(y, firing_rate) - dev_model = model.noise_model.residual_deviance(firing_rate, y).sum() + dev_model = model.observation_model.residual_deviance(firing_rate, y).sum() if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") @@ -84,7 +84,7 @@ def test_loglikelihood_against_scipy(self, poissonGLM_model_instantiation): Assesses if the model estimates are close to statsmodels' results. """ _, y, model, _, firing_rate = poissonGLM_model_instantiation - ll_model = - model.noise_model.negative_log_likelihood(firing_rate, y).sum()\ + ll_model = - model.observation_model.negative_log_likelihood(firing_rate, y).sum()\ - jax.scipy.special.gammaln(y + 1).mean() ll_scipy = sts.poisson(firing_rate).logpmf(y).mean() if not np.allclose(ll_model, ll_scipy): @@ -96,7 +96,7 @@ def test_pseudo_r2_range(self, poissonGLM_model_instantiation): Compute the pseudo-r2 and check that is < 1. """ _, y, model, _, firing_rate = poissonGLM_model_instantiation - pseudo_r2 = model.noise_model.pseudo_r2(firing_rate, y) + pseudo_r2 = model.observation_model.pseudo_r2(firing_rate, y) if (pseudo_r2 > 1) or (pseudo_r2 < 0): raise ValueError(f"pseudo-r2 of {pseudo_r2} outside the [0,1] range!") @@ -106,7 +106,7 @@ def test_pseudo_r2_mean(self, poissonGLM_model_instantiation): Check that the pseudo-r2 of the null model is 0. """ _, y, model, _, _ = poissonGLM_model_instantiation - pseudo_r2 = model.noise_model.pseudo_r2(y.mean(), y) + pseudo_r2 = model.observation_model.pseudo_r2(y.mean(), y) if not np.allclose(pseudo_r2, 0): raise ValueError(f"pseudo-r2 of {pseudo_r2} for the null model. Should be equal to 0!") @@ -118,6 +118,6 @@ def test_emission_probability(selfself, poissonGLM_model_instantiation): """ _, _, model, _, _ = poissonGLM_model_instantiation key_array = jax.random.PRNGKey(123) - counts = model.noise_model.sample_generator(key_array, np.arange(1, 11)) + counts = model.observation_model.sample_generator(key_array, np.arange(1, 11)) if not jnp.all(counts == jax.random.poisson(key_array, np.arange(1, 11))): raise ValueError("The emission probability should output the results of a call to jax.random.poisson.") From 2991896ce0e0c485bd814d298e6fea072fcbffc1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 2 Nov 2023 18:20:40 -0400 Subject: [PATCH 149/250] allow a loss function with 3 arguments with any variable names --- docs/developers_notes/GLM_scheme.jpg | Bin 137389 -> 305373 bytes src/neurostatslib/glm.py | 37 ++++++++++----------- src/neurostatslib/solver.py | 32 ++++++++++++------- tests/test_glm.py | 4 +-- tests/test_solver.py | 46 ++++++++++++--------------- 5 files changed, 59 insertions(+), 60 deletions(-) diff --git a/docs/developers_notes/GLM_scheme.jpg b/docs/developers_notes/GLM_scheme.jpg index 7fea537473e6adf6e5653c1f737b30095ab27eae..96f86eff8e96833a271dd41a453a75743546bc41 100644 GIT binary patch literal 305373 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

!T%g=1zmt zQGcZXSi-0ET9@XEotqC^gQ~Mcwn~4_m=XZk&ZeepeJg2gblAPYePVZhDIyiBxV8d- zJ@0B5jMbhcT$^;wnZ~&>TXOc)+PK?S1C==WOI094iG#4Wj)Agmm}o5<=96o0Fg(*X zOjiD&R2w5P=}A+bgQ#1vEhy~PPPXp=9=?u`aEnCb3OHjkT#vdwC)TbQED^QIHS@jM zI3%{@XA(!|xMdxU_*N^>H`kr1t8^$=hdiSbL8sv4mRHhLUU_B^QI0-oAwx67e=X{E;*We&_ zPc#+nO`dVjt_}rCh*l&+B#fD|$mM-;KP>Acr500#IxJlSuXfz=N@CMZ(d5Y~DM$mr zg0&NI4;|qwA2k6%HJ#zRyTJj1-P$P%Jaf&dgv7uazT*S5N4bHe6>))Ap{2K6h5GjX z%Uv3_tTehu(8$y+!oQVPka$8bm&7Cg2^6)9$yX9ujBB<^$tVYonuVTIOkQ*%m(cjIZhHAX&pJLp z{lCuoM?n0ce;8!0f*vf!-65FkL=2QhUv7XubWG8-H^UW?ZGY&*AT)|L@9!KmxFZLz zNQ*HLTQcf3KG1>6_2IGe^IO`^S&A*G-4JBN)?%7C%f)#YbjP=M1RDdMh>JQPi_%{T zq~X{+pPpa`asE|15ZArbD&i*iQFEhQ&1I--s1QltS%{WmbO6sYEJ*J*d_=2a)v->sRy7BC(vA!%%@XtB&HrzDMj=0ib+G-$8ep&j1IG zq2QiovbE0Y8_}B{|4_yEPG$Df|Np1Vek@NvU`L%3$bC}MHS)0hkCA%*Hy=b|LS&We zDbKsfjxAtn6(Oq(-#&8T9a{&K>}F7-dmt@s~jVf0%2*KHfZEbqD=0)@8&Fj1Rguqcz=8#KF57qDUZ5d9kOQ$J#=87fGB{kzy{dSLhw5X6atY9=tU+V@YoFK zjPJN$pP)d8Q?R3x;CFV=@V)F@zZX7`3|#^cWXHi^m!LU&SFoQufG#|4grXht_8_l9 zfMnDGc${{DJ%ZvYT84mbo`Xm7B9vkWkeu{xAaepv-O-=O9Yvg8DL0~&j#J2uHmKJu z0u}}1@d6h0pjk7ZS-M+|k3vH2zx*QskF{F{;^ueAU7}6T3xM~U29apl9DDI2cP$6 zfGN(e3Mbt-e(({Zo;cH|muL+j7OS987oHaz?2*srjf*2~45>M1E@(^^g4|z^LC&v= z->bIXP=)OXO>e3`dFpZH`4t2_0JR0y7&(fhww}sGN30z?VAYs9I0+?kZ?8_S=Rgv$ zIrIi!M#3AH$PC~P4q!hF|FL2bP!%)gH1?WDT#@lc`c5{XFAU>NjbqdJvp^F2L}x}q zTwdgL23%ew0}O{)8Nh6bU-mtuNkYpw#dd~I6fdG#2=c)u`Ei5tdrgZJHVtOQWvQly z-3EyHY}Q3u-9NaKFB!IoM8xuM7>Qs7%{&Q%;$pVH(hV8_~kF2UTC0yMP{no$6I?{1$wNWy?uaq@6s=#5N|sBobOH6hQ+7h z9~SFvaMqJhOC2bW0hmIrv`XdGXOCN?^*3R)B@*M_#~C*^@weY`K+WD+05Sn!q5y$E zPT695`Zbuf%znIJsW%et!>mGee8pP(7W&SYG?%3BVdOM>k6Gz#u)FFgY-qduGMz#I zZpd#gk+RUdKr%}#5m{RDwJ?3H`GB%vRwJt%pU4dz>6o+}@0UnJ65iC--DDpxY=} z^lX?y8CFjK1o0s#Q38Sbp{#>;>|U;KA;Iwy#Egv@+$4e=eoA%-+W=+H$(Eh;h~yd! zvn&rrGrc(F376HoM0l5P{#OMUNiOdY3{lM9>E#98%hqizu zbr=HB8*)#&(=Mg-cCf_-ULFl>J4d^(RFHILPx)yHg!my{>o$#ubV=nukY;pD%EwF*E6Da(T&p7I>?Nvi37$R&f}Mpg118 z-t74ttzq~weTOw0T8mI?N#T@k`jiHEyPOG|-KzQ}pT*gNvkaCd^_(;;MO*u){(1n0 z1>l4P&v$5f+8W0Lxo0h8#Vma~I9MB>PjTp@f)EBf@m+}GVS+{?ED2|xgMzh|V2tTY z50d=l+7pQs`NUVu)x)>y0(^fTpB0gf;a5wf@M#`)SPkZ- zK2ELyQ-5em>YfdLBqFZlq;}|51|!^|b=AE>f%Cj!~1IUEUfAKPBp z_xoz~Cs#=XG9Ci-5}=~L4+9n53{5y9ul(SK1X$6$4w-2R_3@w3i29hy4$dA{Gk<2w0`ok>zlN?NqlN(bZ5v-W@2Zu(yAu zZuk_&xlnlAZvQx^dew?AlNfcs4%27SN=8KN7wbzs)6383<6Z(VC!5?ZzZ+AQs|APt zkdt&NH-c^hZu>%E`KBrtoKWeHzC#@#;Rl}gqw{x5U?>)ae9c`(*tn{7M-PwwQQngQ za$!Lu)uaAZaxUAqS$Gr-X;?aD8eGZ2wMHP!i;IQ$lt)_=b#z~RA?reL^=Tc`Sgc>v zVm$1W?8zpW?{oEUU$WzTd<>8cLU#^TQ&a87k1(x2e%*i2Gc(&k=%~LamjYRUvC6ea zNW$_L={$jrUd&wBfkMXj%RgX>oAxR)JR3sLRp&V7*+!2@ew$I(0KlTt(+70|sPflh zsmED&6To|sARIoJ9SDuKaNs5~=`_q+7rUl(qatkwPfx0jQ=^_FPK4Hxe(WrBCWd~y zJgWLl2ly5w7r<*kezAz6Pvx}gDK=AEDO}cExe3NZ%j&Y~qOmwCE2hIf_Y`De9DTFO z|J@!L1YdnIBr7e!X8Owb7|>sTQiXJQ24gBO>g9L(i8x#;PRzU{+jChHE}N2%YOY`r zQHXNQTdf>KG=s2+#auLX$@e&jzknKbfT8*KqbQ#rk;*;VKwUzofx;4PZ=D@Gw2uq0 zaR}3dmR{9p;n!&4oyG5Qv+}`aOk|~xXdhIus5=+^FwL}3r8eVJsm1_x#hZH31K*)u zbJ*w+Bj+_ zS$wQ5cJsNBWb}^nML2{`u4j9i!j(dJ`S28{-0l)<4Z-Nl^zcv8q|lBGUBA} zbz49bw7xQZbO5kDG6wuvb^z%ai=}dwUZ$y^8Ffa0`_*phv}iHLxpvqYOGuZcIAfju zAi(AqJd|*YvEip%I_e)xvT05Rp!uL~!MB>BnfJ*#(eBYI?BD(NrE?)G^h1Hs>V<2o zhuEXI@zn{p`*rCIc0pS%nD-qeM%uHjFKMbCs!W)GMmsw<=l93+QK7X*4*^VR{|Y^P z{E3y;QRRAcn!(9Y)R?;WR=1YHPfrkuvj!s68Y}fu>*fYbke-Z!bTiBX_&*Xs_<#xx zZCCgnz$dt;FlYO4KDSE-f}`p4#){Vl6m2D4t?Nc&4*al5(&5yY@QA2r{*uCFplAp{ zO$o*Q0RuPIJ_BW-c&6B%1)DXv2c^RhTabOmn=r_7)~Gg2;LrfRuL6oR#sL&WIRi8x zPLqhA0YH>+dO{ggm92pAx$t{idS_*5X%WB-RH}l3jlRi;*aSdDA2{(Wn*Z9pc7RLv z61bIjXg{wXBmm=kcOw2JN6$WY2c9D|>vK7C-CMH07{Pwg>Y@kTQTuN3WqcLKQ~nCloiglNhRk$eCw;D zg#22Vwg**$m?@d%lGJc=Hr(^HbW={~Ck6K>AT#&fcAbf^^#vINfYp{8R$ASIJOe(K zH50mO3r)5F%Zu|>l_~k^*>|*{EoMIT#dF1;O=3cXlM^V1#06cs}7&hzci^ZL*IPk z?D-_5K+w615WusgveN9OXZ?2j!!{*+uxUeUhQK*7e)kF z19RQUyd}dwKuVoIpnskQ(Zx5&&zWgS0&) zuhVU(yGXU>2j7db&|g&sLyP$t?-%k7FlC{5A6$RLf~@EVKQwHObe9;&n{P^~NuIofp zYhZZKPF0CfGoM723*${0NRa*tO~bn@RLmjliemr z*zzz@cZZb#cQ+P?$iF2fj>0D3ya1L4Q@_b%qvb19%0g-+i~g+bGV2Hd3+b38Z4PHk;;2uUq*2A+OWC)>akUT6!YeGn-z#!Ci)jDv3#06DrZQk)SJPx86`UaAY-cP?-0?1 zG?>qFY2gNvGpLc);QVhNA>&4waaOJP&+q}cM|vKXR1OeT-wnL>uHg0LcZoTYv9-wj8w#7k1Uz*7P+d+*86h zP)cMrf@F=Bg4sza)<`IF4M`&C;YxI$b8Dp z`HYbwg);PCs~NQKtqYqxG68a+P%VJr)CB*y0|?cCH7{V$aN6VL6Bz3|U-e!F z8~)1S!Vd)S78^x@8gB1TlCbc<&4;UT1Y~+BW}DM7P>4&|IM8qkbkkM{h&HNo#2ER; zOQY9ZqT|YMzKCK2NTGCt&1H^=J%qZf!24LPhNNzSX>Yo8hHF}Goz1cO#hfHR5gGl2 zc{u~bh*Yaut8sRBBzippw45Z$@8g$#?-n}npN~932&%SW;6nH9GY*&!!z`3=%NV`% zC{D#$FlWfHP)k#FCHvJOVc=yPyfbZ&`@m2Hr@D0*|84z`+Rv!dyEgMoeFzkJM*&g2 zLy$gQobgAj8KKrR1p1-|YQA&|WXo1SOcM~LP~8h;0_giipm!t@>-y1w!FAYM1@)i- zu0eo+&>X=@8@u=dPO~o5Acruu9F|?(Ba4LVcP#i^>cs);Y8sBOc>Q2kBt=2OFPB8v zykQ|+T(d6vS7#Bfmaf)x6|b!>p4--}st)pnm}Ac1LXI+*tMDOgd58A(q&-v5FewwX z`W1R{d<+b;rhDuTkSL|e^zms`(SjJt7M@yV#9c*C^6v+c6miCV;BilFSmrLwaAmGo zQ1E#jhl@*&S9|!>?bm7Wj4kzd zZm8baL38`Ze+Ghkj4f7~VU%EJ~l>Vg! z509&|PbB5RwhqcBC$aUja?)bQw>8U_1Xj4hBpgcy@hd*!W+KtCUHEU-x>Yf>|J30p zw%_-_e}U9kVIi_@9Q!*%6pu#bF8nqdn1;m36>ARkIt0?!-*T`O{GJE=tzlpapOq4< zzE?3XhF6I7NY!c$_xg?el{Rc)(2^%YF;lRH+h&)Cd-W>}-V5b#QFhPQg7GPUN<-Uc z<-Voin0j~8@9WZK>ej_yj~pqSlOu818<(e#2lFbv5ObH!#f=D!4FZum%)khn#FyRO zs*kP74H!h}!Fu5|^MJj%aGpYM7~e-ECi~YQXJM>stXa(}2Frv7VH3R=;gAalD$9d} zLdx5y5yRWqO_T%Hk{wP~PUTn6K@o_fr5A1}a(mlJXvmJ+Ov$nFVXh4UNY$zO8#n){ za5!>hJVDG^jFOe9*&mFnvQ-3ah3lt=Y0Wbu*Q(`efniZu&_!ry^`{^9*VpyTrx^X> z^yB9f2o2`~QH~|dJ=SolKb?~s1DwF#WWiTdDn)8|VK|xWPMXI8ym&k^!SEW2;NPVd zuL*;xY^BsB+6BuJZr)~;?(ekY@2TEX97wb7nov%?gR|RJf+}mG_$EQ*EDm65e+9mj z`Pb-VL*Qi{Sq^?)T2Oi=QuieAk&^##>(mU)&EvrYwO14eMO@l53UeMR<7KVD=%3u_ z+YW-_EP6up>Z;2BVDpXZjtTN8eFb03mRx8R?bduMXC(En(aI1|7EqYUqQ>4-@x(9R z2k+^f+6S`VW>v~wU;mFTX8WuNzjX zVg{gB#LONlke1$pE&{K~;tuCbHE^seUWw19cY%zxq=d_xIFkwyPKfRa; z2C+3Y<5xsG7OIT*`Q)Aua)Ao@Eu4f45Qz5LD<>B&9_Gd*v0MevsNtdqkSjT9mvYv4 z7i!RkqoqXJ;)>G+Dev?tVR94_S@6qFD&3m3JZO21ye}vgAeROHjIRO__Zdj{wW(@? za9^g?xglGY$NWy6>K}|;3mH>ZlL2yCzdy*S7Q4Zjh(2d2_02M6go((GduLQ!R#jkaoZL=gT7%355Ncu2 z=S?o8p}ltg>nMQ*zlj{*3uVBkSw?n43e2r$p&&&}41fI=$?P?T&2LI$GygOel%3%s zL=_u_m^k4hAdUx*Yn!o3>xYX^S~q;oSG+`=TX#u2mj0G6CjK9f6VZr7e^%ams<#6% zNWCaae)rl$3R#|L1-hHR~>Y;Q|v03;d{+BK{G^@|2Im)GEkl$ei&CyI`pvu~)O<7QuhpX|m zhn!5_3uI_=QN!tLij*M>xruNZO(8@aZ5)X}m31Le1PF`(t&ixN=cIay)$p(ze$XU} zdKx02sB%Y_uq#uWFr29%c&Cr8-@E?G7G#YsslaJ`ozXYaDT!`e%;R*pkdSU z0b69sZgNk682S|xfdQLD*{JiEAYZVZ!Cj2#uEF5-q%Yt{k9{=N}TEeB*Lp zIZrxyXUda$ZIeI4JN7EK%BU<$Y@u&o^ALax>&wA`vo-T)vSa?;8M+dNC`QW?Hjj0a z|BEzfwz+!@)}TuwjDQAcda?)F-%_wnKscLdigE)^55_OCVR8fBJcZSg|Ro({`tQc z6jP9sL`VDo7L@eEx>2Gf8O?NiBK&Xas{ci12cX9P4eBf<$&Wh_7SjvAb(pkewq(nK zNNZWD#cYyU9}5FQHRc#^I;Uv2ZAF3yw%(cmH(!n?bHEZkg)C9~;hy)aU;o5Oj?uL+ zSXo{H(Wngu<4M{UhA!zAuZ5Slbqv&TsiD#j=?ZfwIgBpK#Q!#D<2iOcu9Dcw^IKKMYe~HZDiLiTs%rU z|6&uNHyTG{UTQgzny`Tg#ueMg-MwYFuT??O1{U&UMnt`23o3kpg$H24^hXOh!Qa#F zj#9~(_&NL%c|q_MxlbamqvQtv^y@DW-l@)oFo7RT2T$$*!5+xRad2;0hY=VZ z07k!Op>sLKM#f{8yAW#Eb7Dlde^mUL1GoXpwmC9rb4}Mk`$BWCyG+c~R^rGwlNnL zl8>e)X0cHOcsl04K;nl13WbiTqa8;&VWWK$cxPrGNbC_bL-OBWbIxYY6F;y6VL-3) zr}TyoRC>;y^#JJ?5Nj*+9<)|R0H;+vsq3H$r(qDk3a8hajG?+UuG^1}3MC+wz>8!8 zfv#i{r-`cqF0dolNXZbi9Q)~``~l)MB(IQ{blSvo#Vue_;u=uIloBs`s-%|AYt8oE zMMcbw_C)1tY5S_^u`|y9&mZlY(K*A|j3Fb1J^mL||@b?Ujpe^X#=6AJ8sFSBN`TGmPhT zV2rp65m4O#z{oki#?;%|IGWBLfJArq9EO%i-lO=$ z*G?t-sy5WVGu+%gaB;`az{T|F_wnvOnhFk7^zs2jel#z|Ywm#oVB%4bC^(t7^51#B z0n|&Do+yFBF2$$Web*u8%;~G+8%RqSZ>Ig^vHMAw8CgPl2AC?~0i-Cv@*6oE8jyRo`V+W4rsKV^Q-~JE9di#dApYd~&*v;*#&o zs|?#Od7$`2)(i@k**)yOQ;K7SqX6KCpg9!U5+48@J|$C58zL>8zO8yIAl38SmX@*^=3br>(n`~{3KqE1x|g-@yF z0GQyT1x5yqaKWXg>zk8FC2-m0%Ze*1nz*_nQOB*sK{+NS@FhB=Op3B8ll`=1T3!qJ zkJ!_|oXz!R-=Z6ljro zg0OGt=0OFv@2B)0Y-CdQk)=ah@=u@^Vj-GTuecVP^*Yiya#p2uaLk0OrbHg1`lz?N z#P6V62NUHBeG7^G&aF8Wxq@YOsc=8)>tz+}JqIma0;!ltfxJ4Yz?i<_OcRvz%*8}j z>k=5(XKPNW8y@%8md0h|RX-|0VOE?dGD>$0&;0Q%#uVgw9st?sAG6K|AQ{A`w6%lm zz7ZLc&Oy%0uMKi$BeHTP)9-D{4LajjV5hYM6H!fZ$=&UtQ_dvKTKFnQLm(&k`Ed)@F1FuZrUG^;yaK}*TQG?5w==Cj) zCN6efc_Ggb`Va$^ANk5c0aS)(DsgO*h0)pjyyRI3q!rrfD=pasidd1oY9)XkC1{T- z?Pb`r%WsMR_#~5=U>LZNDZR?~xq`kHljt4%`Hb6Q0o=Y^FrTZkNKnBosq%F6SGudW z0QFO8RVVA&zbM77oB@N?jwMXRT!BoBb4`GxE&#pt3Rl~Dbl)wqAO1|UQnkkfy~hs?yNoSIPwE}MKg z0Tvkt8V)Y7>HKZ~S<()>cL4JW%n$#kS4EV00;u#b##%<4`6N(BRem{7pHYiSDUpv1809j+yswCBPF1_Nmlj?WBUHX{y>tTJFF-+JdaInbdCT@O)Vgq#7uJcWt&(U!{5xVI(v=Qd;$baTS2eD&PrZnQ_Ip_9;y(5E|^kUIhR& zo`AyC@BXrc|4t3x1Cox!lCesyg=QjtUk8r95PL|FL$fc(_C;01eoC{48^}VDO4>eq z039YBUggRBM(9n_5}Wj3%VNi_OdIjNitf8Ta6c>yly~KSjt_+W+*exbXIb>%(srVi zkIuQ)s|6!MIKIX4@XkFbUVXu)83{nng*8)(lV7`*`_){MfEr_Fm*qE53VL%K5kefQ zRE$>oYn7xap{xEuP{G}v{yUK>|0>1!%n#X8fAc%m=g#+8o@XgFcPU1 z+V5MSEiMjiF1hKa&(476`yJMW)@EwrjOD#yjgS9y4xl zMmAfD_Yyey$-n~hJW2`z+CJP~hX{R+3^?pa;Yh-1F6racSR_GW?Sx(A$qTfII?F^v z5&c=TM}rMFeffF&#=33QEqi@45jZs(VC=ZS zBJ?)tRekWuUKlI9KIBj2jx}+nKNOXha!+S+Wq>m#JA~m!GVR@r4;IXX?r&opmQ)rx zPHi&t^X6Y2%zs5c`>>i2*&^pZw+jRk&i#x2LWoom%zFUx1F*MXKyLEas&5A=X$^TKp2ogz`#Jkz~8(9g#-lw{sRRA0Y^q*gg_-kB4YZW zV24J`EFiDX@}5D^VAB5A5(qRXI4CGT=r52|*>Tj;uDsPDxXpS_sAAE2pe1rK zS>FXqMcT>v!xPgjl`X4=%PNGV3O}leVRxuX0KV4w$rp|_*8cIdEqNZf*%zjzhKR^r zwxhGgmthUdg?8vWXD?uL?vKmn)^v7Nules5sp*aj*%N1?uFoHEtcW|)hb#6FPmZu= zPPo4V3iv}x#r^!PIl3q-#iVaB6k2@!>&bDx9=R}N*s@kvyhRv5jof&DgPM|6M~oF} z^N}lB{2RPq9h=|miRp~yc?vYQk7&byBRKf1z>y1Y+QLFjjm|#G2iaH`RL#d*rJMAp zc~#|W6gB-CmpTY6<}K><-cJazczV5vR}dJ~ctp&QSPUG*%J*R#gh9gWIoBbZ17hdP zf@AK}Z32)|(<*xGbXT)GF^3%SC6xX_(#{DeOj&WA8WYTJi56bspQI3(#z=l7si|fi z`rS-AQWbC=SwyLZ>Lf|_LFAJyywSshKu*;9^QdesB6!9-0zgV~O6lQQ8p7lG- zW(-pyu(E(ByS!_h08LQFH7TawyBe5&Osc^$ITQ5d+1QN7WQtQ5GgWQBqgj?Zf|)ZJ zR{=q`D#nw}Idt&1kejzXU(^s9tpc>qtS%HPH)FU0TTc9W*rP~EGvE!!t|oNw1DM9u zDdI7Qf5!U^F((Kl4RLHU1dm4dlY3=}$DUgSveeYo&>B_I4SO~f;peOXfbnm@iz zz7v!++*UQg)2Ij^X)DDu<#h24Vo-4PF+Y@iJDw3I(3gK0+E=BfR#3gSSn?oCk`>PB zlrxk;{~-BIE1mJBIdykEexK%xis4(u0lPKmb#0f$4d$RnJD>OC)ZO$@lJyvrSfkZ@ z+Pd=Y9~FvGHX@1a!JCVy6nmK}A)h`2M(%{BB6p9Zs#`xENtqF-n1N?=W21fC-Iud{lh!Wz+en%xR%ZSgt6P)pYc1UmQ6klbQ#|y6x42x;4P}{vtQ7lX z4hfAV->MiU?G6a#MN>kM^o2B&7C1wX0WYTN)lp$+267t91w8NwEWZf3An`IJ1EbkS zTRuF-(AU39N~a9x%a($_cZeXg?z+GU?~)fARBG9SfLj!p@%n-`nNjj`gZb4w-+W(L zLLmykByb(7o_4VM@wq`a$Ll5$_qf_!rS%c-IjFHmj+WG%8uRCA!qGY1Ra@(U*KFDD zMa^K;cc?Tm9dI@IMD63{i?Xe@S&p}MAq9<+R`4-Ws~(j07#h>ePA3lt zFVr5Ts*lxm^L7u2R#xQ40LdQP3cQ8da1`r;5v(@1%LUmH_p?+FK8s*{EjIR?o=H@m z_*ylZP#p11=hMqdUe3EFYLCvRpqk|!!7beuTnT)G5qcBOAaPscuGSD`)xNC3@1K7} zim+74ArNF~kH~HXf2<%=&S$cB#9lwYms}Ea=Hk>QEGlfD5Gco#D?)$fjT-FL^--Ck zX5rv=9MA64^$^w~&#BoRA+a=86WJocL-$Nc=4_PV<{$jhe1RWZHKdXIRo0B-r`u`q zUI%eOtIH>3bOoD)HB!RPwx{D{QJ;yX?R~{`EQR>^%Bv?VL+V=9uZg|k;A)(Mie_!Z zGVxQ4Cb&wi)(c98p@@{xwdW`5A~Ftz4!5*GvrGm8}ot(Y=h~Sb)1+_lAf}_LM7Ez6W>42vdr;;+rGnkZ$DRWhXjr(qa>Ffe|ufVcF zI%+LQWn~~qNT^+qwm{}{Mc-{jsYlyTipBLcDT}DoDl#2aDWi9_hSouTAuUO9LfN6pToaDEDx7?`DH)bV z`)bDOTBa+3LQ)pb(;0_}B(BNIkoSs(^_uB77;T+;s=HJ>i3hwE!|!hEq+Y!T17l8ts2PpV?<@pQkzRArH+HgeRA zBr`)s-c%8Jrbrbf4?`HKl+LnW-E2%K9MLbU7WeclhqY!#?Z3BVX`H%ppPh5bjSu0_ z2xPfarL-ekm4Ul0Rn)N|wt+ord{FkO3-8Qd!sX-N(u@r4=$9p0rJRRM-N>(4j*D;CCA zY(K?KLe#j>46CKXHCttp7qk}^m! zVTGraxk(b#!3}CE+ivBe@)|uA8W{RzefXFd^i0|OU@L;{&e_-5`PjFtsISekue?8& zvKedr$-dVa@I)_6p_A4sSvLhW^gAl)5wy-!l zyE-J5N$#6!s(e-#h7C6rH04cvk9S*?O+`6&$hJ&JD-;As6`4<%;p~A28H>;MBvd2y47LtY8hgS)JmB z)Qv)-oAK(8ua|g|U$T!1ul%BajN8r`mv$*wwe8+P*M1gou;3k0%*#-!J(V|VAQ6`; zoKr~6m z`b-dN1?#3D5qF&DPm4%peO`Lv4?>u#T#kAT|M(c{g_8E>V!pZR$HL(G>-2y1g?A5? zqAKC%eztT$s}hQKv^Rkc86_bUEl$9uDQoMDdiW`s)+*&gwYYRqs}d7#RFk#&7YT@@ zCBL4J_n{qJhdh>>=Yo_@y*}g9$k239ifW9+7`nr}2EEw1^5)}nCZmoSzndwwN)m6VtBxUQpH_zr z`8L1PR7JQujm1_hr=Oyu2ku&Z0~RmWj7RO>{J+|>p_KEcvyx;p3>)dTLP*?l7f~@FK!f9NDf)j zoR!N+wsIhk4PKXD&@~6=y6zQ9D9K<9OE>ilEm%0rVvcT4Jd@ zW07|%oRnrU^f-1iFgk4zB)^wAaD4=p20Ld*E^)CG$)K2@glhn6%cq;1h!N-~< zV54N*v0D!wl4kavr+rj$WWsi@|ggRI%UrV0WNxEX!UJwL2R@!oBb^Q?{0~-akrdkIfPv~Q2ITR8}mSF z-LnM&<_w!pCGj?7i^PLQF-W`o$dE=c0s(-}{~zvkr!t$yqskyP>EqK$yYCiy(nn=n zvB1}X!9jI6m*9inTiHfql?;XyHGhgXFZ;P{UM^R%b$}fW+xbQJtq;aO-QkcHpZcA4 zf8@@`8z{r+)7pf>qLIpq87L?Z$%_n5!(paxQc)%pf}Mfz*Wc2zzPe1N+h~w?z@8^= zVU62^NNU*ftuiW&2aS0OP$A)dNeDV_RW!8}xYP-RAy+04Xm4>9nJZ9D+}_#53kY3M z)>Cf}bqU-pQBB<1-C(F$jVNr=8BSyt6bn0nkQ?DOD~WHzn*&kOVZ0U8O>%DuQdw)B zt|qy<<<>PB#82zFMmcOR@UPe-SU|=`eW-pE`pBD5m#Ijl!tBJV3pe+8cm)`Zf9P*& zvl0B`l=%3k*o0FT`MHl3 z6i*~k+G(Fks_1b3GVO5HIA7$fg#oZp+%D|~}7K$6|!{$K3O7F3X z`BSvb)K(!>hL9sI|1uLo)rMk?CISh`#Rl^|AUP&H5@x?>;Ptt}Bgv{gD7&No3gds$ zjWOvCrfwWR+iXfm%?l+zJTm0c@^Z|q!LGK}M@b@9p)(S*q)Tp4X=!kWKgTNR_*}oO zMC34*d}?oWDlt%mn(pNx9GFkWQEyKym__b|ACnngtd)FoZF%anUP6rKOC2a`?#4{d z$xtxA*_x2+iG2h`P6A44Qrnx@FUO^gb_tS_nr=tJ@f7C#fwN4;z`8l0d8vPnP%tM7 z+Cjr)Ak)M()y>8h_Dq$(ovc@U5>%s9^y|EJycr9Cn$=VYRV(n*rapr6^9U1Ve+#j=NFpU#9^ zcT}i}T53VFl2DeqbzHVXAnlmUkEpdl#2V+rKJI%P69EBPk@EsY?hmyog|5L>0MHT zGVA$h*k~ls-X{zM4UzlMH zQ%NN$j!bFJLQfeue?E;BvWt$CSYvx}u=O9HL^lUS)Q!?{Zc{U^*nvB&F{W&y}G}(v|9JO`ptVNiBG< zyz`$}7Vzq5+bq<1y1A2~27SAg;t?gv>YJ}g;s;wQU?doJ8d4}E)z({mxYB~5=tEsr zdM%}7z(Nf@NpaC~w=f+pJp=z~iIFt59!lNVKBJt2s6;DeePrycL==w&&S)}MFX>b|Zx}LOglCBU2 z4aDx>>4yJDEA4;H;-BhZW0I;mniq$0)dOjp|JF!)@ufaG!Xll2KK3$_+3azekFn_E zFAzT#QofPC{T}A^N5Az<<)UWLCNgO*YJB z4?j)I0s;FHWU5hjUV8-|qN?#l-X0(Af0>v1cID)AS~3Jl1ub=PR1o?dZ_yrF#=%Hq z;?ck%YY{Of&=b}x#|Z@_Us{*zu=1!}3Z|P_)Tx*DXy!Uqq3iX;QkvD< zl-^9=O<@h~pfiltQ8tfIz~{17f_(LC$Tle`ApvLF$jQA>iavkXkSLX8MjWZk(zj(N|JTivse9e_i(&j9fp$H zQ_iFxlU{WeW9gmpQa=H0-%vO(Pfu#W3SswBK49u%&|)eUusKoXAF!DQr^<}i*!2q} z=AP14)p(!lo>QOgXf0iV)kYy8J)g6i*cUbGlc=hD-=cM_a-l@$=Gevpf|53~kScH5 zQbJ?kLu#;$Y9X#-#&>xFC|(hJq;f+esG=$7g2~TG$G0VjAqWJ?_2i0*TK2bb$|^upxUDnY)Cf> zss=Okw00L#d>CW}tfh;#Olj>It3raXp~s8A?Iw_c677` zQqZ!XmNnRse5?Qz)QAkvOe$xxc`rHc(S)QayDh%BaLt8$u&O3kHhuV0c@Hegn4Rl}y=R<~5&B`I3Tk+O9yQv*}EEVO%8C1@f+ zwVypJ(hxttSLL|(d|?}_BT8)%6g^3FCGK@{?F}Hfr@3vEYp8mgB-P$lmN2&{lS$X$ zQXfmHYwx}?I4Hf|0cO#CFa3VbP{IY z#if^}uvT92*Y$oSJ#7=AwBPmJDd+5YQ<%kvfQWB9XRxd}WObqY@L-I1(Z$K1-e2CI z+J--^s6ei}$(T+eQHkEK^Qm1U&(PvcJoTBhPsQEucC+t@ zBY(X(0xj&SoOI9u0#cj0+R5`Ml~mdu<2p zO42s%(9xfUi#VC{nW5xWa04!8AunJ?9D0lQA5q?iv9(&^5sZ*r{$vh<^Q z7_QRWm^oMHWtiQt8gvG4qfG&(j)DdQF!kAcps8PSo6>3(y-WN^=?z=w%K=I-cppdG z;xb#535rVMMukZ#Ho&m69Z2*dDiii@1@D&AcbkQO4YE{1G_J2r@CYeWzVr~SuuTQL zj{p}c+V={d9HnSBuwW%M!I5e3E>&-`*RfVs&q5u^V}~FQaEB^N%-jqsb;Lb(`wJg> z?8cx?5;#&>s^YFn`z1NcEHd@f7U<12b(W@O512C>Wvj7+5o}pK=!RwLlXq9&sRmj_ z&BPC*p(mpitEf=?0@3$uT~XOe2p>3-j7~8N4P8tpnpB#osMPc43`W|*%7t$XcYpQ% z5J8|Ih?~f1L#3Y2bQTmm`qk+qrI(dt{GBQjTLA$LLg~>u8)>GhhbG0VICmnI{f})Q zTFfka2OCsUHOduyJDje(Nf}9*=$Rg2(fbED2Hb2UCE6m%QRF=9enbKIO$yS}^kVb#`C%c>%kDTR}0 zVW!wKi9ToQ$_?5#&gh)3d4KnfU-P{lwtj&i?v;lhUC%A&qtTNx zQ>nD*AP^6*V%oS!jGR(~A$t1%kRv$C+BrE+#+C&^crZJN;*g3W)i{$HW{+Gt!X>#l zz7{kqj!ULhiN@5oL*PUG#_Q=GOVd4 z91Imk>6H{Vh=bL#NcT-5_w)ob-I}8@#~zDXxVd%*;(ft8H6DqhP_{!=J>sq+w_RF; zVR*QHPNp926ppAE4PfGeM+aOX@DF^H^*9k;NTM!@9z^FQN@%joar58yy}ZB|>KAf< z;+$Dc+4#A0`Rr}U%1&_v@P~8-XR2?cQZiH7H4Gpvx3_T3SpIKEC-TqHiE6Xbp!G&y z=cn;|q2DLJZf>~0PUZaaY1GR#A%|Utn)Qs)x8&MB_b-8q$AUI?wj=dqb6w53GNcg- zdnOAV(p{yn1V`l=uFmSpf3$_j?|;}rG&k^SaIaT;2e_JBgIgWD(UPPj$?*yqvvDYE zUUPZ1S~&Yr)XBr+M*KtKhJRMQGaPt+n;_ZovSdb z4<#iNdIoq9zqoj zi#`jr|0|~)3T@kmgq=eT(F{xga~pZ@MECu3=!92D+Eu?Z@-_H0B2Mxbp(aZ$`B9$^ z;lT%sta!c0m>jWLMNhY*Ao9Z5%eD`R_$Z@)Px2TZR*ko5v@3q$k4_Il<>g6aH+g>_ zzVB;qm-V6VuTI`5#-G=bYBHW&!eTis8or-aO}D4C=>4Q&rO@qj|KNCk zoYhqZo@5+9jFF%3)EIpab02%N@VS9Kr7n$k@r7Vaj$m%D496PAx*o1nPy_^*fZ@*} zVWe$B0)*LG3n-M4yK&NsM2$-wesBg)@li$NViYBPum65N%`i-UICETFn}5TX)B2ou z0I)A_hvv>MKyt)V)#Dn~#>l000)_|s=OsNU4P9*y80z4F+qh3k#UGDn%36%uN=F!8 zo8nTC(en`**laLnYO>BkN+o@K>FlE}fA(3>Qi3&+f#1D= zq>PRz=F7xfHS|*=Q*}4_yOo2mRkuk5q4cqIg4xr>mL^5OB6l@kDW8lhS+|m$V2jf* z%OSD3uosqIy+jvv@(ToNuQ>epT4|5_kI8dScPmZfnpL^;tiZHAOY)&~5Zb!GRB76m z?fK}eGpxKS$7oP*cK5HQ3olvOZOXVtvYDVZQ#_|%D?6rlDFH6r?(x)EfBHmO1u&q!K9>E ziS`Z6I;1-#X_N4hxXAw-G~z$fwK2f3IWTTd&MNugY0!jhN$Tn>yj-xMCoa$%mFPyX zVqw5u6L(m)>^M~FL#{J@UXyp|s2E|%vy5)Gn_k;z><+=LD1+ytH@HK^)a*>PTVpZ< zgFnQ__K^A_T9uk{8vLa5Zc6T79)T9AaW1`B>xf8A={~3~nc$JvFwMf6eN=KU8dUeJ zLI6EE?^Bk)iH$vRqwGm6r14S~9bj(7Kc{dJ32*jT#)q5>@XJ-idj9*I|F8Q%Bxkga zcBD2VbACx?Hfk?Z!N?tl_rqP6^LN3Bm7LaH>Q<}%J{eDy;EK>>lw~EdCrEWy?FDxc ze%I(50G->ewn@S}j51YULjzcoWN^we#p!KyT*sM1d^*BNLOQ-_@1suc+T%|LvwkPl z1mx?$xiOqM1xSj^^XRsQM|j57--8y_52jg_1cTmvb@2s zUYFj{ecq$4_mzNr1R0Gju~`Y<{TLfROiX!xBtGUuzuw|@phh%Z+)T&r_Gg{BYF!&< z*Bd4j#VSgSfbZSr^5x?5?v4oBb^*;#l1~ZUXCsx%gNjhK?b?^;8QbU`79m99>H3U!DyO8 zog+0J&FsoYAK{2K1Nb2ccKd=Y3Bbl6cKZPbxOAC1X5Hw;r~!4a zRHg=^-#us9;A-gloc5#Kh3rA`S=9d=HPK)1g=Xi`uf_W}Q5UpkiSsAM}XmOnRoj*_r zTptmT8#=BV(u_N;%LP2ninPU{lCyv(rpyF@}J(3Py? zocL7zjT)Hr56Ahoc!U`1Tm+kDlV_-6!9poxx1bG89QWm)j7)Nm+*D>>{ex!neK6J- zq=iuQ=qTMWjcOBWDIa0)_CGoS*He z>+0L3Y5w5o8)|^bk-B*1cAG2Ym(RO1BWT+PXkJ^BjE};N8fm8sn{AOFZ4B-L9e$D2 zagR9{%%r+~ZkYo^vGR!qY%;f1MI~}@=4Y@7|p%qRPkRRl=DhXF9%S|c%=*h zLrd?KiEejS7%A`V?6&Wt{`fMsT#l6~OSZuLD3Ru(Nvd{ko@p8hArGj))wW*r=sBLX{6q7421|iKfrDP*9s%s*d7fJ07<RdX;xMi$H46I-B7UzCfo!2{#snZc>q#Gp4!6NOcW(u8W%kH+Gur9N(P+tiFK?B9 zjEs%cW<3n5I1?xR&cQry*w}chs`Su9@Lb7T^hRLplJA;kfL#jFlbU95cx|Po))(xq z$$2hU7gFISe%>h<0fb2bF^0gf3xm5EFiQZut0%~W0(9!Dt?*pNcBs2_Y?~}PgtEt5 zjXGws>wZXBwdvNpv@9NOIQ#y>#z}ervpa}g501@TtbKoash}y61P|8B<=ZW|io0CTalg$lz zx!z4TV5;3wp_f7NZC`B2csLZ-x|Sv#5{}9GZlV?+U2kVdh(ViEOg7RK&J4$@X$~8r zFwelhX41Shy=?w2p#d9&!{9+e%tLonayDT{0@j_gNm~q;*l_d!xIczfaweTf;S> zT-|{stoR8pMEQ!O(=ySuBdHcV;|x>fZV#ZerCJOgVSc=8HX{#F_Hm1T=`ZG}(#q>J zE-LP$hHp*UUDqsC#qn^7#xv&`KwF|W2G3lRn39b3`voF#4?9n~zbhGz2pir5VC|m( zEmdi>W`F9-aIkoEdMrQ94Q8@%@b)@tu9N5AfY&vEA9blUEu(7OP4u$f`x@!~6MHXI z!hK%Z5yUkl8WFR9TJiw4og+6`xYWyZ{?Dji;{K<(LA1ua5~XzCHkfW z?TJYd@s-OB`?;HUOBNdJFp9-wp)d-A*!!x)DsG#D^COOmV|kCONjf%H*aXH>8cLQ9 zHnIj5CT#;{#&@N~dwacOgUoCj3f4ihN1q)hVf!bOlLGO#-}ft{N*>KyX!{g!9Hl24 zSX5%#pDz7WF1b$09nq*O1=UJ)9hxYIe9NOQWqaC%Zm`R=| z35%s5`scIWpLjOtf2#l@9A>x7o3`W5HXtLhS)(??1GLb?IP~8bNMJ-A7Eg<5x@&dE z1GC}DHTK4$z`mVjQcslH7!V_h*;>CWm*O0SQomZ5gCKtf_WQ{9jEF8{yHW@ zqe7z}M7v-D<`;l31hFtj8Yx-R7^!D#NOOpvXcln(tu+F*=WflpPLt@@ z&Fcrsx1oMFgtq9zODA|jTR?BtfQx%EQE!;cx{~Eb&)B;di29jV3W*dHKM7`jwR>Wx z;)wr|wJ^Z{Iw#3tGM)l~IRK8fP4!=oz$D1BkJ(rxub$9{>si>L8OuyJvQpC>}le(?|X02>X5`S`lg0TIJa#LjiB|3M{dE&YLZjHQGptIZMNle@RC zSl9s$`}AB+?$+>L?Qqrlv-%v*U1CAy-ld9+Y0xwd^?c0SxA@mPeI0KID|NcLSY+)i z;SSqxMP46pAFsB)yl(El=;vzug-B| z3;=S3O*LjY$J$$KE3!*JTjKW^7U3HOOt)1;X(arN`xK{vc4aMWu>@SR~t$te+hF7xB<_<2{@26}RpB!Gs^|jRpB< z|Bee&@r)#|-7CHyS64D6-u*Is(OWI&h3!$cV+2h;F!qtx6& z+ZctO1}?{jui-E}+&Gzc4Wh<^xhVe&AiY^NK9@FUaG`%VLFf}3hlx?`izkjX zy0zMAz^UTv+D19_a+!AWCJ*b3+r2sB&N)q5*;f-x!|38+dao9NLPbI2kx?Pe3|r=p zGl@1JvD ze_wCxZIzrHdYQ+rjtNZHBwj<&qjC9C%btp|lDJQpPi(us7RTOJRG}Op(!j^l?O^Ap4AooLoPT38x3RhiN{u56^7JhF_+(rJG+tRpqfyAHWOHHZKtAx5#?8Eamj;;AwmKc8ka9<;q&nj#m0NSMs)tJ zt0gQj>?A}T=3Dz+6QW?ljMezZPD>yy^lgVkFneIkN5_W zAY;mVS#$*xvl)iAZy#HWYh>xD^orC!ZL;VbuJ-d(KWJ&Uq&9Euz4fXU+YG9bhJw9o zyy!IM|Lhw4ry{AYKGo8=GZBuz-zA#f_TmrHm*-wSpW_K>geJmFkceUoJ30Zj8S`OO zxe&8B%4A9KWm~Z(3hItVVu)S?^L zh0<@$p3Z^C^j0<3hU~8Rz+$MMa}(LCWKQCA;vOA0Q|nf4jx7+!;Oyd_e*uhSXZH@M zN$?AZ$xxd}oNS~sJyI?Ibyx>h)EMv!mw;JggpnL}WUc{8uG1{0l)-LWRR+@en?bzKwmlf++;#J%;Q z^?iza0oix+tX91A&9q4w2rX2S=z+WfFJ9K~wH{quDGLrUuiEtD^ggawbyYS2%O~E& zw8gYi%(jd|OqK%_f_PGN6sN5eb!!?r03JiiXq;E-sV8=j>Ms4#g4$n61`5 zqte=`+N1N;fuuM+BGLxWh?Yo@c+lhVUd$e%?J!4y1pE8T;52KMeP!2UqU(OgFSOhF zw<0pQE@G=k(b4Xn&K0?fWASA&K`=2$sf_)u9(C**$wonnZ{#8M7C1kVNv&s#UeUaJ zDKjPxF&!o+X6zp|J$&a_wM$>fsqxPJznw_#rmjR(TM`=~NTbeFtWb(i6YRRR2#S`zVEE8;`KwBy^yRV6H|RC9sLb%Z0%EU3T^JE6X zs|9&VcUtmEh|{YVEA2|4Xs*%4!*Cx#tz<(zsliCsNV!lCsZ=4BCxYx#!|C~)In&Y> zG%bXBT*h7#+!YGrWuHr05OUI9JGozZ55-m(sd?<33S?r-M51%zw9Bo<`?|Ze2XpdA zj&gL+)fN<;wBt%w;-jec;$tz1N4_x&SQ^0N%4Cvsr+YLF33q2_JBzP1I}Q7lo6c_= zUkx{mD7$+b1W`MF({(bC@0ZQK|7`Z#)+*)g;T#MnP*3~q&?snZ$GIh4lU}NV^S-og zBrKI#fnYNS5>&U=&*4-&K$f*wURDjcxk%&-q2~aI%ay4d3oBuQcS&G)lv$`3B68Wc zLPI~$pcEa$l>EtEpwaEJJW;Z}yMsW4tXiolw`@wR^<$5mWVIu8e54FbT9{b5us>b! z4FVgpH;j8r?+UUUEB(Xid5eBd9Q?MvM7qg48Cpz8Bm6ik`VB!tiiom@-yMp6F3q`5S3v}{7sY$^jVr`;IYI$RWC|~ z@%=t|bZ6#nj2g4;5cKM5b2E66)lETPb%xeJDw}C>M-_i+k9yV9v+VXl{hPnztR!-j zc-jPxEci0rDGBCNYCq5>v?7eBB8`MLENB+fY{b27f>~3n_}E0$49(1g=t7IC?F1(8 z^4D$&t2or%XcNiyrOtfTFj#!k@so%QqSfChxQ4x}cd-IIptvve7Pi}I#>ZsA4E5l{ zq#X3W)ADifJCVykSTm$f3MT&EOk}-J*LpZDRTf1Wu*X)+D!E#bbLfy}aB~PBtc1ty zK6KUo{GxnDtve2%)3w5PT>WEv(El*7Be7glJp)OO#C-h*nO@WdjDp7|uS{3-2^K-sF1i-41L$AHH*I%0(_JipENw zQGH&jPO!rH^^!(B)-U%QW8ni5(&7b_F zvY31}o83PW27@D3%#_F%9~wI#W@ZwkKt!nuaU9kmLBa!_7Jp^t>QZBn#w#S8t|M*e zr2gY&M5RDk&UQtZ=+sDa-d65;>mlVo_d{!6pjem?_l@A0f|b>bm@GU&`wPYYvMb*` zAcG&88B6)V4~>Oi5oqTp?H?LJ#)nmikJ%1991$1qI{WDI$-s=z)^I<(V5yLTglJb? zc)d4sN0#fDpE)vt=*dTueUr{zx&IPeU}9G$AdSH=dQ4z@JaU$r7Co zWXmvH$)s2pyf|nHt0MIxM4^GyPIDmbMx2D#IqGlT@ue%i%!JuX)P)z-^k8EvQIIU~ z{Xir7b7Mcn{O#EI6pzj)ONX9KSEs({(PP_3s4niWsPHAyEDiT-(qrpkdl8kb5u_W) zxO+C%e~P!>{34wT1&lr zh!z1ecwl-ggMVSB_59(Jv%MFIyXOC8z)K@Yo7*PvI|NJMZdx0qCXC1t)_y7#lP}L6FAuN5RAM) zNnXZ_jeyFL&#&;I$%G%uop7ZoJCwid8JQ;a# zSLyJcN~l9#vRPdm*t-ACmgfJPoQ2=fQCCVDc(Op`mr(0;aspcdCmE4u>`3iGA?lJZ zIoseZy9CM)U#=2|4St7_^dzbdN;E>ljdh zxpI!i1~GMeFovR&)UIEBQTEHFY^S4L?J0-e+Em8pHRDg@Qv%R|7zKgwFSE40E~(r*o6Z2vlcovby^6W*Jykt?>TGO(j)Fz zQ5>}OdUlJMkmgu(F-URl6Q4HNBoHY=H66MEenhp2CsufxIw4?G*(iyIDjT0B$X&S& z!=^&FP<`b4rU(jItqu3F7FBFS8x54*i}!NfvcVr-koKZoKOqwZRtI zB;Kc>ZmmKc`ZRs&Tf3dj(f!}3tHJw3)i{YqJ$_hUi4qNdZ|u9;KtaxHI5DZRTgZD}wJ=9I%kKA@3}P zG}jb(aoV@ktp$!Sb&ymV#i5J1hjOkl!oO&gU|eE0eM1v9nxjst`%@P!Fz4_>>X8R7 zJgVN(_qMAIp^~-4G>qK$oH-s?k(s6UnLPZQ*ylFLNw=?m;6a_s2JM#f80MvAdGWqY zcb4`6HKfEgA0;YsjsHv+in8unp;Mop&(q11Ra+6h=yptu>+KHz; z3NZ=KMY6)DlUC~@?mcUiS-uheSqxh4N+@`!mUn#1782K`n6vKZ3^K5fz{gJz=IF4! zIH(wLq7hrGNeNpaLAgY1eMh-OOiu)`Y}r+sFO^gto5A1?(E}vD%N97oU!6I?uNdgd z=IrTI7T8kx9UYRd{R@KGi%|Xr)}I5Vz4ya@Psj)YTy*oYwc=uzBMbbeZGB<3t|_vm zB)Udvwda|b0)={cQ&I=uFHvG|jO{R=u+@wr<_p}WPk$26eo-MuMk?)S;bpzL5V+lZ z*b1nSQ7cSQulxQMeYS?bzry6mLb|h?3Rb+FpZrEriq@m1{DJjs&$doAz{*tO{l(mLwB=)Hd7g-8fw{B+!#N%FSC6S6 z`_bGP49xX?R(jjg+q#$HLnd+K<* zwPN07h4G0fk1|R<_<^<@UE^lYcWUa!{lK!9QlRJ=ve*6#WWG;n=ECDTt^C=-7Iv_= zm;oaZ91ox|GY`{FJ#)JV_rNv8>|eEsVDc)upC0X~h?JiB{GC?tX-S~0Wq4zIs+oT{ z=k8XTID2mf>RGtGM$2xXpWgT_3LNxv$`(h%gZ{Rk{Us9?k{LnOfnn7yeNB#lLQHg# zEW=kF8mJgvtu-rdjOFs&tH&60B&lA+G`uxeuO{e3!GpC({1@B2?)&JTg*?=NG-sy% z8^ugL0mp1w_HwiC=cl)OlY`0Jl(YTP9?2J0vt5-Ia_1=u;zx|teN7F!`R;w8vrp;n z?8Upk9^A3if~fK!eahw-j49$u5vCd$Nq>Z0JCm##Jl}ZcEfA`^#-}&mhljPsAjHOh zf?>ea;N`c{uX3h5g^g})`5w&CYneTTEH7b|UkqM~O7q?-Dc>~A!RjWorzoq8N}ah8oo`uUzq3=S-MyK2KlN~iTNY^8&~wDS zU8YG>b)*k*UdluXg!rnl@F_k<{JC}p2rHj;m9xouS?0bze(U!7Kx@dmw`9tQPjWh4 zomGGL#@V~ef7YZF6}qH;u8RX_pQeFlp>6>Id-wlhI?wl&LB9<~B6_B>h%Qgq+bUnH zEIpWuieI!e;GI=PM!0H%7nHF#72J&zG^T1c6^^tNLLRv$l@jhtiuy<@ ze1Y1duXv@KUC=tOXKQWUSw`}O#=}PL^svVUmbgz4YJa|iwaE3?yzI*xxkz6rDNh%; zoEGs%nMu4nHwYK7DX(=>Pt1C9pFYVPb%1g%e4_ zk9l3~Y-|v{fu(kJwP}MHF?EQvGc1P}vIlsZ9bv0v*UT}2QWP1?@tO>_)e9H)n2s8B zxlzmZKmE7%$YXw!3UB9g0ZMvG2 zE_W#^jN5#Ew2o9eY?DiiB&vpGdp{7IkJC`YqFcZ9_eKRSuUfi1uU@o0Rk(s#YAhZT zj0%6!RL&wv7eYXfHhVgF?dfAME!<0hNXyY|EFPBL@A8ys+0Y>zXk1H{=uH<#s^L)8>>2>MMIOTk zvEX8gtVjRcP61`c_zC0j=rX;Q<#A5AiN~(?|1?ax7%-EI2tNy~3HKS&MtZ0%FJRD2 z%E-$1SaQ~^6E*sRrPpsB%FBTQVD_0qd`e6E5zgMQ*m>4pH&M*HBd(>U7lzy# zPvg}>&~$9=ATwmefM?NwPEx3dx93^>7hcofia$9Mj7`9L3z*XoYmZv(S}R? z93b*K7VtJE=?0Bq5#yS`T_@Sgt2tmgzF9dU6?J z2Eoru%}@SpuR{H9j3V>cMy{5RYf3A*&4tFK;D$bqJQq?Db;N8QN@1;La6FtYoU|>}!xI zn3*~zabh6F&_*@Q3~D?WKhL^`mJSgXy3^7use{-X)_J&fpS}=FNDK2&81O5)d5Q zN%tTAAgkDzE8>q2j7xYHJ`Dmty?Ij5{%S*Xc?xaI*j-a)d8gMKYY0Ds;7s`9>*ORmzMM_S0S-Sps#B`fUQj!`cq8$^TToFcRH zUsF%6I}d8)GJ^6qh}SxN&N_R#{+IRIC#7B*4LvBkf{G)tv!)s%=sc7)!>-eUV1 zQWQ63P5&C`JY*gAzyUi~da@9p>B&mc;qgl*@~3>9v{Y$HQowfv^CAexV{9~OMik_4 zMb%a}2(+2Zp-Eh%8c9~r0^u}Tt3(pNKnx@*U}+aa=G=42GLfpa2}G8i<*bt$EDl2q zX64&&`B9p+%7ddVSw|Dv+5k@8;IzEMCi6Lv2j}d?Be9?au9F)g#BL_RE46>^Ka>nh ze(TIX9qk-|adEa!7^hj7*Eq8?3W0x~>8#o2!nQxn|M<8v@7yZxp!yj>6b*Z@Veik6 zDNZU{d|{LH;K!p!Zu50~RAL5@y6Xm6ZbNQYOJ?BlRIvyvQrB6gi%W6}wq&5vA+RN* zvj*tu;#LqPJF9$5E_Y>G@wRxY`d6Pa^rsD7Oqxxud~TqEj~4$YO|En)+eS z7zcOYX5coD1E=|GgFXq=LaUe8id(7CnECUgnw(M+eU3sm!@Z~8bNal}3ICkN_fZ{# zq>p!cJUxA{I|eIW@=QE>@mh8x4`7b$(hpm9exRxSW~=wdYF01R*o#c)9u40bg(h6Diwc!1{u(~)j$yS50ii^N0~4P?L)GCBD*izOOH^bs0LBW5kfAi7MI~&ALx5Rt@nCxuV^InCA zYUy+2Bz;}lOpQG_PWoYJvME9V=JvVV}Um)16!1TA4(Eo z#6x0)UPfD<^bq^s6Cr)(&Xn9>OTo>-GtZ!U<4Bhfqx=y)WwF?NE*i-vU$NGfrb4%6 zyN~aiT8qePt@({L5fbd>5=HCVyIq>7yJ+6w6?RZdOQ|g|suCop>?F~!;$dpx$l4VP zEE(M6qS0iSyNR(_8q<-w>mFU7)7s?NQOM1^v?rauEg}&yk6LQd2Tt z8z z3tX8rk1LsnhDt3-yE9hj-z-s(^gkcyYt-tD?uxiKQ~AMmgZHlGuXESZMxi&b$G)aE zX)q<`0+oW_sz9aSxUbq{84r_PA~T*@z&}-h0q(+g{+yT^hA)$-c{p!&Kc!yT&OH~E zqBR*MP^FzF^vlxTDU4zn`1w^W@uSY626nISah@z$~W>G_qrAJ9?C?r^d(By@p?g?1`I6QeDXV-fil}P zHhM<8D^8D1Jl9%O2`ov|K^X5vWZMxa;hBnCB%=iJ_VhAIH{LD&I-at9iPVT07c5g^ z`Yd03O1+u7aa)Lxe%rW8$;A25XUP4=Hqb>ai!?k;u6vj&qo83K{fP$te1QCHwfD=y!gxiD z->pfxwDT6<7(gOy796c$8jhSmt~9xdkD<{KQ*irXkQ<&_@K9|v&2(H0Di@j;4lNx+ zFZ9Z|IrMG^4&Zkho4s=_s1k|u4;O>7QLn$#^2!@5v$EHu($j*pHdqf)5HZv)h8AKb zLu^(&(+SdnvaBSpH7U!EtM7W8%^uZuS%1(9jxEVdfDs5T?qEx9vd~jzY`AAPNpb45 z)E=UAWdF_p7YWYJRc!YCESB`sr$-*BHYXlzRXl?R5QtO84PH7L#}v078T>o%bs>*} zz_fKACbIgow#;<$fB5=9%?EyPFL6Kx$uDN6jxQ*L~f;72kgoY!BtN4dT*jOhusvd@NR-jvU(@ z9DuUH8D9^)`$f^r(B2yJ?N{~r!HHY;^q(c!y$`K98NT-s8F-Z@+NHi`H?tq}FX~Fw zf9$`whcox)uCdbvm4TVY53~S)Z}}NUA%&tAo|u2D#l35ms_Qw>E<(*3>PAqs^D={=8%uabz;Ip zEMgM$s5=FfkvqIjy?3A*Q-!NTqt7{G-^Lj?qLt=|U+wL>demtPid5vK;o2X?@YIbK z`hNJxqg|EQrA;mf@1%Vm+1QNatNUV~F*s=Z1tanO&t2;`7hYHM;y7$Y713ej5CLZQ zUHE>7f+!{X&}}%;D65`ej!33#SI2M&n|v(SW6S=B8bTaH{xn+lb!nwAmpPx8%lWne zW6~_W$BE9k&lOi!b|oKL*kbwC-;+-=5X1$%v28{~M0{V5OYT)bwC%PVXJ85k=o9X^ zCo=<;6(*{zG{Si1BmPyEj6;>vX$axaPh_h#$*SF`lit~|V18l}0m?AUZ90@*`@;Q# zA+Idb&2Bc+eq$u|{mgZ~Me7=rK<#uxgv`x6U|Fue$|o!)mosSo0c+tj`PU3()`VN$ zLe&f4LBL?T;d&5pfiQ30aC6}o^Lf{M>PDZDCiQWr1M^4!x|#11oUz^5a^VGOS@h@V zejP0%VQR@LlGfBroWesZmwdhdG!@H81l!j{`=w_MXLEJxBB=4SFWS^n2txem)QFOG zi%@=U5qVinzx(mbYUO;zU?Ai}BCf?jWfP5+sFVI#b1{<0XEp*uC@6joGy#(fB8$`Y_ zy1A39oA+-uF6@rS3NT%3PX@oQt2|v^{J?Wjc)6hP1C0UYF+Akf{R3@=^V?qI@&A_J z6XukSu&47r5;xS_;*C?-LNkOq*TN}TDjwLg9~Z>K^Dm{b|EKrxOt(U8l1I&Qn1J^& z7E`-iA>NfL;AHy};hgJ&m&v8Vq+j{}w-MIZ7nr7ZWzXuKk13{|dDvN}h{Akbo$7jY2jkC*MRvRPQ-1Ht?~TJdGcxsPaP{h>(F)PX zn+a%hx_T2vHDzv9mbwuSb?tG8l#bxH=Ap3(5x#6WuX zK@tGZdkfC&({Y5_r|_=7&{?Kjs77U)=y{`RP5(4NQ6r7J}}qxSU}QucGt+m$BTX% z(&aTRjH5P&yeYNi!P9Txaek^#3ypq}H7`iVM^o@uVajO`CJs$Go%RSrwc z`(G^-v*?7-^0>V-?S^R|16_y6;FzWqkg@#ZzB9RS#{dXyo{kR&Py_N)Y532Iw zKRePMzh4Oca*AV;JCm1^AaW@-Zl9h_qp|%YOihoF-U{$>tyW3I*z~co8bi2$l=yot zvG!S~2CG=@A!m7}N>Z*~tDBb)w!gPLGY-ZB3&J?+FNgfK$58GKY_IH6+)73|}BPl^;>dlaFvbh=CMG!w)>OZ52IZ<>J5(s|xt zNZXO|3H3NskjCWkj=M;3>3_?F{1!1|!TiKSIi)9vRng!ZH7YmVG9bh$^vrRwMpGT^ zY^X|p>a=PYToLWjI+M5^`haz02RVf4uL_VzJf}ZvuqywIs$k@px;3;V=e}dGn*qSl zk6kG)1(+f@>?B#t5Ag%- z0)ADb`i2VS0m?)%+^!)jVO_;``R!72)O%?`Lqo1JU}dQ%StC01si}%%*c<-fdmX5g zPF_YXXS8&1F9hjONC~LRGez_Z%*gx_2L6(18$u1CUy#vJ5!QjRs`LWhCY=2UoW9Qc z-VrG&p*MRBJ>NB}i&QV4Qu*TFe-{rXzNZsypu_a&4TqFgrGUYc#s{WQ-se5NRW^Wa z1{5Q2W%=}PU|D%hZZ?CeeMUKT+8dcm9^q~slpo?0J|>5g^Pz6*kr&uvbi0%)V1bL> zx4!i_IPh-Ud2JFHiTyBZPb{>hRyStOu|b$)$33_DzS3#Hl_z+}5yi{-R0rEX>w!b2 zGM2zFbYb3SL5iQM=KGeI6su$fg%*GHiD=#+#vKI=52rl+w&4j$)~#VS!H|jxAQS#x zIIbyPTEfF$0xMZ`{fb8PES+`a1d9BDgkbwN`R<%9d!Noi%}Q`JEnQ2 z_k1hc7u<1IbX2A)IPMbZCr*aTW%+ccjVFHA){+aQE`2pz72~d!k{GExFn+&zDPX9;%TMp2q-4MfF97r^@CuB_&^QJSEhA zS4__cCcFQx2~`rIm=U+dMw}Vt^DEjp05tmA!D|JOE_nB1ZEY73O0!^}zCD9~nN@LK zHn_!|Dn2#ToM)MPp1NdHSVbyj7S&bm?8DYlRoJ=XRWS=}ZjEDY(Qzn$=Vo~iVErQCNb2ymFIQmbAWlhex$-4tNO zLvzf&2>j~?h**j+jD2^T#aaTWkEMdFo!(kUYAZi8YrD5EKQS8Hje4KMc)<%Nx^*$_~gax3-BCc&(09BQv+*6jEp$E z3H{`3$!HS2Z3UsnBt|njKRn;Jo-LKxz&du`y3UH)jdv3B-Zpcb3#kyeZKkfDuO*JJ zzmK>qC_eA`>yeBUy6I|Gp1o*U09uncAt8yg?r9&>;zS>z+-1!#=MEpnUj~<-l;=#E zss=Hu45#A|Q%~_;%X-sYWAu%fb))09urZ?R1kVcQ&c%QU4(qWW(M7SI`R5PC!fiHJ}i8rjeOzlicd@Ttj1%eBsme)48$>t7U~=<=S{y5RUeS@)kmUGqOkW*^bPm7(vwd+1+5Zi zHCos$deicj0hnC-M^^vIh8x^G^s$8s_H>>!_r9HWUu@YkrTuQI$hz3AM{@Id`bUYM z-vr2i%9@1)A9(S;Q)AS_3MfL#^OFLa%=1{vr@D0pJn;W0jqgqJ`H{^x-1~V|Job4V zX@G%}VD5)ZzBJ)|aKl8lIK&LUl|=;6Hkz_F#|n(nQ@A;ukJG`N-d3AVoqKyO*Nm{7zdoVcyy_GZN*^poalHU5OtatCsga+a5)DmodjDTsLe97wCYiN0yoy9MLvzogO~#kX2! zno|<#(qiT&P%|bN-SBwxZc}3|{Rq0W1{a=nXGm-vC^(`hp;8G&(i}_aZZy4f5$EDk zRi%U0u#nmHHl%odwk68W8=yion3aUv-;{p&?Jax!5`ZKV=p}Q>CHR0BlF9JEfSfkEhsVQU1-s z!w~>IYV@VX!cLjtDOSMcdh2XpF?@?u3mY3Ida;Tpyc$_R`M}Kid4w^+-j#ot!y_jG zG=bWhp*-=><2}cwZaG}r)a9v;^p?3ZCF7gkn3vaEKuO8>#Pm5j>%I0 zl)qOQHWuk0ZjWgC&*Q$#P{vm7XM-f$49qXUF&RdaJ2&@#&YqU^N!p+gHM$o5ltOtL zEm^0aqmP^`*vI#yjIeOhf)M}(==btQmZv?HLHA3S68v7;0 z_4~SB4z8$=5h}I#U~>16Lf9$C_@nL_4KJ!MG|xj_b`HsS6v%$ayo67z2xtzEv`#Pc zjD$m2ct$SejaY7J_Pq_fqP$tk8X0+@7F(f|~cxWd-L=amh7y=_mjeQvQ1I>s+4M*05XrhA|#7j=8dHV<2Efiuv zdF2Uw#zEpp04A+=I3_kHCp8nRO@(bISH_}>Aqn}b0X2!r@Aknnf9Y-m!DcE#J^Yj8 zcORnG?1);nNcA79?Q&rNdZd7>&x;KoXjqY;~qxGDFmfzhjJBv**bZ3yIAK`~VlvZx zfb_(VbW(b-mSSTC3VXfO!J$F_W(v=h0-v?jN|${T?<)eZ)AZ!d*Lg{&+&!@KlFE~Q zB{cVRJlY9GPWSU!uqg_3H-yWkLo63M;FpHF>1o+DBWem#7*)GwOPjsrDsso(!=KR* zHzo=E(KF_l?^;*#8EDGag#9L{_-kIRja{&m?E9S|)`c>$D?|3=n;?vJACa>>8qNY0OkpOs!C%p*c>6hG5RGVVZ1d=dXsJo>g&op7 z)BAE48#hcska<1_uzq6G8kAG19GWTvk)`R3tWgH?_5P8`V>`^jnYMy9Y~I>?yoTRu z6t_PV_o$e*%dF>2f%ztm-xZRpmPD$0*8BY@5%Fo9p-}O48yC^^b115b*mfSc5HH0P zD9>4?TMfd~&n;%ZFEC6TL`4NttA|bR&t3l9hMpq<;oNG8 zO!BC9^Pj!Sa?u|vreFFJt_+1_`(cEi4J6Rg3R2}pm!g1M*Z#JF%|i*;l(;F880Kf*THFSaennxn%l8OO7`^qDd6cqSF9E^rLY8_Ubqnd!6ntin z_OHG%x_kEBNpQbNQ^(iu9n-4xrt zDra>eZ2~1AImn!?)19lL>(r1<;GDvY;sqd|SHo9LPG0ThhJWdnVwKAU4^B%Re{zHj zpWsF&*0Gm-)jP@La(Z%pu?KeGwfqrdwktx48rMI5Mj)`o))bdPnmD!cq1_xSW0!`u zmrIujx~p#C!Z1`5!ZNEjzgq+PVQ%xu!RPh}UxC0JmZ}hyY2wzbE%w_T}pC>1OZMfTrY-cGbAd*$=c#W7NK!LX!h{ zvvs1{!p1n?haX9h;{1j~5^$RsCr1H+;X?z>QOh_2qTWBCnVUa--F0GBQ`a*Bch+00 z-UU+kvSu`o#W}Z8$+l!Z(|0}%r%=boT_Wv!=hv6WiPbjU3PsY-P%a6Qo9 zNBNWWLzf)+_{}o!{5*RUuR13-tGq_QM%_tqPR)J-KHIOhh6%;!IWijfos*AI#WyaG zs@j}tle17!<$Y7H+ikmI;qwLMvz5Bt1~g{yoH$V~Dm-nY_8pxEuB=B-z1iL_b}NV~ z=Di_A^;+*So$J29b*mFj#Y0U~*J50^65&!D1yHD#bg=&6rlMO(V-9tTC>=P=H|X~z z@N;-sx6kP|C|pqKnZd7be+|8CEJb~+b4~vGGp|#N`CJs@k-TlF)gD91Oe3*(WJFr9 ziAf(!CVY#QtYa^(#u@9P6)z{9DUW{W{gJVoNe-3LWw?tnGzpsQfu;6mTu6^oP{`+- zTBDNi9XP~oi}w0IpB>e7ZZrN#I`2U8&?hX#TLfbTBAy!T!N5veZf(C(!cEW$5>d9o znaADo$Qh4i&BKRyE}*}M_ULt{tv(s0Rdchz!o+Qsjh{L4+xTk^R(mY_USZuqsU;KYO-C|x2YqY; z?3N`hfRWBN-hR_aqrpsITvXj&m_&^F-oP!hsNabK+r#_aDbjHX=x86ENa6g+9E9{_ zRc{}S(fvRZ`4(JqP_y*~p0UQ2JnW3e8khP4+~c94&h<_OU2>7N5eE+(e$t6R+-<1w z(n)Di;K&7jCg4#LtUDOBZ?&LJXG){iC<^bbi^!06akIG#WJJ-!g2UsY6gM@3LhOlj zTGQgcBo#P0sOwu>>OKW(H>Pp!DWZLO;mtqGs8EJ}2B~@50gSX*tl0?9P`a1s8;Uf-?x4EIu`6L^C>DSzWy#Z;@xyzJH|$Vx;0}-b!h|roiQy@;&?J7R?y{3mJ0R0* zSNyt8Xl>j~%oAs+tdZj>Ov9DdFP+4qS2C}nPvRnz-q>%VLIfh(n<#B;4nd8zwzqS3 zZ7!9KKZubuA+mm;bsqe6IBou!Z+3n|B~Y5;*VlERZnWBLZQQ2Fa`-vze=d*;-xF!O zZ~ml)7S41Xi%eQb9~*n)u?K27TYEx%`pkQB1$ozNm$I@3seho2OON0&5KK;g zq1(8+Q2zhed&{t>+P>`@P$Z>6NUm%H`)=>Ez0V!j_I%i91sz_J!z7CK^G+Y>j34v#kR_DZ$v;DWl82j?wq|yfgQ5`HAOiCOU)y%B?yZBauLC-N21aF1vR+)PmaL#Hh&x%v5S#n$n`C z-~`yi*nlXT2drJSx_5P$ASYeH6VDnSz78EYO8(JyRSH+Zk+w(tG#8mKbJk{AR}BB1 z7j=;ee;PdUCa;id3ybQdI_f-PU3TluhD-O+EiK#h`^N}_4@jSLHSuCbo3YF2?&_r! zo9yK(ps@X7&s~pYICZMJ)gS9=&2brtz=JPf{WOb00u{x5se0YJy50*P3h$U!tWRi4 zNgrd2L_W`v*Z5eoHe2Ve&X+2L?ubAXGE$}2Y6+{dzb%}2WICLoiB7u)*n0i&@!tnT zirvU5FaxFiEB`4$Z57D)S8|X#Huz&e88?`dGy2S4C(EObN*V}0Vz}`md`M2DMJW2c zcmAvkVOqoK+3<{Kjw6&|36mAc)($(m6$kIFba=#{=fE2m*{s=${Sf73_FYaY);5Q3 zqqBQ*A1AfaqP~%VcEg9Gf>7rK%agF-B*RZXqXg;Vf^aKN;%W)FMnw@N6IhOM#C&Ni zZthHi(Yue%7k5J4xbwK;5;-BgD!ijOuA?>i2}aYzEg}urP8z0()J`_veHXZHeFi3L zmT*XblwZv+>y~r2I8#9!!K4F3KWylSLF}4-=URTcQ2+G#_qa5g}8ye4G!?+tW+y#ys+ueXr1B;&?*YMx#vdG z(^agNK|_emQHmca^IqfP%yFh07H?#+dnmO9t4VTz|DasimATCpq-X~c*p*K86a-4L zEp&$sTL@rsg83F}!O-VI@9How29S`<#>mUr=xE?_rmg5*nq|j@!zhy-@=HlerzbA< z{-I1db99bt*v2iksUbndV zjGk3m8XEgBHpLC&DPJDw(*XMK zjFzp@+Tc(y*>=L)FLFx-TZ^Syiz4THnXXc|3yG8&nQjWj%g2`8`0j_uJEOh-{j!KB zJg~z)PqlhXx^q>$x-OE{b2a9oHs}*ZFa9MBTP#jbP;>3C=?v-+S;#y?Ms3lKNE=y8 z*_99|^a=qAGWwb_qb=Q}QQNN`t{0M)Yl%;5@aDd+P$ z(YqFs9^MSsH=R@9vJYA78}(OO9qrbZB5f$SK$m6=)K>GMMP*xXV8B-f%QI1%r5_*l zr+;6@2j%!~n`Qo4(()azCs(@3wZ#AT!qSU9c$z?x)QE4JB<5CEQacaF$f@;8;l&5$ zzrwE0y;6<(EaFsS5eG&?2Ya2#ee!N${}7*mOqSiBCLC%Sj=;yclZ{PX8W$U56iX_{ z$9pQLk8|;+)yvGzIqw|54An7u2I3es@i0vlEiqj9G1zm|9hdy5L3KTU6=?Oj^|oNa zSRCS0E%z+`v66rKB@FU|@Ag^Z$+X`@)AR6<;S2fUM*^Mx)c+Nq{Oq}DJ1M%72%yBM zG)UQFZLjS;|1kVdlxGDq_KU+kP5k4`V$+fy$FELj$C91m*}lKp!RSCsik~d{-e+yh zilvC9_z|__*N+cg{8lP9in|6>#QN<<<{v3He#Uh3W9mLyZ7>sQj;FBLqtLGXfj(x=l z<9j1QEpHRuQJT%*RrEv(q#u_ULi85jn~U}H-piG9-d?g`J&=j55>x5S+--DFRzYFX zrTPI7-&<7A5Nm6Or$7Ddr(t&&T=!Zs+$lnCF?_?r@A|8+u%c_hqyesM0F~i6 zn)yXwr@2%OLPmbF+SZI9#5`Z+(Moj!($qg+LXAX^(XN*{Xgwdze z^u=^p*v-PZ;Z2&w4!=!9^cGE5qE(S{UD_-Swzqc-NtF+FK!YIM1Bg4eafuPv`;mj>yC7`G$CboKT>;)kY!>ZQY}Kq623KGW%~!p zGQCdzDo_{U_%CfA-nrCMaio{ZT=0JV^?l74 zPjdSX$DI}`#|>4v5H;@rz6^U&9= z-_A8fn&E}sMXVR$Z-D3PX{CQpvHqX-7Z{Y8v36#U9K8q%vob12Dt~VLiR1pS{UVZ< zy@=|@K5nu|%~u#F*QQ689ys4M@gNY99(($VHEtqwwp*_~%>IQR&cDhc`%?m1xuP{W zYmpo-k7&Db?gRB)ch}iAG=8U@PY%HPVN+XP0IZ*(sm$g^^RHa^|JsH4f94SooM-bD zjO`0F>IYD=F2~iqF?;CDs4^NX`G_H}Bdncchzpd}DzdG#Q zWH#*@g}Y-jrI&N6pwciUaTfj zNjq_2$NTO05?|@%2emJu3RlIJ#^1ciIlDqgrmMX&a<%IeFd`PSsNC#SCf<~2rWaZ( z-~JkUi_D+~S_`U&z>BbUXZ?Clg}HT^7gW^eD4Q?(@1ZB!_A>P>#O>of&V7U4+G0u*AcIB*aNnAf33E;zb`(Ov-i(<6@2J*Y#@SO(j@&IUub96rg8fB3=f^aYxjbkGD z&1pm>-&DAcUgbj)tep>6SA9L;`!nlm_g_lx6}~E~_NKpi>tF4-i^rcPiD|5=!QMDh zEH>h;Mlmn?T2$b{a?cBv;Tb&dPfO#r>Szw(#?QVmUex>?AOmDGGFcx5}V>5I`w@sHtB!37aF zJFJm}q-m$f1b?=d_2AA|n3u(ivi>XQj_a63$f z21POre)L{#{|yw`OT4#_Us4S(07~wRgJ^4Xln%SQ=RdB{4)A|Gybo@Ag(VhBTpM6j zSVp_|^R@Oxp5Dc>T6)%Px;iX}NC?&(;D?99IR!FEn1(|~ZXSP^hOMe+erXIbXvdk) zSqOQ$w!F-|976)c%?tcy9Xy>z-j?65OFPsqRie`{ zjTdcUQQyk$Dmbgn@|4&+rq5T;Bx3hzWO{oWy3TF(f|bL2oBV?u9iw=&m^Y79mU)oI zCo&@9xxpMiM@jEc+eeY2u)PC@P$I}LS7b^hQyH7v^CsYGKgC9W;vi+KHvlQy;|V4G>G66;>FDD0>QEiy;h=?+BG4W4I~e!BRP zbQDStw1d~ejM*HG`la`zy)>8;0!>y<8_NB$ zL9M#{50rGD9~+G_-`c*<8P!h-OZL^fZEZ6gRUIh)0dk3Wr# z-wKHty_hJS>PYDnM9r3pd_Y}(t#nHF}9m~7sa;&<~d*&4oCp*2w+F&m@|<}?U!wG;IpY}V-fVwlkY z0HF*7oYuE`C`HIkDO|Bp}!s@$Y_Xkf1wA1EX+OFDp=pzEAv(@Og_7w9BHx{`eU9$H?CJsmdiN;3^y?_h zjJVj?=cd7ZE3-PwZ1SapuUV+AJBeO2eBfj4S^2((>wuu15r9pLwqoWIaMQTFaK`^@Y$hwfv&^<8_mlT5I!{G6?U& zyT^ZOjm6K$XJ0u_U-C&~8F7+VUB(7^o?OAME&T_|kn8;U{O`LUsS>cxY|=k+*$N-Y z1^2m!zY`U822F03P9*}^zp7AMv27FE<#Kn`FYh6Y8RH(gPEISD)e{D_>`LQu)|g4p z`?iGaMHasHe61t$&kLG%mgCrvNSeC2T@b3LKI#X+f~*@kI@6rwjht_qG)|yA<{n0K z94#i~_>n&OblS1~L-Gd)CUgayx|J}!F)z*#m2mKBMajMfMtsXlL3*Cl zU^Yz;Q<;xaeP1lVJ)(RnZ#W?@GzBrSz}jAe&3LsIpV(k@1j(~T&0fLV@6sPVU}d%+ zNPK~OoCHZ1+)e+S127T3-AU&9hO7!3YV^*y$M^wltAFCdgXhkuU;df4o+-PA?m_J}hT%NY7F9joCTW86&Ef;SxwX%kJc3@0&N!@C=h zw7VW=twfcT6^Nu{bB;J8NQqX$XZ0+a7TJwr?l;D=fT{fmJ7m{shg`vWqGIy}2eKz| z7rI4XH7WG$_7O~XB^RG12+c;?lpj~CV5cR-FCdNcsy}e~QdnT3CVO6J;EiDNRC;+6 z8o)acoyM#&d0xoZho@NYv*#iItmM)Brpkb*3p}`c8=?E`F1Vbc1t{&lF(`iF@_x$b zTfdrDk=Cy(kO$#Op{=ir1)1qKRMv4E%qm~VAyrd88nc#fb*0SoH++~Qxww7hBg^Gxn zMSjfUXSnk+r~SA}eH>%3yIlZsD$=uh&S5?rY|}_I9BhQeR*EJp;_-Qpb7FmjQ$jtr@WbDKKU^sU|UpP_?Q`cbz3)Ei?+{-9jBZWL1svo{n|WVR-fqrWTq{K4nAltkr<_3rGaxjEX5xPp|~lcznGLEklCD|PFtnAjL;$Qomx=MBZ}#Kw&dAp3l4G&E*OgG7WO$anX`*|XKuMSQ zyQ^^e3OD>>>B_#f1~!yFzzkEZmNn5{%DEK~an$&ZgA7WQc1usz-}$KO{B`UX0Qi=8 z(+>c?kvzBpzKQD-$%1yR>~-I!|J(!-E~qrKejn>vxhZ~ z=(MNFerIOH*_NRb&Daq1Ga0Nu?XWVhAMvYVIg9X#QeA%4XPndOl@63Gj{p*PE`ZZgQFodHqt|> z=mNQ{WXa=Ny9=gH#g2ONijJv)+QPvOIp?dlm|3*644wL2M6^(fTSyYAMVc=S?;)E8 z59E*3uTXh%l}pcD@lC4%yFAPc9X5MCFfxjxqwsVw`VSO!9$Mw$sGi1(L&$uA+RasQ zAD>p+>H=m$kvCe?74fdrdXZ{Jon!%(#8@ecOv{Z_w~N9aM;tGvQ?CahShd;?785bm zMx!2Mq+s6euPKp#k`JgWzS5y;__SG8TT*p08!~Y_+DBmoOtnzBs)G1bV?q;bKqn@Z z*%RdzA$C>8PwdPnTQ;Od21an&F$M(o%~D-TKJzgyuk12Yr= zNOWvv3kFFY(zJpKGalMJ)HT3kCxqItmN zo@WjRqY)wGsjxc@#&B!5#8UqBuXvAOqayiT_obONiVD|!`h6|xu8z`hsve4sSLxP;+Hr zFjK5SJ;9_0`~lh;($p#<{v_}8BUyo=W@UxNVz>^IwZwYUyHmk&GN7r;K&MpzqrCIg z<+h2eWA-->`XFIJ0cbK&A;Aw4YWb>px5W9=jlXA!Jw|v1>k-}4a97q@nwEQ?3?PlM z9KjnZ)_2sZXd7W5eXV>56FbUL!CoXjJ&nheXWn(CKnY;mrT)pb<7$tXlXeQj%P&ib z;ZpZf`x{k9R`RLRy|ciU@1pK!@Qfb|EGBnF*z{tv9SeMSi_r5u{7aNiUNe8LG;k_u z?}E+b`VQG@W%HlZicHWH3j;G#eHk44BnGH=E3A`NcJgP?#-9WC)&BqV&F@ zz=bW(i1iYgl5~G~r&xM+u&JSuanq{*aJ?4oy1sDf$eff8)By z?ijV(h-1w%>GTM-%Epf!>oS7&REv6^9_F&B(UHm2qAE|ad|8hhhGXhfjwaVOV~yM(aDE9TSGod>>%@Gx-rsdpfQOcqA)1d z-L=M8jZbRXl{Ph(bd+>gkhC1ioqmE^YW)mHH;H9V`TUG1k9Uplvg$S^IQE`Z?BUp| zSgSh#2JdDPj>K+W1?whq+%JeTXk=t=89xSvCcV<9e!4oRuZ)5SUR6&p#7wnstR`VR z?j_MDiqh-aF@jVemOs+mmj5NS{?$W_|1Sj9>4tVqcbHPD(Kz-;y~AP-K+xZf2?+Z0 zi}JltHUX>GuESPPk`hc$mW8VO{s4I-#c*{s9=KmPHq|~k)`L<@S2ya)e)%uc+33c< zQnh<-dO_w+_$KC6d36{r5zZrM_x|?42o@u6Rv{(=t47aQd8D6NjzZctD?xQVtguT6 zAs*eIrJnQ)-HRPWC`JCHE-G}gYuaQpEbTIiO?Dbg(prj<7)>Z%W4Peolfi!Xx!YWa zo327^OIzH-%+O)e0Nc?+vn{Q9i`tl+-EKdZtS_jOhELkfW)1_ajxS}?bJ?-gk+8^oz%mNab z=u#@uFYVR(suF>WI(!kYFLF8JM<=kOI+3g2fu7}WJ%N{7>3l{I=w6tCbyulEBshmB zYPhO^{6x}W@@O;*vCjWF3y#@^W+JAJ?S!aeCDHP%=pje>Cl0>x<63R z{{LC>s^FSLtmn%VMGOv(E%j?@jT)ByMMZUsE)GSg+L7H+)Q{VMA*F;Qn+QIK-i(6+ z$LD~7wWu%=z#z*TL9y=_8J6JJ4FmceGtA_b%P9ambZpHPo2@PWpl zUujLqjn@^4H}nZh6@po)aCB6lL2{%YW2!!%s$Kc6%N_s_Unfgu0OAXgx@&d>^boh> ze`$Uky{gvqR8~bv%+6hq1EoK@(Ocx!>QMsH7H>&t1O=&MWIpn7hfUPiLfe4+Z>&@U z2x++xKgPZfQur;A5V9~YK$$@a&;4#sbW&Ot*zC$(wQn+V4cM(I&~Kel)nV_>v&tTu-Sgozq~0P((1Onpp<&?q zv?ya~2JZQ|xTlJQRBF1Q9dIpN6Mef7TtJ0wVXxgiZ6wcdU;76kE`k*r7g~id$2%}m zM=Uc85c6pcRXBrO)3*+L08(sxs^9|SMX!~u)ZjQP0a z%XFhnU-Ls2f`tU~1V?3oQ3sXVSj+c5oCLF!f>MJ=dHfbApL|0KYAx!A-PQ76bkk(h zxa0MXE1EAf7;gS~iZv-o$;LY02!aJoK76_SA0EMcp&+cJ6q%B;cVRrs>VKonR$40q zRzdIi7GG(D)U^~t;8`l2sTE4LDz?%TIaeV<)YX=0W48G0TRj2YGu6d@*R~GWG4`2) zvlXA-&%fVQm*ce`oaf$8m(WwsSk3j*urgsOr|3o-ohmbozakM!KLExiCD_l~DkRwd zz|dd>pc1IPo?}GTziSTdICp&Wuv;N)fg|sqfvwZ!P~f0 zp>G)~(&Iti%|6$N)2mUy_d+9iHhkZ+ThO4aLpz;^P47LLe6=7Wh;Yc;SCxgglPw!-DO5FH6ui`qxKpxr;~M=*pN-@r^KR0wl|;iX z1sQfN5(*xn6Rp*|bH*fhIn0!yAs2v=%(y%rU?dY~Ci+S4Z!ru_E7aGy!Tywf>Zfhm zr^4qR_r|j9_14T&72;h7GnRIdu3_`WtQ8p!toikE(>rfS@NF8%kTcpNsjymoTXm#F z{uMIeDrEod@V%<+2TioZxa(nbUi7Pv z7`+{$L;%6O)4A9uHunCy45o1Gr7L(Ew9&hfnX756A9hRf;D+Dd8aRHK$VL*=2rr@-2e7aPo<{c^O2Rqk$_wcb(M{DLJg~S!YT#*2Ar0 z-cBqqh;B27jgd@w_V|1tHpR(&PRt_o;o`@%^;Udpeh#=uWAR=*zD_>N{X|2@-Hm&f zEu4`iU@VhGL_9c{XsVS#nCtC^JucMBosF6Slh%dNf-04qW-K?xWA|R#PG=`hY-O60 zs)mQi!hnmz3u=DclimGS$(gKBbMp*5&gs!d?I3EAp+>{ls~X5viF=}oF^z!};O@!#i=ZXUs4QShYhC(Tm``IHsjiES7R5p`@6l zYEo}p-kV{jsx*pd-LZ~pw0O=p3u9UCK5uh78=^krwRI5BH&P02D7G1B{+*K(q1c;n zWU=C8t?i7?7(YrVB%5n7m4a3LY?>}?nA$^G-+4`14+>`{s*y?FCP;kj$?6HP=7BOO zGh>0=iiwnTU0o;n0G8c+YRyx|G%gXWfRT$!TJPe|6!)V^5QI;-w`sQF%;Fh?@&50rc>wUrNR z#2;R|W!oD5fg*IiFU8cM7p|lj~`2CZktSw@6|}oVgQrCLb4SESEm{q z8L1G@yVOapQp?=8H_6p^bMGj zKcMX$HDO>-tx-U3rQ=`660x`ySaU+ek}ayRCZl?D{FN6taLVcFSI*qc&#KRHdlnZL z3U7*XY}~D+TmPO=i2iyGI|T2BaJT?#k7ED*BmWTl92THI6C@hH75cM!Zb~|>(!-DP zm^XOy313BX+;D39$nEZG? zmBJGS=_v~9sVZ6bbZynG42es+Y#m+4MgMmA1BKh`P@?r$r!+hzj=JGDW&7N>`{s?h zJC6dH8jQ7Cl0Ljp6)cWGhBue%_oT>r`}T94vP^7VcjZ6XXdBdQUCUO)!YS8Aa62H zCzx%KbB(UU@$12yp$UdD$wn*Rebe5hWjx$iD7^juX|vMCR>FWAutYYtaa{M>`LhLO zN0XurY|IwuweJ_}z%u5kpO>^R5?&u(SKl_Y{tj1Q(}dhI^N(SK)`AF76u`D?qXMj? zF~te_ZR!$c(?o^dl47cQd-Rw0ZVFm`$^!{=_$OA0trBMA@kUW@ED$t=ZXtwu4J#6r zePZrMA2rJ*Ev+Uk3xlBQs1L*jr})K!;{!Nu)Iat1P?qEs2@>QNfxd~|ArPC4R3P5` ze|l+Y_4aTABPTy=H9cA4(9*XM0z=6ZZm)N@Uq=40dM^7^#=eq0IaS_r1rT+WTBiU< zFZU!dcR&3q!b6}ca1P}}Tm!_;GnEY2Q@X93qvgV=8M87bnDSL8BwxOb%$%bBd8z`5 zO)ch_+99t*0H+w&bPr3jgT<)EN5=sisBxAx;|M($bLc`lPY`Hr6?z*&vS3xlL5!v& z4Zt(BiobOf2F7Q%(0X;?ara!ipwsgk%SM9;3j$_$n4b{%xN_=YisWucb@ zOd&m=0wZI)i20&O0izQY)QvIU`lx=Jw@R_(SD<1;Wj`Ml4)WWV9k*`aS$%5sK3QF2%UKu5@3=y@$WvP%*KR;&-awW-f%QKM3DE*vl5vhh9`XC)@#dJAc-nu;P5MG zWA*9OKQH*7{94&ckWo`ChC{ZrKeIK;S1kc%82{{F1y*wOIBgcbx67YVT-p>C7CIQV zr+@ny7eNQj-vG~Fuea-G%kV;^TuQe_%N_^4Q0jbnRtdClRhu1QaT$lzH#8e8+^?0XDU-LO>>LD_LQ604ze`24_ z5%;}Pd!F25sf(EQ9FDVvS&Lek60j;=bYABd>4bxQ#4R|sqkt|M)@g^f2({hHTYR2} zMh`-ZL}bVUEBC`a(`N8WnMC{_MAJULPw6COOb}0xrOSj#op62a4-MZmLCt*+O=d*P zg>OAKRsG~5u;kxJ&n!(;1|bjyQLTSIc=g4TJA@9Ns7y?evqvymntf*KG8rw=ZdY%{i=io%oB0PUwsGx1A1rv z_6};mfnFb^ZhPSM+XhflspS_pG;Kr1z;hFCeNd=?SuPLrv`d#|uzE;Q%IVVob0{-KOcV4gGR?wYQ5Heo+M zrheSPKA1TN%TYj$4CG%ZX5%OT@63En?G;0RyC$$^5`84+o*y1&YJx+N(5a2Joai^X zd-`_usSAsEOd~;@Bt?QJ5~-o9)M=)jk<0Y{fd!2c+l{;2OdMPlEoj7@9UlEY{dygw z>^v%32MA2|fozR|ti(Upsa_4KAkS^+!5|K6U%Glj&^ zOia|C9b@w@$*-8JZm3h!`u@G8{C|g8|2vKBR-ZaK8AC7Rw~aoB&j3R2)XH;fTJnEo z8Q5>aq5n!@%0Ic+Pd-1_TUnvdQQe4S+f#E^t$WuPnf3b58BbVOeG<%oFHF&(2a~iZ zg7?4Kwf}d%um9byt%@ee6Y$cBM=k<2-09)+KZpN)rZbWgKEyU8yWBehLiPV-(bj(` zocuq3_s9Z<284OKE8GpOb>qC!TwBzuRFwdfOWKC?xAP&j3ZivRPXV}ori)NHUy-jI zagujMy3Bwf(nvrR2jk>_ri_HyVmkG*t(B<`8P`WO$-7(6BFIcYW&kyUw#50mNBqzH zR@bsQBBCggWb$M?ILS(pA}v2Kx!i4sFX6Q2GAP19yY{ax0s5De6Lxm1{{9qivQ1Zf zHR_os>H++swK3t|7R=NMnw&yiSnNJK*y3n@0Cey_V-3r;osCStGL6-ARbpJf(KsLK z`BSf+sI?_`F9~x*!L#*0d$q~bB&0o?h~~;?)U56r4GCyo*{x>hA}X8_R!`($F7z*8 z)}pL>GuDjUC^eDB6QIg@Ij|L6B9dA#b;s#h&FxLWSma?dv6}&Ry&*{t{GA#hwtuo1 zkRy~Hr4@-A#F^%C+dvSi=|y&`H{YZUC96v2lfBtVEB5q=<7c`_D`9{7WTtF@C)`vwAI#=up*1ID41T(@j`_Nq zTj+@@%_n~TN@8Mb>C;s#Rw*3;%6NnNy1JZD3YQW$cU!c20b7k-*~Y*kb<gF&cEW(y5cy z^K9vEcvJPUS%!M_L?-;z{3FB0nUP7pk0$p5*s}96#!CoW74cL)LHb0{Ki8%LKX?Pp z(^KLavgv+2^Y}g+LTagRc!x4gA*+PC373I!x^ZjsCkDd3(`S>rYL%IZ`)C3K!tJp} zd-~~#8|@aavuRFH`*9s!`T3!jj`mq5T+rX1-EC5^{KRQKjqt&S%@&Bk=K?M^R~X;4w0KV2 zKY8aFBEcS=9myP8POA9tK{q%l()N}2{l4XL6&%9w_=6TNiegfr<(Cbn;_tBzi98 zF>dL;(2WmLqxfF$OJexJS$0ww`kli)WX{=l9c=`dQcE@(PbuXt)b~~hX)~>#VBxNM zgDJ6xKPc{S(9;dfx^Qu7m7=t@4u8i`QE&hOxhPMLzS(EBm&vBqgt7c z`?GTRz?Z?CNfPiattMQASf3ex@)MXtN zaqj!O*T*?#uc8V%84ZcB3g#Go7OQ=(8~9^scP-m^i3UcmY3Q0Oi<_|3t$M)?@UEL! z7{W?DNQ9R#VPSK8L zG$6{j^kh*hzw~4wS*6<{^7SVc8g`$JHQLXr=l^PYTG)oGIuWk3d;l}RHVMp_!z04q z@nK_Ox;>>5XaoUIBQuUXqL@Pl+Thc`Jl(@nv!+&F>L-P#ZD73$ZWGuNv-Kzoc0d^M zVUfaM$m*6X&|qOrs$0hF$3FX6(iv>!1j|jA2BYm|KGjepf1MLeJV(SBOe_;hxMz4Qt2@Hj(HTJw$vcjG&~CMnj71 z{dv)MP1h{rUnIu>U0_JSOTRf}?GiIdP+-O#8CU6iThfl05Gs8`yC-`(@#+fr3*}RB zSas{}njHD=XYAD84}7WkaMP3D8PwWPDfaumH~-B@;!960fNboybxpu2>8OUfW*qNs z_$?Ma*LXNsUqfP_v51aGfJd>0nOv)0$on-CwRN7+!R*yOVDovIr_9z3l{4;LL=_aJ z_oSbZiah4NVKR@A6oaTWXzC5)ju`2tIl`ZPz$3=bgi2rkni-19BKg8(5}VgTd9BF;&YdLW@EX)T0b18^jeV-*Hs?7PyrEyHDE8rADrZRocQDTW2l&r=?_s(R`65e z6^!7tIyO_X#wjp=CE7muIP)OIap*}M@0sf7Ot89R&9@1~Tj62}86KZP3FXX}qo2OX zf2maeA?(}oW{=uyMrKDOJ2AA%)Pz?djU1+GoEpE4b1StGcM@=JVA-{T>1@rA9`awx?@hOZ*S6VKY9%(oX!Cd{nU@5-yP4(4rcM5A$r}xn9CtMAe?$J304Q5!gjK*9c?bm(Tt%9oUY-4amz6YpZY7R)wFf44s zko9UU@lm(F30@F+TI0kq>4}bgdVS-A>Nnyp-+|)=_-cw$3PA{{;aMb$jwywKfT(r1 zWiIiIMig4soe0M{`rCEYYM)-Jj=LIdlN;$2YYJJZ$GJ)!;PfQB80pJd8$VDbJP}S! zcj{4eJR({5H?dD-{s0lMR2>@;n#p5BxQcKy5WM!3y~D)FT#$Zi&42^(q>)@cxJp9X z7c(YBJKl;Zk(+T#PoLQJk=A^Q`*il}>R$xLrW5SAMNAKGNQSAAD~>Mi?viW1dLP5( z?m4bvrOI-yu)wZZ&)jDu_?~YIr+A_K)(!j)%N1htNo*K$axT#`NsK%YOW*)SOT;n5Y{% z;Z(qxY!oRzjxKG)$JDqKdszIukh5*hoXE;K)t8K;FChu}Adul?g1>?{SH6~0z{>fp zzp1?VfQf*ZrbX6v6>y(PeyPbYx$H7Bj4xlC=wZx% zX%Y0n+&DV{t)7WyGlTaJ6kTSKmGH=tn*1N#jVgnY?vk>r`zt(?`koxBvB(66?U$Xx zLwzMl*?CHrgTjDn;)luWWEM-qx*-P?_Z~@E$JV&LcBqi63PFHeU7Dtw_pwE%A+yD3 zw<5%Bx?z0ull+a&=5)$dXj~*ys9Vqo{rwNLe_a~q5%FYYT0tt>ZcBQ3KEG$>tEL4n z8#S$Ax*76e*8|mO51cWdjK7y;Mdw4@oe(j-C^K{%_)Tt>egI?O(4IC(>Shw?)yl=! z`tz=d>8r6wO1=W&O!Jqxs<`g>0Ayai!7SDVMTkNQqpxP??LO6nfk`ZSD{a=`NWt44 z8{&`fQ|$hMn&_|H46h(IA7{*)){c@~TWZA}lu4?zgre25eUH!9!6x^38@ycVrpdhC zH^?)wt-nA1ezdjgSDoEO#ML12rQ%}L=VfMkx+{lL6*s5NC|tHuv|lQ-G=0KN%RGVTG`%RP$pl z+{t1QJkj}|JrsIw=(>}Zz}ml=jblO^(ORlhy#DM+4;*O!%%aT_qh0xPFclPD_^RAi ztQ^-q)7O~dgWO(|d=~QBe(GT@wXU72O`>^tEB1ENtG?A<)5aR8-PV}AQJ{OCD3&5X zuq2qCsUjBSd8nSaLn%0ZQ0-_ru+MZUFK=5kwdpEWFUqYWB1@yS-ai|Ozdr;>88vtY z0V(5H1~9rrwyjiaKCC%~i~ZT`D~rfSkGe%B>pPsH%A@)X#k_6fExR-vvdmB94(k2b z?Hg*269g?Jb0@AQI?U##m?=pQH=$6-TU;7VPUXUYxI>vbi~eyuA8my;h?HuJPXu&1AAQiZc#sW};w zV|zT{?R*&sGVKBbxJnEd1p}_GlPoC@Ji|lG`p?<(c{X3H3K=(?Y#PN*5 zvPklw?kY`5K{6~Y$JNkf?&BgZVC`+rUDwU433b+8W7j?CAQZLFjX;pf#xQap9*Y}4 zhsFd?NgLP!;NIAW7#O`{SA@w+?#TnYUz){SPg?XdIz5pJ`2Km#!59PRHAx4#FH7R0 z^Jhe(K;m?`&O>~7zPtNrX`WBkci}{(mO|9e|3HyblqU65R-&&(=ch!NKm#Am$`7df z_!<6hoFx}lSNk<>PtbL6|HZ_bd1cOeFGwh5P6AZ!YN^D_QGcHdqy)koY9_V%cj zA*wV9o+!$-r`@GeTLE++pg`l;R6uy3)$-O+wAyKv+~<14`TE9jPiJO2ErLIye&skP z?8H;*Ayu3h6_CD7wqUI?u|J=F*C91s7eo(=cKU9jOHYnZd+Vv~Z3sl*Qtz%Fxc;-? zFU&)~D8O4i{@G_=@zT{}tXQ=&n?}Nc+7%zdN+~SV5;>glIHv`*Rf3-AaGhjDZ1=T@ zH)9m8BaM~PRgJY_oKkx!%VbdSYMS{9AhLX93tn1X-9@MO3U;+elr`ax(yr+9w^M_$ z6!&jNuY1hbBZg8>r5HJ};RJ&PIpudP!%_E*uJ*-vY)M&kM#8TH6?b?|IXUMWXIKK7 zHRfAytcFZIE!Ht*Wculj4n>+px7mnLn=x6FZFV&RJ0otVZM+%S8EW5>u9WG0NuI(q z7J4;fX=8zmH-36-PER3#UaY6BPLUh*lRn)1C2e2_ONGPicZ=Jk(ZJIp@-S1`U#gs{ysqKg_ z;fW|sqpn)!AH>=&>O@t;=@8i=2%gq2TzXh&i!3|B%oVvO63= z82v5k@?w3tx&F68ua%9fp|FBV*Ffe~v z#Iyd{Ma|gheiol9|A7*m_c4BMV__E)N`wgK zvuu%~N?au^fZ-nS1ltLm_ooU_C~p?cmp6}Rqqd&V)sQTVRJ%F|Y@`+jy7 zufZoQii~vcitvcEav<(d;B50nXpLlyo8u75^5D zh+HZB+O8H}*57*L28VKMKOeHkX>513(*AHVi?XVlY_L%?ENPn|A9p@b^?hJ=8(tXK zwnl|e!23q+{DNb$-ui^5#v6bfoYgr&+d=<}y|;{uvhVjrK}95`5k#7Sp*y6cVd$YG z6{L|4K}11HLO__IrDtGh>C#*221!Y&0i`6QM7hs3!@VBkS$plX&VIAc=d)hP3uorK z{`re zg?!8!&o8q%_is17fQ2V?pu9|12(WuA^!KDYSS@9VvHJQwhpM;Wf$d`Cw?@w{ncwsx z+=tQC=y=~Dbahg7xrLJ?jx;(uPjay*zy@{wCi0m=-mEsbS?-a@y>j}OH8dzq*}E`= z{8B~m>f1+>8yCzVmW}pmPd^M57ltinuSsJc4yH)$E0EhcOEt(34{~CXD?4S~44V?` z;X0ze7ym6luuW7CT{<-uT-MvXvNrxSf{~Z4b}Ou&`RM64U@*(w2gb9BPjX4cYXJR+ zl`>FH9MkY97B6SkQH!i863{y{JFLmldHjS7O55OIyq>xhqn(|vUl_LrjFOIty=kI0T9boXB z*-ChubfZK}dg~EG+2a)ujt&Cc%}z8+O8MgnT&8B2GsOd zpZtyU1f5as?S=q?F?5+(?vy5rGd%IqV~TXdBzT;0R`N56+>#NWo#A}yM6t8_S!4Ci zpTWq>bJ-Wbx^m<%W(o&0y`T&0AbJVo7iOdJDPgT2J;7-&E(7s;sYi=g(CxJ_&1tX^ zg73~QRrzM7ziv>)#kHqf+2XUkYL6&Ca(0Tuc6}A#6aS#+daBP~z#~gk`@O1RsoB}v zu!P;}nu+gMRd--*qN|eJ-KvB)*5MX4^i{pcRVAD|}mh4J6{%&JnR# z20%A&S^9M#qXf<_%{E+JeBkU74Cj>s3y++_TBSR73pTEuI^JV(CrDm` zwp2w1S0~Tfm>x521qS(Ca}3uqz7Oxemli2`Z6SKnA86Ea`n_QyYm*of|2dBdZoS$y zDd;&`<8YVQ*H68Qb`VxppYAPW0hgLLp`vi?i!*lEh>3bcE8@x)>U5p5tQ zsGyM!1dZcgYj^F3t~Puy9BX}Muun?QLEXj_JSKQe(duJzaK)k~RnE=Cmv^k5&<-X2 ziq|vETx&8}k=}>?6=J4YToJ!BYqEBAUD72M|Ax{?^yTLRmH9qv*SI`_J%-ekUpIwI z#eH`qWg=h;Wl&51O+o7`3x@N32Og(0*yvfXfb%8!@w4Sf4s>+E{q=N~%Q2p!$jy_T zfw%@|zhN^3@4-_j3^4hAb1?&VXStNX3G9bXV;mv8#uN){!~?t*ZMyRHBNA`pMIf`cIZA= zuHBx_SLGY}BlZ#y+oB7>Ky2%)DoN;XCDauhCcZQ-&x$8)IsTx*+Sd%FHI?^0W6a0+ zYjq82A+UyGpC}}kJTKF3h*C3z7Yv7bEMuYeC(!>g%uv#8fNPo>Ng&0F(-Y;(?|-KX z>+7ye-obS<@LQvgYgya7j6EgTXqFwd!gj^BB9cDdd%m(!=Dik17Y3#UR5+HY8v`Zt zY8hnwmAnq8DfX3z2kN*A*4hgk#?vz^LAJ>2K(3>mIsM6>RO&AU zMeu5Apj17_yf($kbUinN1b&9^>X@|->*1}&xiw3%T*>SMre|d=Bz<)$Ag4Ld*vskT zM@qyW6Ot9DjmGI<5@yc&O+jI2Y7xKTP08v@JA&RPQ%lJVvEM6c9nYYAM|<7GRbvD0 zpOH_${Wxl9t>4D@y=$#-O2{k^J(*)r{+??iAWJdIZ@eymYeu-Ss~Y=xdK$}hr57o5 zamLnL90y;P6#>+4bN&3Aes%e(K3lH;^WXk&7!XB!R|2_7JI$VOJF-$wz#1K^a>nbi5C{v5|10P_D@3;S-XKVMT5I|rGV zK}bEv^=dlwF>Yk{xL`{+*yYlE(|+in;L&f0%kaa@m6o}|Q|3>WA8*JhkkkWA%B6v^ zT#+m6S;T+i#PD^|0ECYgMA0yA8*V+Hv0}Rk?+?REAg)->nDB^N23cM$-lGh%UD6(*0|7{T!7adueGk1?YL* zM>7=&ZU*>&hCy!qCBKvnmlfUz_%Clf$1T;-PiG6hmyQNoaoMeujz(T>`OSKSuUEGh zfcq>(SOIV!j}wC#@ysSe_{vzsILu6!-T=zN$2#W9ZCtO199`49@LSIFW$bI;KwnkX zNtcl?TQWs|ErL^UuX$oRh+BW9Vu|99^*zs5&u2yFY8$K_1PcV1AtG~~M`V6tC@#Qu6* zh_rIEgdGjuEI~0;n!~c;=^jKK#4W%-i^tD@7JScl^^YZK4EFzToIniTIQn|>+`n9r14zERwKM}u$essj6&R6eT1;-6+zA(IBbmg}qGyK=jkj?m& z#>DZ?K_~^EYAg(WP;#BF;+L%U<;;0%@T`&HZdw`EM!V>a@+F`M7Z%H?ZCH(Na? z2U_5$@qwa%e+ki+R8K(j%R`0X<~KJVv;A^1%>V2D*Hiu;GxE7eM=v@j-8e+)BE_Ly zp&$z76q=+Iw^YNs+9ic*V+0JsPQhKgsMQ(e83=TozY^z-(7GaW8zBE&uu%4 zf`CQn@=?kx!t8pmXAS|~py*<3-n6}nD50(^f4I>Sbs--cS43KtHX(ev1f?zcoTukD zPqJsv1HGo3tsB0EJI2}a(Nw1d2K?fRmUgyAplstu{_)Gz#>&x2Tr<`zx`p3WjKqc( zj;3F=$pEL@YeP*5Fz?sVr>@*wc6jzFv~lFZ*bcM3z6PenZ2V2I>p$P4SXpF1UEE8` z$$`f=z>siw)N~1(8xeUL+eAYJu9xSk^;5^va?Cx1xK{fb&SqYHn7d3DYu5|ypHW@j zx>)gAS!w>G5Cjw+!(4T|TUPy84oC%b;`&ef|0 zsoK`*Cd#|6c2d)iaOskjG3&6dH-&d=3W)53mb{H?2Gy>SQGIkCG=kahi_nR{8Le(e+CY?C5rkzc8p(3UYk5ym1>u`Hywxi zj@qW=ukfM%YN+#YebPea&NJ-2_Z?om>}Rem}Zj?vhnb=OIlq}M66%B2#Zr!^RWX+l>`sLN` zErXyE$>Nb^{^2;;VKU>MheI!KH5HUmiQbIX1wH0+_CfHyA#hy*)j+zw>+NBsuE)-c z-tDe`-ey=gVMXNq6S)rnG68N!mg(^y%1?y8e&*2-U(uR|3SkW)05zrVR%<}mQENZm zlX;JLsEXg}ovRhfRY~&ffPAmVvcG2efLJ_mRwSvNTfXI%MPHoO&D!@!a&uJ0)3B2k zPnfw@H!?(H3o%^WqF942z>}D+jxOUD-nz0phf72(CFGzL_XB~1w-I$q?Qg_3LQFFp zXM=Klf3UPa$z3?n{DsYLsv~8mD?SaHh9;wYgYkCb++j8Mk+3uk(5yvMXwZHRy-m5c z;gHg8>|Ghvtnfa7m$m-!i++4?PQ^y5eQoA;o9)nG4}Y+qNX>5EU?WFCb-@CX1h~QzYmBW1@u##?BAPe=hVO0463N z{Ed*hqf@fe-`u&$Fv(|5Pj5eVms84{bA~u~23|JPS`#WJFPP@XP3gaE@D^u?P)&qvd%cO1$^Bc8H5Q1z7SJ2z2f5fU!1k(u!~GFMz0#0e`&E+94Y zqx$vYqY7ii&=@DuX?4Gl1tlbjut|>2Q_wfn#F@nxGk6WAYqgq7n1pCBL{{26*k4b! z_19te_DNEQE_b27Wu-OAJYpRhzT;BFkeM(7qEAnPMr=9Xf|5D4J3ks8o6Sc7=-mJ! zoj2_@`Cg*{lUP%OTM-O(`kD(ZF&K5vfimp?x)#Vf8a97TZ->m=?}7u;t)+lE7sifqKK2@+BR1XF z5!dxK2T?fg7odMq+YuSk(0 z=h$oD_ZdutA9eGu^PVp&1%ha2S?C`HYt`tYlI@^9&3{d ztsVoveU;wtBZE}6-|x9)Hf07e&W`7>K2@~i=Sz0-7v(NhXPV&TBd~3Vuxj+V$yG{* z|20Zl&MvB>W#F`!U&U$lY>V%3UlFxP1e+%Vohc`xhQIKXMYJx&KL;%lK{LK^O41OcH3Jxa~3K#Qu zt6bWtV+#llXU{fPNRT=&TK%W#1lEPZ21q2>PsV^o3OJn+B zFQEe{uVgih@E^vmxmc7Nct1YEn_gY6U^jzcUI$V z>$eXK1TS1cxHI8#J}VAQspgaBWLH1@`+hJI*g*orYj-%1-1o=`Shdv?6$gHxF zbqdO0&d;8H!j8TG%O7D`933_qJJJG=3_W}_>KZx=n}YO4UTaNnu=yI>3#i#6sY9a% zfN^iD1Z3z$bR_HAa;ji2ATP?4p0b1P^nne^5Uc1JoCY!#XV2E_TFaCaJVx%^X{w{W zLT8M>tRQT*e1uKr&lCHppE_WyCiV2G-gN?&-Rz8qY3pjl(k^^m?1<}uEXRruR1EQP z(kJC7ae7VnbsH_9yP7sxLJu!;i%^(UO9=;)TM<5W>Wj3&W|mk;!{&gdPV(@0YtDZ5 zN`my6dUD2ncC503X)$ny{&Y=<)aM@?TS}B5f?Uw6))CQPjR>199V>|zwVdX=(k1JC z%BJ=G7lh(3Wk9PtYcMlY_$^nW>Q{pnt_mY9o8<&v^r} zPmC@1S=-fuk)wY3a=xVP{rQQb07c&b0#*d;Iel#w5suT;1?cnIT!nZR##x7d z{0<-3K42{LR!vO6VK>%>CX?zZOnPsHk;R#k3R6*`Xb|Q zuJk_NO`ks7ip~|9%SsZd&btWJOAhIv`oWgy;g;X1X^wwsN;8 zotVoApw0sK6qd>xTt#BF)XR)oitVoJa&;YJ(~^H&F3#&%^!MpRA8``=57GC(#W5&=-&?-7U45f$V*qiDD?y0+owDRab(jD*HT}Yl>`{} zif8G^?;KuKnB*K^q!Xlp?TuaRx&7yjjwdW4kp;Oh(K`aI`5wUY!Q4&5OdC@fFi^k5 zNP5PxhK9skC5&keb7mX*5Re(WJS7IRqlXYuYu}w8px7U%*(Yfc;JAuhN4523fgIq0 zpnP8#&%68fbSJuWf@6K9Bco|5Ylfwt=vpOfP2$_7d9s~q4mT72dSaHolhpIk%d?H2 z)%f1F?KD*+BVP$eFgyKfnTnK_)<6T5xpnQ$oXoEmDrfUu2Lus#JY0Yqasmuap}mcZ zE4){204BAB4v_H&vF{u|mtyI(BO2&urlzq{A(<6w&A&s;xgECQwQg){gV^Z&)S1d) zt!k#~IoU!b?Mon2D{si$$SD&EZh}Cc;_lA>VItw$r|Km9*<(%>PkHZ(I~$uM+W5j= zih=S58>K-NKRA*;x$n{zjuDU`YIci_Yz$*@M@<3&U@8;>Bad@k*Ymnnb=HetDlc*QzuJB0HDXMheIssJ za#YNY-MNgWg6W~W<6)rNK=mq!E?wo|u=zo-w-v!PXUR48w$dRHA^w8?63}cb#4!Wa zPg*=V^P#)p8xo0|mj=hRquXDnt2Tc1pvFPk4DFSXan!&+DRCo!jdf-no(hVxcf3Ts zI#9GWV0MV{KUV=v#T(LKUS@#$PPCH>7off)2NMzQt*F|&h>_a#yn)fywI2KcXzL~f zPCK9&gv6#@wIrsG-0ep2gDl24pWgTd=9_1ULUpja(ZDX{+GpD550DTvnxago9Gh#* z$T1^->vxR$W>9yxj^EJz!?KO2#)k(V5$TLcPKBJS({CfXNkYpwEZExpnKfR`5NnmR zC~X}bQ#dL5i~Ycsr2omw9{!qW-$_hMsc~Rh@|~Ku+Y~QptW{26qwfti;sARze7fZq69F#eOSNi%#fBj+Blq7HD{( zKS$kGwC!}-N$EJ$-vVpV(V$o%#8HgpiYqS&Y7 z0Dbc?&Tr+g{JXn))cvLRV$f zhRP&6nl{EY=YbmjNSEM136oH`l&!W*W7q{R5eXfGXXFsTd;6K2b!|y)vZ6ixwU25s zVa|XXx2&THAMXsux7z0`Ar>rrXla|ruKo?5k)>zMm&=J+aC>^tjoKrL1vt9Vn_?oo zOWD;gC2&1rK3R$|GITL*4o>kmTBJO%12t9)M8lg;JISXf10UML#QYxwJD6~&Wl_yI z8wc)Fy4~|@8X}OP{|+!fR`zC*pEaO3oE?7L0`d+w17p53rI*WB45`_!S@jMh3}(TAX^$kBM{mkI7e}}FjHXYgGCc{KpOkyaM}Qg>Y`l+x9qQNfR=By z$jdp~QHhWYquD2pZG(soK$}jV_ql?+=3(`zn4(4PYL;^ATPZr1e9m@_GC-S?sd0al zq(4^e{(E(q+ql}dm;X(V_;)?27g7#=;Et++KE$YzP4JUajdPK-ig^+~7D?Xt{EqX= zy9?k=rws0-c_DMs^U93gqc~G(TAAO<(#fLrX_n{YMnnlsRvf5y}gA2jkoF|--{p@r1iVZdq2V>);JT; z65P;)TXH;=D<#D}+I_s&w|HP&OhQp~J5a;@yXBvl@mBS^;$ELmFs3{AOaBC0n{AKH zTsc&=Yd(6BXo@h-FY@e6`gCI|7`e)rY2@UYtj-qitM1fAuT|5``dpY~-9(Dh$}zQO z)vTeh>9TviZxa2++lne;tfCY){*MIN9gUU{*Ob%}aMGWIpbPw}Zp;;zzd1$LPo+q7?i2xwJoQYGmwsyx3dj$GOQFcecA;bs+3|p2+zDTv0zSHB>R{N zq)w^mV%Vx&H$h?zoo_2dNqN?^wSj&4Lt2*6Y@W)5UIjs0ssmc)R;zihU;FpzUC&W% z)9<%PEUqb8HcN@3njUC9O(eJCMlxh41q#^bZ7qD>21+ulJi2nO9x?PzU*3K!2W-Di z+&oNzmm^6ewXm~*X+|1q>gI42YxnYy-wI#6BdLbjrcg@BIfD?CnNVSB#Q?^#1&FEu z%N8+4tYwR+8BN|(NEF3DdU`y;?Z<2BwALCGpp4ME%p1EEAxj8?#WKlzb*nTeX-Rs# zxjE@L7I{WS25UvLhn|Hk)Xlz53@|2(OGU-TzM%p2`e{mG9-+IJISP{=aOL{00D^-K zbSG-$#!b}c0MzA)lpCaCD%_E;o1ZZs9~}PK z$NgVa*)dKSEE(cj;C{#_oyC^PWPg#Xw(MF%IyaBP>dJFD6C*dmG|T2c*p1zB-HEAP zWEA?LYGu z>p_a}a1KA^2t7uYms1|dIb;FKRf01bOWoS_Qs6*^5bKoTgX=Wob*$DR_ItW;0X^@ zTd3kDl$vXeCO`U2JZ&h!XvyMpasrB<5J&>99M7xiQRzB(j6g`E$0L7b?7{E0jhTL_F7-FgavViec)ep4f3%#04+A0n5J4%fG=~r3*(JUhwvci#Xn9eore+4hJypsCz ziqh(Kd7d;W(rpB8-5HLqDOA}F51ieh<$-NU`Pk090hle8VkTDwqtXt-@pPns1WGTK zXN_lH8QZn<5Z}|A!Nr}NH*zZ!#Bq6RT|%PIzFvq}Edc25K9b{9#f*pB%%RJZOde3Nm{YA|g2bW(|_j%fXE3SXA9T-1T3Y@O+z>#E-0(;hDYa zoY$8^!tpgN4CPu3Y2jM)|u(j231$tjf?TijL7x3|{T)Mkh&8l-V_dqNG+UsKaEJhz<^jEoY94F=Q?k3^ID4mA)i#>m2SokKcb^cb1XK zlE%;k;;^}QyR7hDxo|I0j1kW*A;A;|NcC(A%>8=pn54$y%YaN&c(9l7q^j<- zB(rtk*S9jLd!h=>J31MeQTO&8FUy#n)gk(#PG1TyLHBML0n{2;d2x^hY*R?qN$aqs zq??olDdc%;71b|x<59xO7VLCz1=oC_A1s+4&W@WAdMeCuVb9)}BUAT^{>2ZD0W6Ig z^^C4Hk~i=YamxUS3}W40aYJ((>&`@@tPo|D=Pu|qY5Qdn6L%zfllrT-Qk8;+>yA=v znNgmx{8D~0&2-fWW&d^LdO1oaKN_g$QtWqY0~K8f>R!RKYfh9T+8QV&zglMS711Rz zpx>iDP%WB{$BXYy)Z!B)PX!`b~ zy7OGTw?K=pKJDS5#>ByBn6qTOoWGxr?`G-OGA_h}L5a3b)#}bv%yz;6v=%9IcsOr< z5MzJvc{V|cKVZ>Ixe(00NQHA0J$E6%{iK;&jOa^hGg|c?zem`*E13>!=cH#kQd-l*h+4y)D zzOyNrF&6(7Cj5A9MGr0E*7U;XhOP0N-Y(aEK9S@oDx&5_7h=W7G7Q(+o?E=0(Pb%~ ze7&>ro$*62R}r_FrCVP5;FYcyYSoS!?Cjz*@cZmZ$~Q(-Wt3H5%P|IV{Q3y6vl#pA zweuCLt4@|!n889Dg(GlNhruU)pWT(fXs-aE&u%hzOntuWK!t5UWi4&)(iwv_@|J!!g0r><1o^+xYY^~*;xo=&J*6k#5vVftO; z1hsUIF(qf^VybVUS=Ktzz4%Fh_tccWhBr^s;%y?iag{{CYLL~E&#ghfL2j(|7n_M2 zuwU)C#$9W`U9?>57#B)62G(BB4n*C1@ex=pJ&r)^<3lWp6pMpnHPf$3z?RSKQKEo# zC17(!3w#JiQko`@UV99V=ru7*qzWo%&k&Igd(GQuWLr5KUB8wSDj-PJ-lxn*D(=0* z2fDzCPOG87or_zkaw$&@WUFJ$wNr|JCMo@#%M9h=nG@1p<}_w;8?+*ZF_ zU;Tu7w1?7r&=PE2)m&^e(*U+3+hhs}l=9{&*BEGz1*45kr!}4FBluStfNClQLr@1$O&tfl z#-yhjF#zVT`21rAo9o$#dj$|So=9X})|N@Y+N);oPYM+q9B>_i81(fFtDiSTyNET$ zv&Ld$q6iTCaG||zFFcWv{%Vjy>4I#fRl-9wP^;1_X1J)&G}ZpQn>WQ~M*olZd|~=y z77xD!c{6-P7RUY4#rg@W#P&h0V-!tKM1>FBB8_uO37FKPX$oGmt_K$>S{NC1L|eNy zgo`hc02a|O$2p0Ekuup?KT zuqv+q^0RSeWPW)drHh$6IJ|DzzVZ1&HcA!SG*wCBrJl7gj{}Pi8u*aDib_jzINTV_ z5r^BAThI;Wn=P(BC-^@3gVjrSTUED?&fMhGzTTbCuYf_R5&WfNMv|pCDe90 z9(C0Hovt-g`!ZO6mvC%%Ih{A{Zu!#EtfvOjDiAl_3Y_h?f=m^!5UO}%+!DJ8}0LheR)Lq z1i${v|4ZB!CZlAX^2n7^j!2}{x+SqL8#?geKd!?iY_k)97%cN)yXopwMf4$ndb-0m zmCalBK^Z9}NuHo050blo<2aC$^i5amiq|r(cH?q+EGok%tt9pQ|6s;u@tIsV%kZR# zL-Ad&<9Y zMh%Nd;)F+@AZ&VFTXPatO}tx!jb%}r)6jn8GH-vwG^N=v9+~uc=9hs%`et=tQNz~% zhvy3Bt5RQ$OjCp%=0oySgus`w<*}}DtJsrWn()O7s70-@jCDw>|1myzS@|y?2ZCnBA39h(;$R8T4979f7aEF8KOcccT3$n}nS+*9 zlP`CQ7=ulZXC+_-m0WK@G|Oa@_h0TyQt8KEoLeHDbWYzE?w(@nO=@=~BzXHr@6KNd zz_n?Sd?&2=aL_B5kReZGjd^)1k{a~AWsg!5Rj%FP#ULMj(VIk;Lwu?weOm|rjmB>5 zlHAHP_g~`E+@tG~O_%dqD@sj5uhUi5+KyT3t9YE9}wf{EKdNl$Z-AJ+m zgiF7YEj0!$di|Qa{m@PiKbK)LpmPYLvUYc)565Az09c~@C8UtN^ZKK_Bw<~uhB?)n z$?40TJimg0+Gj^7mW$IKK4;zDPD@B9@yUtOpxK61eo$FKVm8gPWcv!bKl{E+y_<2p zJN*dYV)zPXZe3H8h-z`flMBNdyikm?4sCI!E4V`X?z|Cst_bTIqJh2awawD3-^kiQ z$cN6*VDRr0F^X=egNEgZ#(|}56x)~o8~~``1yyu*s&s=_^(OPY3p*Dj3STKr3!5EE zp%W1MW_&%hZX((2GE%?Vh(``jNxXD?<<|?3)uelL(n5UebgpJ^4k7*VLiW3@e=}ob zH*bQrS<@zF)W_8t#4dk$0VHEE*@U8rM1ro42h%oVFcwoabETa1JPrJEmR}7_3>@d( z$9ueR-;=?H){Q?6Ug z03c27D{llh!D-3nsdkq8Zpb}dcFk$ZEAZ+yJ+2S7WRC8HEARWBv#2X_04D~skeLpt&@SVu_ZHfKWaG|##h+N$-tD;nV#StO2R%#{Pp zMyb7$U_}q7%y6|7%mdpbQ|%wyBJ3wskVqi&d!`L_3Nce)bcAPn8Hq&Qh1(iB8EBE- zOI_~4p0KK6XD(+igir4yP3tY7BfZ|tl=FKk8Nz!NEnlnM_zV#m>LB)&FDs|kD`Jn~ z&Hf{=GtToe%)!!+r6%}Y{M{}91o5-cR`Jabz8Nfe`QBDE1d*XorA;*xa*4!+U>llCQ>!jQ$+lT+u8lbyknz$f!?O)<4gc2~8vpA#*~U2T`;`lC5u)+Qdp3vN)QRqfYGVJ|F>_ z{<_BIw72$LPYORf!sO{wm6`En1jhMF{NYoq%@2#NR?BQN_)Dg*2~^5jn|JCkV^XAM z$?%i93|Jx?#RZHd& zaCy?PwO@YX%DV4jSo$>|C^3976l9B|6<<*IJPCMUNSTgM7o0H>@EhfEuKn`pCQxpw zuXg4`SgD(tmx&=*IH!qC`R?>uIq;Yn{53avoQI|0f3XLQ&d`EQR^OB)W%45EGF3_A zR=>H8Y`~}sX=GE@kW??<&N8Rl(W2P8NHpsVzs`=r-ZW+E`0{Lk=#uRu=~iI;7vp@qZIr(4FF<01{+26lhvtUf z=V_e0R0^tZ=_x$R!=7f`ZgxGLR<3B#(xj*+kIS#O>6Xt;HVToh1U-HRA0;;i5Nyet znp#s6ldglT2c3aVi22zBj|+_@KZzsuUfMN9nv+}u3fDv9sE4%1mtt6#>iv>G&pexT zlm5CK|3p_*XL_*SBE`c_HEq8lt_Elq0s`NJSbcAuhq$*zDDiarn-s}r?nzvrV6~jl z2?7k$8+lvt3_6~i$qBZUcvIF_+sz|BQun=b@~hLWZMN%A)@%W`I1C#z`=kOOc#M-I zK;SwLhE;6~8yfkNASIay<*dr6&el3sh(viJM>r{Zsvkjmvx_C^OrxB9zFFFsmhFO-^WDwQm` zIkM0&HX?Iy8@+R{mNNk+rP%{EQcQyKY)%qbjKG%nwrPlp#dvZqF$tr3KK$fj{iK|I z#8d;cFdPRH>pmR~IG}~vq@?uS=liX1sCTLkQ?%O)brpJ&NtsU`zKuYlZ(zL3y3Krl zDW_JgzBAUJn0QZLlz3$$H4Bp$^rJC(f%?VbKuXNEJ*bfzbZr;;WWM{DEj9deE!5ZtD&T$heIziHz$%Q>DWC?X+*Jq1mPO?L?z z6*lD@v)ufwS2Jx{v~%GF)Wq%#3g17c87 z@ZpY=D5S?~gUsWLW5(FuIBM*Idl|3!IOis`y^rZ?=erJd&!bC>yHX8MZBKzx)om#9 zeEr9*CT_MC2?#DX1|+`->@aNqGV7wY9bvp`(wy5@^IGStXDSx;2Ydavpi_^rE;{d#D5j+WyE{O3-)*1@POAAz znvFvgl}3Y@^{r1_GV({L^e>YhLjmjpA(-D5r~%_$oGu?gU_dor`{FQJm_FMpEPrvT z25D+^XqD#K&J8Q#W(UBvK!3>ka|#?VO zaTm93UO8iA;>^shKA|pSt5X|&F*t|ZZs2KYepy+bm_9rCq>wBx^;||;FIcpnDgoc{ z5HwtCX|Um1*CCSe?haJzyum})Ebb&ckV2#r6Q!@uiW1*2P4j&O*r{s6MfBaf`H>b*(%Tr;P!TqP{5_=p@X!uzVF#n7DK-wavC+{v z)2(AvGWl#?|C|Siw!au)k5}o-o?!KqEII`rT$N~yd-k2dXtF{O_x&amYg$9Y^|r1n zsb=mX>`y-z3xE!7q=hpN?CYAG%Bk z{Ny#DaTPPqjReiUT7%2YpuuK)MIIN@7aN&gVk7?Z?NawA`SmCnlO((JQIDz3_ny!{ zJU6#L?*;-^Rn`jiGk_b`kRs2AYV;Lj{i&y)ZWh6Y58BO=Y)iOC9UVyw_1O}EG*`USsz?Qfhh7KkEZtzZ_V_@-3ksmwx~$YxARrMXG5x!$1sYh`8N4S^p4 z@VrBcU?NwO)Qn`W&rPLzHCg0c#JZX&=~vCS#Cj^sbYrT2ngRai3)s^+2Kazyh%vhj zUNeyu8I8Ww(qOR9pIFxV^fZim@B(2O4?fuLreON;QQ+3kJtO5^DHN48KdW40Gm4hQfx%ofzi>sB*f=1=-14htQjsP%5%M!zKblJ+qJ#%W?MKZ19$FA?j{kB z$3yly57@J2C~-5(o5Q(9_n&3n=Q9<9P*a&)P#jg-lJn1oCKMp7YnfksB#NSD$T`VV z0Gk;W&{9-hedt~^yhpyaX+{;uTF)>3a-6U@*YIpBfQNz*LRF9>iAa!s@2OY_il;z3a;)t5klFhpAt?iTzd(IrBZEtxkUz<~lbC=SYYB?M&>l;3N5-5Zk5>FQvP zG-(jdx0ssjfXw|E{qG-%e0&aCezWNU77vdtx88G-M?5^QcVKV(e`^1GlzxH8^*bs8 zL6n({N=i(t>eCb@XpOV{4+;^JPO{S?q4?KH$~0tbf){`6wobpILPgcaf~e-mKC%o3 zfaBRE_@}9-yIBlKWBJ*-T792`E)_(p%`0~)GJc|q}sz}}h*w5N*8PI52ul-mq6o2K_S4j5_V zf3|aRhLREhzn>IJkZ@AIg)L?iUa{{^Gx-}w3(|y)U$qo1r7?!iZe~R3K_4i--qRhk zmiHM$5!8e{X+){(f1fs)X)C80e}>S~F0~d!eu_9D_3qIMV4*;HnyN*4ehwcuQT59I zvBvtV0U|c-jeo6@%Rch5hNEMgnej)dHqPRu9|1d&CVUNdj@>oo0Kdi0q~tg{xf@cw z6wboWhEmDntF9zdQm!`|CcpQjUXUmXijcN<@EGy13x2>CA5Re9G!gBl-4HF>8o_t5 zbcbi1daOj1w9By)(z5L+Q6g^nhvD>RM!GQ`Hza0tF7Yf`m9JdOtmhLHe$J%UV z2XwF(!`(8<^?d@ZH5-$Y9)}!u1x46McIX~GQ-sSQVJZ-+*7YpjORTDc2Hh_dIL3Lr zQ^eXxN&I(KTWMYU#S3bNxso|s_1{4lSt13_@hI56lg6V>FeliO6wb9{l$!MR%9*61 zVJ(=8R*&F|ii<+8ZWcRQI6$KO&J!`O$T#f{3%j_Kd(UB!OevlPaC?*nIqzv~2^cI0 zN5S2+q2)(0qI`>vb%WZp6Emmbam+GoXX$Mtb;7fc?N%(eqNzBFCu;iL9|cHLP`X_L zzQlF>(ke<_2&SQL*xv+rh%@L%vMOs%?R$-?bV7=diiT8#(X}tyenHsb+hrJ3SJ|IB z>To#8?mf;w6`iE>g3EG!YB;^M{!JHXyF(}aXQt~pd&cM!Sr2wPx!p&__kNg zK;-+$cp7KLt;UG4u|wH`lJ+<>zXyM4L&kRT@JGFc<-}~oUnKM zL3Zon2VC?f76bH$w#An+EXj4yd%mQ423l&nIaO-lDIGCc#x$8iJj`oaPi6N}J`~QS zk5~pe@(*AAd^WOe4cbDEz-rNJ*#(*7?aqq?aZ||v;EnO_O91fp)|$}pTe?0qmjO|+ zejZ9ic>_n)irGBPf1Zkou=k{@5epkApcnZdv$sfTVhU!H>irr z!Ap{XEOA@m%n&*`H$1v~k8~Jr?N;xEeVsVLr^MPqYVp2JBy30l&($T5?tL-Q>m&L>7;I4 znU+^V+##m02gdRfVh)qfx+2I$lgDAU$@3|e2LAS7`FRf(9*dd8UU~G*-ioVpmjbP4 zgu8qB_!lvTg$Va9f)u-TEl637dc0(*$= zWQTt{7IA_}r7K!~W~Nw`kT<&}o(Y*RDf&J`gvv!t}U z-1?5rDI=e=6Q|7gZ=8MtprO8M$>HMJmJ#kO#K$$I?;0^O0;wJ*{IBzr!?6-NO^6BA ztj<=ui`v0Tn~abwKf;Z43ulz;xs#lvvvp+(0+dnKF}&c7xT8-l!dfyVn$cfoMd=_k z(Cz;&Ow1Jp-FLj!&(V}SsF1p6K&sBKAz7kolXF?o@FSQ*=*;#YSM-e$((8~+=JtkH!o z1>yBHI^8F@W+yD9=lNsV3=P5-=thJ!AsC@}blX3QE%Ca|Q~-4UK29tOlG%3^5$?HI zafYQf8Q7c^5Q*1Q9 zYaiaiM>Exv_ig5h$E$YyuqUIp&5c>%LgKH&S~K#luSgAX2(4^lOPwmeR6WnXS@peV ztxU4*?O9I3L|Dj{;L7Gk%A7F{>A}TMaaqm!L|y)Z=da^D@+;3P zbb1lwW{7^#P@K%=9J2iFZJ16Ug5TP+b9^!Xc6sU4;_rLl?v$VDYt~}gY^W1#0ta94 z?7PjV2WKsNd_?p|k5Gtz_1Px4jx0&$UGM+H-g`zxwQc*ppdgZw3?hg^k&#TH2ug+` z7C8%&5hO?k5hO^?IY}s>$T@@L9F!=bkRnRXK|$7?h5PKKYwgqSd+oM+-ns8?8?E^P zm^GPmjNbd`;rHj(!P(1gdOm~UJXo_0dv=Jiqen-3{Wp6YKpk(>%|O}3Vz(1tLd+)5 z?*O2Y0lMeynOn1kOWTa!t+~Zsxuu6e+B`?A@YYBaXaS zA6O-;S74QB&Ooo@*|r6%u0MF?-zuQY3k|&Iwx3InB(V!q@cm7=xhc;2e+!l2}OP+WlYD? zI4sMiFOKja*;F%P7wvIb{j?vfiBn+46T^yZDnppyGd78%G<$Owsn1ghI*LhouZ~!I zR?A9vQwx;tSA#L7JL=9aoFFd*W?6AOC))Em?XgR3d zf)maRvK>h_>R*VL(<9waDXW-E1ttdx5Z zMp0k6!A#qhN{Qgx<4a&Gtv8%BnKvX~R>D2T`O=j(cK71N^MS5~rG}iku&jis)~IML3*Mn(()N<0Ky_F_5$t;#s>=c~_nW7w z54W(@#_Z8|b4hpIDGA<*_ip-45B=w74gH)nn-YgNWb@Rr=*tNPmc1+AN?^>55tM?l zMn(rOL|PlJ-%O0}7ZPMtr;o*1L5J<=vHgKoXX`9G5T199(7pUfiIhape28ns4#LQZ z<2v+N+g8!dR@EVn5shT2`A#Kooh6v3^-+9Jk6*_j$vah5UVqj;I`r$!`Q!E?y=GJK zHj0nfIYme!97a4Mr%vlKPjYqbzhUh|bsy!h`QeP+@l3si>XtSNfTh?6-Qbqfdju== z3m(JQYvMXC<(WxwHqm>=MzE58%96LU-}TR=Syc6C=^tn?9oc<2-{k`no_>c2whMuj z+eK^5*So#S@87*9mh=J>!Qk7p0p6b~J4)WSIF%xEuT6o&T=)K$yw25$`3h+A4|JA( zn&3t0kIQc$Cm7a8&Mc#BCrh$o@Hba^<^i3dhEv}_t-6>2)nhUFp`xXtGa1W}VANb8 zwl?;wfpTbJHg!6&;_J@THH2rLbyJ|NbM*s#{yeYGfR0q5h?5zhBc;v;26UuyJ+1Kq zIHdGYqwdrI?LiFS&)(&9{^VdEb*XBzT)4y!_I3(@F;YW_w{i2uoSy!!CqiYd-JP_j zyh$-GWF?aC$YCvjAQ$6#VNl>i@-B0+r{|_uRe_l+j`Ki6S@+k8S-mv21F)ismnfSs`kO*a-%-<@45 zv?V+2HFZ^_UbBV*H4_)a)k8XGbcB8r0@_!npF6A;T?oO$fN9G~8(`YY((zFWKCpq<&pmKBgbkuQ2l=NRjo60 zv}kr2Zey~WeIhBz{|8zprVe?G%_rAd_K*ps?_tc28(i9vW47icIHo*UhbZo=@r-4=ddiYgpq_xmnIsMfm@E2TN?ii^bCD z1<;(i7iN3b5iGevnW~{mC&6ldOm``pVi6RQ+@z$}Z^lJBSgHm;BVCDQQPI2Mm}u08 z{b1hMrcB%se{G-kmfmZ!Fx1*!94>Ekxj*A3vK`&*zJcSY8Ga}AJbTt%O2DTea3J zC8v;Dxa9Se=)YY*sj>o58sCWEoQIJT*vdFN?nKypF{yUE@2JJU8-c-rs+emv3mc8?*HQ9H1hm2v;Qaj?tDE(dM6Kc zQzBE%atwH`2Ft5t;RJs$K9GCnG=(-3ZPb4iXfE>g1aSh<)k8f7GD!*BFd^YR?Ct7WWdxaiKU)!w`CqRDKvhu#C=Wnn zc7-&GrMJT8jm}4qUhC@m4vBBbqeaT~!1qoOz;x{o2RD4gJnqCukD^Svd4SybGC=5Aq$xy_6RX zs$jG6WOYjxtlA3&c6q6-dKUwezCMq9<{~Be$LL=CD(?6HTfKqCg1^UabrUS8sR1>G7@BghMGqD-Z43M-n8)57iNb`F}90_BAe z(twQJEj8lWNdqceZk+>8^}{O2&~{wD(zXr;i|t={qvY|$YD-bkYpU<0W59XM8kqS7 zyjdVfhqZD|<0Z>v7ql{fQ546#yUZ_l+yn(vvhaV~nY>h}CvdUV5g$COzu+4g5D^BR z8o^05_O62F&lcJ;+P<(PLr@bLM*9DF#v zvfoPzf1m+j-Rri*tQ@niCF`7MxGIz7rF6HJ0>sI)IZ*(qFjCTRdO$alh~kk&J-8Q0 z3D+E`?_snXi23}^zy6^C#pbHf} zJWD30>fldLWfsW*BoIYj-h8UeT|a@)$R#Y%ipY4=Bot?|xGDus6UA$8+Gxn((xBfg z$4oO9xdm+5*_J+F%dVDLf1xF4pS8yZ;HA}o&2Cwxs z)!y^dEPY>~UxDb%G3Fwk590Woj70-(h6;5Y)B(;OeewcZGpT@1OP(NyM&0cX#1g;0 z0g`B!1Fy3FnGbS^CKjz2U=Lk&>+1i(BX33d5y0|h#G&dxe><<&*;}AIj!($TZXBwrxH`72No}nxEUX;EYb}662^xQw=8&Gi;Zb02DR*)tNEuKr zw(RO_eMAzI;RGz614RAIM5F6u+Uc(~ziBmc(h3`L-eX|>Hi%8 z`a(rfQ8usU=KUIKL5k$Kj>jw|zGAXegEf_s^q~$(v z9Cff+$2i?Aly?1hzWe`=?@cG;6)KL5u5XkpgiH*&_VoV~;r(y@p8s=r%$KdO?!xnDvjUgMB!u||MkCx)O zIs5;eT2cYCRi^5kd6>(_q=d|VU_|=Wn2>&=56% zn@|>+yZ!4;gg$)IR62K$gYw?1rF}QE&v>eY~Tfdh%Qk8=lR+6lK{?DzH&J*Or7$Q| zoU6f^O|2p~?uhIvxIJJBnn#$JewKFUjf{=>fxMYmtC7GpZ$r|CWVU<$=CE_Fvr&FP zkbRm%ApUE}S~DpPtH|eA6Bw&ec7ppJhMw$@f)gLjhY)Tn`j#cWkZ@+TQ;EHe+sUuj z0p5;gdbGC1giD;uLnWf_uRz}!r|~fK#dNZFKmV;LZQ4K>o@6xI!)5bRY1k@rhg?fcCG8Qg$Tvdc*jb@oy8K67f$R?p@ zvT#}A!w%3(BIa^v#0JYZ#9NL>oO1&J*~n}{rPY4qPWf|q--#!PLcTzf_tLrP^XdqC z$_}e`^`~iyL+gC9T>>V7rsIIou|DHhVdL3>uLy)%;%)S*erTIQ-Y@}=u{6=tbnc$5;oE7^4(D>MZ@=9ALDDmw^jZsiKwdW3fT^y` zMM~qg{_PXXJ;Z4I?VCV7pvkEq)ts(tH6@Ha@){m2Vy3mH&0vb}M%ZZZKE4j^OYb9$I1qtdLbFxFYn^zZqXsZjE zRn0Z$Cg;QMZJ6;deVcpznu(0zJG21dM22hAPxms^ZsGZ!)AH(Q$Z&sYi*%=b_^o8n zJPy?Gl?hdYjNmx$lscvo15km_>Wmy%<@=!DT2f^HBcq$a=2hVzXzLo$o>mH?A=4y0 zxU8%(4)9iGde^aGinTx{bG^DzJ2aO8Q3?Al%vJW)2>a)OhjBf}ZksIDIxt|7#91lE zVYo6%Z=Dquy)N3-;z_93yk4WR_|l%!+2G0Ya`kJeV#*oFnl1UQ3OXs>F-Y`V~<>+h%6bI&8sdpf0; z$o-nB`vp(6vmT(Fvb5^YUV&K&kdS%7%I#rx?Y@-H%ph(Hb@ z5R|=EaUGv0}biVL=rm^>kq*-PJi*3DzQ5+ZeVtmWSN_%SA=vKKZC$m5Z6 z4;kiw>%z(W2Zj&|+3c7H%slY#Pk=r;8onP|X@Al^|3;96H~h%@x-+u(+RC-%1HXuo zgPc?2HXD(&=i8$|n!mEh9+~OdZP>Z_x+r7>qW{n$UkZU-jR#nuA>WJ_%#lC8v#3ZV z>GjY{{Y0&;YMUIIG$n0sBmS!2=`85PA?om+iuZkR7leTJlxAU{nsE18?R87eDz(lI zoIQUR+0sz+#ou%sUKj3ucEG6{b>V%$aC*T^qbKr`nxlT8Er90YPLt$u1Wps$xiq;b z&yUf{m0cYmrTZ?{+>MsQiqgRjiEVSLe#&x)h=KrR!w6H?`d`5nbQ4YkJhCgeDxVs3 zr7tipWWH~9 z1c0Uu12=Cf0U1M~OAB3FdSKf*$tdUF_q|?`Z#Nxr8Vp{ZIATLBS~^D??2ySAZzSAp zUOGZ2KsXhAS*WP^ixlh|7$z|5VOulG%V0-6gO z#FM9&uUIgB+^?f&oE`4&b-$~VJBQQQcB7ao@_~l=qL*@;k z-eJYp>oq!^Uu4?%7T%8(yd9oM0T-S;A9&Box9#Axw6OQ>!n+{RO5G+!;c}meCi!)m zNWr;144X1rsERJ!mc6vWzq&GJ5~Re#HVy}|AREKizKVC8h^)}xtp-)|3n?;2%P zf}G2B=Q5U9gK9duVI_!g%2t!_%$SZE4IU%K!RX8JGURUAFd=3BD2j@mzisZ| zq7lnGZ}nVVj@@*b(YPD_4QrhMjIE+pw0YyuHk&8#CK78APmpV7Y?c|F7ax!FL02o# zmVd1HtMt=#XrY~Us7nyolniBCr&Z}*6=QqU#AG00Zqw2s&~V{Y%8Ttt90;$??X8DW zAF^D5u!87cZ1!)^td91qy-ezNhnMhDz}Gkcc{TmkFUs9t7cxgmvWJJ71zqm?dT1Bn zALBis#i5zm00PZN^_G1+*;aglG8W$98Sm)S!OZbroD3Z@9#4}tzuM3`(F}?HRdV15 z^m29jkiv#Zv=ho(5kU*15dw4hZjI6a2J3qOV6bq0yaLAdZrfl3z8o$liAF;w{J~ZR zbS{UGuZWtrE3&uyb|F*F^njesanFTYkld5nj$~Wd^);O3F9JJaDK>H=uZ?X6ZKt%6 zOm7X-8JA)HlUh&2T+c?RMh-`REu91fLHa9mW$W-2Wep`V3JtJ)%U%pfE*Cgu&grQj zgwtO0JZ|#4%$jtutma&MVfw&yRLR4I|0XhQ)BHRCcK31R59*U8_vyXB*7D)D`_y3efH|zD@zO`;jB-*?<2A>RBNj$6OqO(dlm%kNf1S9ULU8HO0n; zZ_VqL&`Vk`z?%#Hk9=fI=7Z&+0D}*%RsNXI0172VKQEx$QW#dE_Yl&j!rzn$7x(?I z`@;zO6?(Nx2O2nwaco@wK%)UzW4Uur0oK^Zw;lf%uEm!}Ur@{hx}+)wfbcK~FDuF# z^B+%>4($$IN1)WL8>@jNoeL@%w1X13MWDo-8-eVYi!+cN*x3eBDgXYl|1A$BYc$Dh zr0$Ss80>GF%16%-fpeu|y29ug_SmU2ba^>k$8_K{Ax^89JTo69M*i>j@Q?TKw5c4d*; zZ}=W9+`%W1j@Yh=GFanFIc>7qZ7!QD`YP}LK8wh2GXK?>%0zf}SI~t@P|D-WDK}M? z7X=F%6lzXqexe)}iq0J@@v1&Td`!3dr~|1kjK19#(5(R_Y1`O-cx!q8@eRi!s}(;} z8%w{rp-7$rF^>0n5Ceq`_^ef7ufeZ%KBjI5sIuo>bFfepjOHicJ^R#y zU%wl~y8AvLiu?WpApvEc;ilAL^^$EzJXbIqj+trvDwU?^EW9bEk3g4PSN1u>qozuk zx!4~JKFP_)s&u#6O`$<S@MRwE|VkpIwr$8>Vk6+kOM-`cj9zoRj14%r!yv7L9XhCYk`oj%VdQ$l0 zSVvuI%%l5tZ{ikY*fFS$Zy>pDJH1rMPp)NdSNN2=5Y>v^^O)Y2bx=i4a>;3&`I~Qf zNSN@-cd}YLihNWl1}XFYpsWLF3M~ zmQIwibO`WQ5;d~QcFC=Pa5oce)U?~~GT6C%n6Cn-a6e_G@LyAoS!Hb}kEKAR+ME&$ zU0WFOfmEBsJpg+7@^-jDDY|=?)7r?N6Qt7foX$q9j<>^VGVtB~oy&!EwrVwc)n zS#ixk*Bed_YHmn~oEe8u2DZtLO6q8RH19{TH2wI+=rjcmPPV~lc>&NJcjw{vHu4-! zby3MHj;e13kAi|MvGb@a9*x7UAzg5Yi(m1B?No8v^(EsMp~Z^D9EF2fcq(oDrY)H+ z%rpHjL$T3iM+zt&2s#ObmQnP+@6TIDKi=rH8wD|~2EWA%PRYvIxITiJvIElY`UEHE zwR+FDJA7TFY25Gkr$y;tJe+jr9HPJ$KoRMYktmrqvAu=coa_?(r=R1UY36^@Uu}>6Tr+}k2AM5$5<`(PELR5`kUsb5zpk? zJ7xWvvDhuD;rBH|JH9x=fT}0tyqdMDm^Yo5gyc{XfS@USf;yqE`SKJ*$7V&%9BkfY zVOsye&b6@U*AT+d3DZf_(bY_npo9{`LwL;FpWcpJBg-|#JK`313X#l2BqA@C_q`0P zX8yT71B%fKa@{C>9a+!_`8m&x{z?-jt`WD=P#H^b_6uvxN81}ezW0}M%^t%F=BC1t zf!w>fs?@2v;Tvy9x?wP0^3)N(3K!;KOT9j-uXhIrr%M=PPy20T9d;U0az5I(GZqKV zr9^5_sqds(hu4pbmbWS|Swq5#4CF?wZX!|RVzNP0x+U^S&f>%egAB*s5e%hUv?B*pR`?lb!nie=?!!^CI~JCj8m{A+$7bJ=oLK|*EE;(l@e`{mH>jO$)z|seSZbrKu2tK{loHZ- z^;RJU;4(#x{kwa2){(6ws;!QbASN?~Nv)?kb!UN8L)c;m5hRy1u?6amX!h7WG-p6y zJ~Q(sU@kFM71B~KSc*X%flXsfm0_aS>^sv8oe8l2v2jmidAh$|V3Bom9b83_*;<;f z8i+Yu%>fWxggZ82KeSlPosuJyz9k7VD$jJ$+H|G_s?4Pd+YLoo4w}w5?^&LPX?jED zf`&4}bDZzQR*=KB?@A+C3iKV)RJJ?9BMqV=?I*ntnEe>eg5i2Inf~$u$7x*e)y9hJ zdjVm@oig@D<@t1V$O9N(zx7?8M4*oqoKlGpjBQ`{SjSs92&NR*n2CEJ;1k-eTR^i+ z6~V~0>@%Z!aLCHsuV>TdNx-*8h5R*-9AKthxg*@)@lHd#LyVi2!q`lzSI2aP2;x-{ zs_h)$NEqvFU{Cr_m>RE*pw~Lt7op9V$0u|Ro=@7BgESPKDwxrB#~LsW3=dD3 zm~KNQx_DlU`!s@$^o~XpnWJtBsL2y9) zLF!cbG8N@Lt_BVPq;6DRN!?XY@Y&2(rAE)*5j~3*BUoQ$TpHuo91MAcEkEZM>*bwg zVregr$Fd73OMgq~5zoB(G<%W>?GcHG(-MW<7WOl}M&^EHXvxVwLd}8nTc&dX0IO?< zzCF^Fh4EV7{TB0?d<<{YvKmHM3`&SfBx$#P<*@(kB004osK!om{+nf|H~!gw09Mbh zLhaer*K;VZ69ixPgAI?~?8_AIP`4<7w9Iq1Rz=Smo2gjhygKcwuyl!d_4Kggz{+6* zG~Gtwv66S|LhvV}^$Oebx6azmu^21U>8Y@fHRaeZ-x3jcUee?ITFE88rr}i&@78d^ zWBty_Cd6Q*1ABPfg%T<&kMojbc*tq=bpH8|J@HAe_cWHA!us;&{Xa*c5 zkU~Eh`RaKZH*(jjjPdQdoEr3y7aQTzlR@q4xj!pEC@;#B>j3G8A_rx!C7K~aLQIQ~gibo!Rrbf4q8JQy)A zL4A24uQMjUf>F`VxZxsr7^8TrJ}+X3guqs!Gm;m-Xb-)Csyf2rmRYf2u)ZCnm|iX4 zV2$q9-2=%#Wh;`iN=j7sa+Z7=@V))gb;R-D0~KRwZO^7eeNyzd0L40{RpP=ym>Vd+ zalIjx6&&F;K<*js`!=jr^V&N;Z3MGXw>&_1%t}aQqz$H*Q6$Gx`*4Me+`To{kD@ zSvmM>{5jOb@}u;wjmjR1<*sVmuj-j9urYqYds<3s(bqroB7v2dXhoNmiHibmAQO|M z*AG+sShq4hUKc!>w6|BDTju~(=>gQBDP&!Jt)K9tw~=f>Nn?uya)=)x?eAsB(g7qk zCAKQsc*&sw(BeKgq`F7Dn44==a@}p+RK0q6oaQM~_OP>QZ0wMPgk3m6B}T@EQu&RP zsGcIi5wz9C_zAVJ_I}KMoU7_Sw!~ahb@zW_q$ETW_Z#QcSkA=2^oW!#B-B21uL@>K z;-E^3t9b7_Sq3dP@tBSOjW0HwIc$$_7QH44O`TD3g6FvOo`v6Uca&s=4!xfS1>3ax z-YD=_oz{}Jl^v8b+;!L{HQ3i6PI5aX@3##FM0H4MZ<~ZlHlQ3EMD2L1@*mdS1 z+d4vZRC&5L+t9H!KQ+Bi4i`37ROrkfm|{Lgf8-O+H>5YQ^y`h%Ps`Q-t0HUTW3fFq zhgaug=KY(_31mKVMRr!)zlpG44eY*i*Ixc&x=cQqJo6lmaJs(MDpKE}FQ@x$}hB7u~&N{2MQjb!vq>u-;XZkQD2->z z2Yeg9)2Gv*zmzzn?Nd_+IBOY~|Dp#SaP|v;VyBiDqMDIk?Zf_l91tjC4GBN zs>WFHaK`VTl!htVJAEiBBI?A8iah@&=Pb`EBka%hM2A*^Kq#DSLBzJz)>b_*J4SsFYn( z<&J;d;wYfefKqY*U!A8H|D<+0gJ9H7Pocw@e2eN0*IQ&uC_jd4=@<1!2ZCVjx;H&Y z>zbI!+Ecw>tp?qaG+BvwQIz8Yss9lCb!Cgb3W1~2@PL`YZP?=XmRs=y;G_Z|E7cYh z_KG-jtoA7ZbpNZwEAgN60aT)c5=Gc6js26PF#VIhj6xDeG$m#vv*Piv+6Kqu6AHTs z`D>3ZOmN8M=WGrat%-)K`_z7&O{IcRTXHa@Y9$wwWbg!nFdVv}R)emBVp%m`7i6gV8g(4nTh@l_IpfTw zaHNWCQ`z2h(?Vw;Bo51roea_V(q8>yWj%ZhAKZd!+VBSjGdf)*?awq=SmSMO^zo_M zVi57Gv8MNTxS5$Tn5jzNij}o_?jt^g2bp5sh^xZ0uH&qq;^m9_ZHe zSl^u?8_)(JakL2F*VXvBfRRau5{<oESgl>+@`{uvb2;1_dVLpmZ*ZU}~~3+xK>6e`A%S_Hs^#W3+>i zr8%3En;Oo?G3~{x`1q571sdAREb81 zFlIC5P>jg8uGh);}ZDtIgo2_@*dz^VxXRmpq3j%k^u1pTmnehAPx@rCwwa zd_C}0VtVj=hbBA79wL~l;m`O4AhZQ8<(Cw71pJlqR$(UQ5n^p$%~~H^5;ehX*$>`O z;X1QXxO?XEQX~C!2l0V5q+--awm$;jyd`szi}rKPw~ShpDX0JD)&+xGw#Kd~y(ym` z-=NGeHzH?f3{owLtdA5X4TZ)mfe-9Ew{xp@w4`J!a$MzxpCZw`X|8BjSFe)J?J8OU zRanWUY=>{xqd!sB-w(a0Z6uE7QN>P7NWknW&SUjwUtQ#*{sRqs6%LycJGx;DYU4;U zj@rXZSc?^3f>-bc2Ul#&tPF5u6p(tPEQJ8@ypaR1#!rCwfRa?kIXC+)|J)b-=Pc|m zwJ*7Z7^ngi}R7Ut5gT%Apl+4c2UKI^tmJfSd|KExk>crX^H z>XE~EFSt#+P{(dFeE!BJ4>O~*Up}eNak@-oRf;7eJMq{6HbQxH+qKX@-M*v7WHZPL z`fP=0V>}jC?Gq-8L~{!yH2Ox`Ec>lf4`N9+r};_=4JLhA7DcL|sCClcFGJdxa#j+q z^{0Bt=f40nWVKeT`UVEU+p}4*clCQj9M*Q-a?K}6$-pZBAVQ3*G!;;QOpm00%3de- zfJj#ARVo;cKPXUj(vK$JA&wieZ+FM=eCy#vkZ`Ol-?axYY%P18iC{M&wi3$Z>R`3= z95_z>tN3GL5o-8PLa1O=RM7Boax3+h-y<)o|LZKnu2 z>}WIV);=&93m2yP%8m5zo#nN3Tj%rlZlqske zvoyft?>y>NvbWU*!8Ey{K6g~f{j$trSZ5VgYDP!N>h^%6a~B~y0J_TA>E1@00ySHT zFgx+)n>xfcEqGoF#V4D_1svel0O4t}JUQFZhnjL7!^Q5Cu?D6ZQ_oPZLL za?7bW2Ur+iA0Aq@BHMoBQA-gQV=)o-po4MG=|OpIc7e}N%BE=kbwjQwb8IirPsOT1?kISKJTNXkz===l_$PKk3Y#>iC6U) z+eF5(oCN5nypQp^&(A-u=FpWpCidMN7RR0wXDjK+YR%WIM1$SM2r`f`#b3bJu)6g_ z!CSDbD?i{obd(dEsfr-!$hWmT&n0`VC^8GNaf9>2n=(HO(55}l%paw;rn;t#a&HXKI-Co}3gx4RHoNG*Hso-Wm~+FmMeSJaGy zBZ&*oR0Td6VwZOmq?;1&uHGR4!w~j;3whT=+Yzl={KZns_t7$44j4HQZL0*qM2L^J zAMZ|6zT1{s+cq5<&%2wD%9cOOa%WUu<5eA>YNag8!%8Nhk;M*SK}H5dcT5mdi}rWv zaN0=Mx#r+IjX(nL`Hv0I9mt8tfK8d2p(&#cf_j73Y>J>Fuo$8cC7*4n8lD%N9W#A1 zS=UxUsqR2^rXq;bR+|h zw0GsFE2FpaSloKFpwLIj)uXq#Sx^~rL3AKP{x#;5Lv`%#6=92J0IGa}2?N?u`j(3G~SYcMn?cPH-N@fJ5Yb&d*9YSVCf-&r-l zNT$mQF%lZH@Ox)I4gBd5LNm${r(Ru3>PY)!3s79V+j^5V^-4CFM7_|jwcnX z8>*3zVX|>^(f~w;SkXG`PDSFw%y5&9q0)f5-!Kv+ypv)i+{_hv+a}Ly4x@?r!*QO6 zvCv(vn?hK;FQt^#_dBME*O)AHm<1~oTVUip_~b=5EO=wgj&r|r!td_rD0(!RqpPki z{W4+>-tr~0=+ktJ=^78^%`@=QBWu59ezF4RJg}_^(kl#7{?3AZDDLkN!Ikw=h+9<6 z*vdMbDY|@SbhcJKWF!jJlJ^@jg~|hCMFlC>)cmohO_){$Z}pBEB89KR=60cz!tJ-z zF4LLusu^d)gOG{3rpM+RQ8 zL}RgYg+Z#t-V9YTn9Wei>+lT&vhG}*z}JG5Xm(=N%?!A({7o>9Pa zbneBx%{RWdPy!6n)uE-g=KYFF3d#obv0t3yMm%w+&(7Wt&f+*o#(f%vMdqcIFd1O| zFtCVER1Xr~fPsjSa@y)-PU^dFgsG6fWGv=ZCM*VGBpO+xvr8X3hR(A)b~ubE>O>1F zbMh>e2uiGy(ol+}IxAW;%ZbO3EN2Hd7Ade4{JzFm-6dIUC!XwM^`I{;2nPn8>c&>( z#D0eAxy2UW`#RDxgX!l^4O{B z+vkRIt*%AQ^N9*Hj1V$~W6UMl>Wq77l61^c})A>Ost69B(ok(XjaV#5MaYiPZ8)+1=<6omWG!C0QNm;>$v z=et)`!Vk0TBTKSo6y$3v7|l_!q>7F?-ZyysTTg$FV}g1%rvmX-fn0dg`Y^9+`N$mr zy3=Fpec@SHFF&#CxY|uTvpgU9>{je+(~6MnceLj*(|El+FA-g~h)jEFq03`*a%s!H zL4~;igI+#Om!=V!zc_-wyU`1S|ETT$>}vU2G2L_jD%$&Qhf3OCY3_LST1G@4?*Csd2`I_|&5ykk+uCf`^oFZ{dr_5Y?mVL%lQA`|e%Mu8cI zTYNKb^eX-NbM4!8Ww3W9Pn*?3@>Mh#GGIOb7xtt6mvn`LOljhy@LOh*RpcmiZYjZj znzwI^f7WwK*FOBwiS~d99Ubl3HT3J(FtE^3|IpBH(Q~6cRUtC6_ldfz{3es@$uqn1 zw!QsN$g;>kX3%b;engc1vVFiq!7n(>=tKQC_Gnp#|Ky9Q)-&q{)WYQtwBwuv&x+l6 z{eyuPZI=PgZ>C?^8&?uM@|Y{R#eE;UXGxN7-+JE$hG%$>vsdhD<|M0)hf#VQMN+Zj z(Y%%XtmXBMXZ+Eu;a#(M?0#?MKGXSPEn^1yp_@hlLQ=Pv964Uxtf3M+#d~=47D1mV8W_E$?!K`+N5-bEHBNmxJ3>l?}^=6lx5A3GRjo*Roz&GqbdKPOI1;JdQI zMCGN2r2`#L;+%b+ROirX&%N(>i%ZG1tdy5?R}D?_mbesWXV_6u)U`Kq8O66$VT5B< zz-^w;joax}YL33=u^hf;{e~IMq?+*VgDS_n2V4}5)9WN#k{WD%Rc$ezWUN!)V0a*j zTNBIn@6~EfABS>HO3e~Nn8YP1Dt42wVfUd_c1{DYh{!bMF?@-xIbtrZ$lSb}`Lt-e z&xu5e6T^Tf$|iR#?b-v4^4ahtdyP@!WXO^*en~7E%~5QDb5TKw4^?S04pEqX?C^TH zskk4v(K&lx0I6r1zeUv~;|uhNu@Y^ULjGZPHB8r164_XDUPxWD=+v!u#&b-G{jv7J z=3L6prUR5)<@s<~ItEoBq}b(;7MQd!w{AgI!VdA)niLA34?MdNilNFucDE0H$-}Me z_m^Q^Z1U_PEhl|MQX<7wFalz)K=vv`;Dhaxu5%62bXmJi9Wn?wzYtLhJ$Ori$NsD% zN+dNaRk?q*DdXi@O#Y?8wHG%+PmeNtxg;Rc_ei@q6)w|F(Wd2?}Fke~W z4dw3#)!}3@c4)St--dMu8WPoBWWL+<4D$^#G*)%Q5*ze6P6`U4Po8tD^TvZ2VO6nv z2=A*GV49XATL*i3auS>Td@ki$@d~ggg!;k+6dI^`;oqHrJG<=o9*!Gt^<6aqx5=Ut z{Xms@gW$Ri`CuTCuh7Y@H#(zynl}u-r4n1P)r1yHBPn7G;^lYC5 zs<(vvfkvuTa+?L8kH1}Re2mG8?bXn`gE!<+c(_kaSQF=97@^!$NDd~`FI=5eGbYF=#}PzV*G?Gg>15qF<2OPu9*LXoJ%TP1|CLlec zl!J7b#H_x=Ng(Ya$vYeUr%k#oX-pzRR&&Gu zcFh9nKRIO2U#3y|9Vr*w_f}QPh>pA?n4z^t0Ya(QH|RGSm8nTr4%w$jt*{x^>E+D2!Du**7JL6=ijm$>CU33%MMZvaj2Q*VsSp2)?lScv=~0 z(DSBtIOm=Zw>zFN)%8__EOd>4(I#G=5tg_*3$4#I5lf}3{&G(vOT4CT)Q6L*64d0s z_3{+$^<3$XFxK7j6*2o>LulG+9z`5Q8PUr!E5VxU$vVQ<%HFo1?v?s|6zgLP1SwqC zOMYKYjEyHPU1@Yu;u_AI`xe9^NVGHR;rlF(CnW zJX{3VlQxIrR4j#6bP?oaC7{hbTv4hd)5!w`-Wb|DBvtN2vmp2o79>e?9`fY6vk1*z zR^4f}M9Su}Oe+gE7EP5+@2>y!%C)=Pj&#d5hz@n0k1xnuP9G;oPwL;mv*X^38vc|h ze?77Obg}a_y5cOo8ZVMp+9HJJ`W$}rKr0TF@+Y(z#F8Hf{H?oBYt*O@uNu>Q{ki-3 zk>JgbwFP>&mhSxz!rnSAj;-ky9^Bmqcega0pJ&1a~L6 z`|wT9JvryO_dfUi<{$XY^mO;$Q(d*zs@k6pntJ6Tf3>Dh+HAC7 z1I?qT$Jmdb*@~?VNXfVAPDT^^e@+u{m3g+ZY6P)K#^4GB_f#Uk!xwlvQ~7Gu2T8sg zDYdqfa33tuA;8Xss*ta+>2xlFn#m?Rq@&HCA~U8cfv1o!uxlMgQBrJf&bCtpq=b9R zo;wtueqesv^KfR^w!>Qre*lg2G<~0abM?p;|K%ep;`-JabKuu#b)Xl+dIMNsm!Tl4 zWRmnkVnk!z@}Tap(~K@97I~*7H3MNnD@G|w8V71k~%zSqv(iO=^SqtXF+fq(fjULk{m&xnyX1cI~tUQ`i z1}9tuh9%ONs|N|d+qlSI{l_!Xj&db&i}>7`>G#<4CgILMQ4&~4FUl68zGwv-2U@fS zQRtXz*)7~T%~{puVEnWyzgP#r2+>lJex~|dixC{kHgv^)Cr|yL_4~m&)`m(IU7r#E zoLcE^ISD!*Y{Hlcdd_3+zr=p^q<>PV9`4q?1ZEZ z7WyBsX> znQJtxcX6bC;u6oFHJvNkhXd%~LC-kkbu!7=4Fz?Ue6h@(lRhMjZZZY_y{hEXUsyY1 zrO|rC-WshN8vi&GiUzcb|J-ChF@<_8oovDNdUy&6e~_j!6XUBp!Zy~14~D9K1?F3i z0R}oEcLcSE_QEo2;A0X1NVxm)e!E4zlh$8I&iLvl&Wwu!Fi9>rWKSV|-O=(j+vH`e zCDNq*0f;N?;rAp6Z=h$(X(=o)J-0U$>N-Awp#fg8u2dPsgH6xs%Gtow60~H#F>C#s z;C0C7LMUMD2yn_%HlwRdqsU0N0R>>MI3K2wMUxxQ6F`U0`CLz4r*%nsO0+Kp;)#F% zP@2Qb!7NWq=aMnqc8t)|n{MVs@I;IC%y7@EwjXcfpBm6E3N;}oieRH%VFeWoP-HQG zv=A|?-t}I6H5p~(K(=(P#P8Im*?}dVQd_^QdgfTNKop`sXRV zNx;#z9)q!9(^!XCT|cy)@(Vyt=r0oiaF&yx9P#HO)EW^m=(hjIC{}MD*PlJuS1z*E zVO#@Y!PY+G-hN5{ev8OT)44I;?g5^(g|VNw-TG_>J4g>>{M37wp(LVUKF6gYAZyUx z=pzSbJPxZwN8<&&wNZB2RSX1SacDql5Ulr|bnKaogDJ+vChW+|;2wpwE_ZLQ?~yRB zVkC!xoN7kQv~WXi5Vw0%$EI4}2t^hu2h1IyLA-Fk+CKVyGC(h8{ejKjH5}Ap&m*oP z>P~DT3HJBC!WiSC?6Q!8V;9-C@_%~cW?c)#UTjOK5|jwaD+YCsQ2<0D$T{McAB~=Q z*%vOlCY4T?CU@(WCr97H#NfmSy*Bv?J;>bw&xyey#qx&k+v!!XLYyfm_NH1R=?gq6 z6em~ue#`T8d#*_oFaQl|?uztukb}#t^gJJN^#?KIeYre8YFJ0lGX(-=Ky<%Wr6?yM zI))x1-P_CKJkhx_gp9&43K-E3&q&^DkPAqme>;qgX0(x$dpqn!{gudzWR@_B(5Txb zz%M>53r`Qtbjq(h-2S(rseM|ktN03Q#vcIB17K9={QQjeAESkT$f?IOp`??J#o#0r zjeZxDQr;r@O!it#V3(j<@^irw0!nG5j0vb!&u-L=pwmbY7Qm0z=wEQ^XOA*VF<5Jp zbApovR4AgPGa)Uss-9Z=*TMdWl?ypiWDrjjKC2q!O)0FM;A`teXV)~dq=$cd0-3ei zQra0I`O9EHhRBpI>i%5_otd>?5oAU<#H${~<1w>QN(KGVtDEd`35#P ztvEJfEbSNug9mF#8e1B;r2Ce}WS7&iIn2}w( zWv1MpQ}o7Ge=t+=iH_z{TUdp8{yeMi$?HB`{-km}6syxK<9t8kNVLbgLTbK`Hze^k zKR^zcm3;q%1b=zGy3dffIBgFfx-+@8#rDQKKt)Ux91VGPP&L^CPJ@$P+h~gS;pM

=AG|T6=)omaq-)7TK$}V!zpPRoUU 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 literal 137389 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�D%{N^U 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 jaxopt.OptStep: - return solver.run(init_params, X=X, y=y, **run_kwargs) + kwargs = {input_name: X, output_name: y} + return solver.run(init_params, **kwargs, **run_kwargs) return solver_run diff --git a/tests/test_glm.py b/tests/test_glm.py index 567e7e06..aaaf8ed8 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -37,8 +37,8 @@ class TestGLM: "solver, error, match_str", [ (nsl.solver.RidgeSolver("BFGS"), None, None), - (nsl.solver.Solver, TypeError, "The provided `solver` should be one of the implemented"), - (1, TypeError, "The provided `solver` should be one of the implemented") + (nsl.solver.Solver, TypeError, "The `instantiate_solver` method of `solver` must accept"), + (1, TypeError, "The provided `solver` does not implements the") ] ) def test_init_solver_type(self, solver, error, match_str, poisson_observation_model): diff --git a/tests/test_solver.py b/tests/test_solver.py index fcbfa8c9..47a36fe0 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -70,24 +70,12 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [jnp.exp, 1, None, {}]) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) def test_loss_is_callable(self, loss): """Test that the loss function is a callable""" raise_exception = not callable(loss) if raise_exception: - with pytest.raises(TypeError, match="The loss function must a Callable"): - self.cls("GradientDescent").instantiate_solver(loss) - else: - self.cls("GradientDescent").instantiate_solver(loss) - - @pytest.mark.parametrize("loss", [jnp.exp, np.exp, nsl.glm.GLM()._score]) - def test_loss_type_jax_or_glm(self, loss): - """Test that the loss function is a callable""" - raise_exception = (not hasattr(loss, "__module__")) or \ - (not (loss.__module__.startswith("jax.") or - loss.__module__.startswith("neurostatslib.glm"))) - if raise_exception: - with pytest.raises(ValueError, match=f"The function {loss.__name__} is not from the jax namespace."): + with pytest.raises(TypeError, match="The loss function must be a Callable"): self.cls("GradientDescent").instantiate_solver(loss) else: self.cls("GradientDescent").instantiate_solver(loss) @@ -192,24 +180,30 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [jnp.exp, 1, None, {}]) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) def test_loss_is_callable(self, loss): """Test that the loss function is a callable""" raise_exception = not callable(loss) if raise_exception: - with pytest.raises(TypeError, match="The loss function must a Callable"): + with pytest.raises(TypeError, match="The loss function must be a Callable"): self.cls("GradientDescent").instantiate_solver(loss) else: self.cls("GradientDescent").instantiate_solver(loss) - @pytest.mark.parametrize("loss", [jnp.exp, np.exp, nsl.glm.GLM()._score]) - def test_loss_type_jax_or_glm(self, loss): + @pytest.mark.parametrize("loss, raise_exception", [ + (lambda a, b, c: 0, False), + (lambda a, b: 0, True), + (lambda a, b, c, d: 0, True), + (lambda a, b, c, *d: 0, False), + (lambda a, b, c, **d: 0, False), + (lambda a, b, c, *d, **e: 0, False) + ] + ) + def test_loss_has_three_parameter_callable(self, loss, raise_exception): """Test that the loss function is a callable""" - raise_exception = (not hasattr(loss, "__module__")) or \ - (not (loss.__module__.startswith("jax.") or - loss.__module__.startswith("neurostatslib.glm"))) + pass if raise_exception: - with pytest.raises(ValueError, match=f"The function {loss.__name__} is not from the jax namespace."): + with pytest.raises(TypeError, match="The loss function must require 3 inputs"): self.cls("GradientDescent").instantiate_solver(loss) else: self.cls("GradientDescent").instantiate_solver(loss) @@ -300,12 +294,12 @@ def test_init_solver_kwargs(self, solver_kwargs): else: self.cls("ProximalGradient", solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [jnp.exp, jax.nn.relu, 1, None, {}]) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) def test_loss_callable(self, loss): """Test that the loss function is a callable""" raise_exception = not callable(loss) if raise_exception: - with pytest.raises(TypeError, match="The loss function must a Callable"): + with pytest.raises(TypeError, match="The loss function must be a Callable"): self.cls("ProximalGradient").instantiate_solver(loss) else: self.cls("ProximalGradient").instantiate_solver(loss) @@ -407,7 +401,7 @@ def test_init_solver_kwargs(self, solver_kwargs): else: self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [jnp.exp, jax.nn.relu, 1, None, {}]) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) def test_loss_callable(self, loss): """Test that the loss function is a callable""" raise_exception = not callable(loss) @@ -419,7 +413,7 @@ def test_loss_callable(self, loss): mask = jnp.asarray(mask) if raise_exception: - with pytest.raises(TypeError, match="The loss function must a Callable"): + with pytest.raises(TypeError, match="The loss function must be a Callable"): self.cls("ProximalGradient", mask).instantiate_solver(loss) else: self.cls("ProximalGradient", mask).instantiate_solver(loss) From 2fdd66fd2b90b7916118410eb7a59fa48d4501de Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 2 Nov 2023 18:21:32 -0400 Subject: [PATCH 150/250] linted --- src/neurostatslib/glm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index ec2bafa2..5fa59178 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,6 +1,5 @@ """GLM core module.""" import inspect - from typing import Literal, Optional, Tuple, Union import jax From 5d4379093bd8798f270cd905894ed01a8de2f69d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 2 Nov 2023 18:23:31 -0400 Subject: [PATCH 151/250] grammar fixed --- src/neurostatslib/glm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 5fa59178..bcd236c7 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -60,8 +60,8 @@ def __init__( ) # this catches the corner case of users passing classes before instantiation. Example, - # `sovler = nsl.solver.RidgeSolver` instead of `sovler = nsl.solver.RidgeSolver()`. - # It also catches solvers that do not respect the api of having a single loss function as input. + # `solver = nsl.solver.RidgeSolver` instead of `solver = nsl.solver.RidgeSolver()`. + # It also catches solvers that requires multiple arguments, the loss function should be the only input. if len(inspect.signature(solver.instantiate_solver).parameters) != 1: raise TypeError( "The `instantiate_solver` method of `solver` must accept a single parameter, the loss function" From 7c1218312739471ed66d3d2caf951ac67e383295 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 2 Nov 2023 19:00:09 -0400 Subject: [PATCH 152/250] changed description of check func --- src/neurostatslib/solver.py | 41 ++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 4adc2740..123c33fe 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -129,13 +129,13 @@ def _check_solver_kwargs(solver_name, solver_kwargs): ) @staticmethod - def _check_is_callable_from_jax(func: Callable): + def _check_loss(func: Callable): """ - Check if the provided function is callable and from the jax namespace. + Check if the provided loss function is callable with 3 arguments. - Ensures that the given function is not only callable, but also belongs to - the `jax` namespace, ensuring compatibility and safety when using jax-based - operations. + Ensures that the given function is not only callable, but also that it + requires 3 arguments, excluding uniquely keyword arguments, and uniquely positional + arguments. Parameters ---------- @@ -145,9 +145,8 @@ def _check_is_callable_from_jax(func: Callable): Raises ------ TypeError - If the provided function is not callable. - ValueError - If the function does not belong to the `jax` or `neurostatslib.glm` namespaces. + - If the provided loss is not callable. + - If the loss does not require 3 arguments. """ if not callable(func): raise TypeError("The loss function must be a Callable!") @@ -162,8 +161,12 @@ def _check_is_callable_from_jax(func: Callable): ) if count_params != 3: raise TypeError( - "The loss function must require 3 inputs, usually (params, X, y)." - "Valid loss definitions will look like: `def loss(params, X, y, *args, **kwargs):`" + "The loss function must require 3 inputs, usually (params, X, y).\n" + "Valid loss definitions are of the type:\n" + "1. `def loss(var_1, var_2, var_3):`\n" + "2. `def loss(var_1, var_2, var_3, *args):`\n" + "3. `def loss(var_1, var_2, var_3, **kwargs):`\n" + "4. `def loss(var_1, var_2, var_3, *args, **kwargs):`" ) @abc.abstractmethod @@ -200,11 +203,13 @@ def get_runner( : The solver runner. """ - solver = getattr(jaxopt, self.solver_name)(**solver_kwargs) - + # get the arguments input_name, output_name = list( inspect.signature(solver_kwargs["fun"]).parameters - )[1:] + )[1:3] + + solver = getattr(jaxopt, self.solver_name)(**solver_kwargs) + def solver_run( init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray @@ -275,9 +280,11 @@ def instantiate_solver( A runner function that uses the specified optimization algorithm to minimize the given loss function. """ - self._check_is_callable_from_jax(loss) + # check the loss (including the number of arguments) + self._check_loss(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss + return self.get_runner(solver_kwargs, {}) @@ -355,7 +362,8 @@ def instantiate_solver( Callable A function that runs the solver with the penalized loss. """ - self._check_is_callable_from_jax(loss) + # check the loss (including the number of arguments) + self._check_loss(loss) def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) @@ -427,7 +435,8 @@ def instantiate_solver( : A function that runs the solver with the provided loss and proximal operator. """ - self._check_is_callable_from_jax(loss) + # check the loss (including the number of arguments) + self._check_loss(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss From 27582ccb62f271d5a0cbb35c7d7302e3ae437862 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 12:42:46 -0400 Subject: [PATCH 153/250] generalized get_runner to accept any inputs --- src/neurostatslib/glm.py | 14 ----- src/neurostatslib/solver.py | 71 +++------------------ src/neurostatslib/utils.py | 66 ++++++++++++++++++- tests/test_glm.py | 16 +++-- tests/test_solver.py | 122 ++++++++++++++++++------------------ 5 files changed, 146 insertions(+), 143 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index bcd236c7..60436c0f 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -54,20 +54,6 @@ def __init__( ): super().__init__() - if not hasattr(solver, "instantiate_solver"): - raise TypeError( - "The provided `solver` does not implements the `instantiate_solver` method." - ) - - # this catches the corner case of users passing classes before instantiation. Example, - # `solver = nsl.solver.RidgeSolver` instead of `solver = nsl.solver.RidgeSolver()`. - # It also catches solvers that requires multiple arguments, the loss function should be the only input. - if len(inspect.signature(solver.instantiate_solver).parameters) != 1: - raise TypeError( - "The `instantiate_solver` method of `solver` must accept a single parameter, the loss function" - "Have you instantiate the class?" - ) - if observation_model.__class__.__name__ not in nsm.__all__: raise TypeError( "The provided `observation_model` should be one of the implemented models in " diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 123c33fe..569d32bc 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -7,7 +7,7 @@ """ import abc import inspect -from typing import Callable, List, Optional, Tuple, Union +from typing import Any, Callable, List, Optional, Tuple, Union import jax.numpy as jnp import jaxopt @@ -15,6 +15,7 @@ from .base_class import Base from .proximal_operator import prox_group_lasso +from.utils import check_loss __all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] @@ -128,46 +129,6 @@ def _check_solver_kwargs(solver_name, solver_kwargs): f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" ) - @staticmethod - def _check_loss(func: Callable): - """ - Check if the provided loss function is callable with 3 arguments. - - Ensures that the given function is not only callable, but also that it - requires 3 arguments, excluding uniquely keyword arguments, and uniquely positional - arguments. - - Parameters - ---------- - func : - The function to check. - - Raises - ------ - TypeError - - If the provided loss is not callable. - - If the loss does not require 3 arguments. - """ - if not callable(func): - raise TypeError("The loss function must be a Callable!") - - # count parameters of type POSITIONAL_OR_KEYWORD. - # In other words, exclude positional and keyword (*args, **kwargs) - count_params = sum( - 1 - for param in inspect.signature(func).parameters.values() - if param.kind - not in [inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD] - ) - if count_params != 3: - raise TypeError( - "The loss function must require 3 inputs, usually (params, X, y).\n" - "Valid loss definitions are of the type:\n" - "1. `def loss(var_1, var_2, var_3):`\n" - "2. `def loss(var_1, var_2, var_3, *args):`\n" - "3. `def loss(var_1, var_2, var_3, **kwargs):`\n" - "4. `def loss(var_1, var_2, var_3, *args, **kwargs):`" - ) @abc.abstractmethod def instantiate_solver( @@ -184,7 +145,8 @@ def instantiate_solver( def get_runner( self, solver_kwargs: dict, - run_kwargs: dict, + *run_args: Any, + **run_kwargs: dict, ) -> Callable[ [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep ]: @@ -204,18 +166,13 @@ def get_runner( The solver runner. """ # get the arguments - input_name, output_name = list( - inspect.signature(solver_kwargs["fun"]).parameters - )[1:3] - solver = getattr(jaxopt, self.solver_name)(**solver_kwargs) - def solver_run( - init_params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray + init_params: Tuple[jnp.ndarray, jnp.ndarray], *args: jnp.ndarray ) -> jaxopt.OptStep: - kwargs = {input_name: X, output_name: y} - return solver.run(init_params, **kwargs, **run_kwargs) + args = (*run_args, *args) + return solver.run(init_params, *args, **run_kwargs) return solver_run @@ -281,11 +238,10 @@ def instantiate_solver( to minimize the given loss function. """ # check the loss (including the number of arguments) - self._check_loss(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss - return self.get_runner(solver_kwargs, {}) + return self.get_runner(solver_kwargs) class RidgeSolver(Solver): @@ -362,15 +318,12 @@ def instantiate_solver( Callable A function that runs the solver with the penalized loss. """ - # check the loss (including the number of arguments) - self._check_loss(loss) - def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = penalized_loss - return self.get_runner(solver_kwargs, {}) + return self.get_runner(solver_kwargs) class ProxGradientSolver(Solver, abc.ABC): @@ -435,16 +388,12 @@ def instantiate_solver( : A function that runs the solver with the provided loss and proximal operator. """ - # check the loss (including the number of arguments) - self._check_loss(loss) solver_kwargs = self.solver_kwargs.copy() solver_kwargs["fun"] = loss solver_kwargs["prox"] = self.get_prox_operator() - run_kwargs = dict(hyperparams_prox=self.regularizer_strength) - - return self.get_runner(solver_kwargs, run_kwargs) + return self.get_runner(solver_kwargs, self.regularizer_strength) class LassoSolver(ProxGradientSolver): diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index d42ad563..4d9cb9ba 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -2,8 +2,9 @@ # required to get ArrayLike to render correctly, unnecessary as of python 3.10 from __future__ import annotations +import inspect from functools import partial -from typing import Iterable, List, Literal, Optional, Tuple, Union +from typing import Callable, Iterable, List, Literal, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -455,3 +456,66 @@ def check_invalid_entry(array: jnp.ndarray) -> None: raise ValueError("Input array contains Infs!") elif jnp.any(jnp.isnan(array)): raise ValueError("Input array contains NaNs!") + + +def count_positional_params(func: Callable) -> int: + """ + Count the number of positional parameters a function accepts. + + This function counts the number of POSITIONAL_OR_KEYWORD and POSITIONAL_ONLY parameters. + + For example, all the following callable will return a count of two: + + - `def func(x, y, *args)`, since x and y are POSITIONAL_OR_KEYWORD, *args is VAR_POSITIONAL + - `def func(x, y, *args, z)`, since x and y are POSITIONAL_OR_KEYWORD, + *args is VAR_POSITIONAL, z is KEYWORD_ONLY + - `def func(x, y, *args, z, **kwargs)`, since x and y are POSITIONAL_OR_KEYWORD, + *args is VAR_POSITIONAL, z is KEYWORD_ONLY, + **kwargs is VAR_KEYWORD + - `def func(x, /, y, *args, z, **kwargs)`, since x POSITIONAL_ONLY, y are POSITIONAL_OR_KEYWORD, + *args is VAR_POSITIONAL, z is KEYWORD_ONLY, **kwargs is VAR_KEYWORD + + Parameters + ---------- + func : + The function whose signature is to be inspected. + + Returns + ------- + : + The count of POSITIONAL_OR_KEYWORD parameters for the given function. + """ + # Count parameters excluding any *args or **kwargs + return sum( + 1 + for param in inspect.signature(func).parameters.values() + if param.kind + in [inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY] + ) + + +def check_loss(func: Callable): + """ + Check if the provided loss function is callable with 3 arguments. + + Ensures that the given function is not only callable, but also that it + requires 3 positinal arguments. + + Parameters + ---------- + func : + The function to check. + + Raises + ------ + TypeError + - If the provided loss is not callable. + - If the loss does not require 3 arguments. + """ + if not callable(func): + raise TypeError("The loss function must be a Callable!") + + if count_positional_params(func) != 3: + raise TypeError( + "The loss function must require 3 positional inputs, usually (params, X, y)." + ) diff --git a/tests/test_glm.py b/tests/test_glm.py index aaaf8ed8..4ba3f38f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -37,16 +37,20 @@ class TestGLM: "solver, error, match_str", [ (nsl.solver.RidgeSolver("BFGS"), None, None), - (nsl.solver.Solver, TypeError, "The `instantiate_solver` method of `solver` must accept"), - (1, TypeError, "The provided `solver` does not implements the") + (None, AttributeError, "'NoneType' object has no attribute 'instantiate_solver'"), + (nsl.solver.RidgeSolver, TypeError, "RidgeSolver.instantiate_solver() missing 1 required") ] ) - def test_init_solver_type(self, solver, error, match_str, poisson_observation_model): + def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiation): """ - Test initialization with different solver names. Check if an appropriate exception is raised - when the solver name is not present in jaxopt. + Test that an error is raised if a non-compatible solver is passed. """ - _test_class_initialization(self.cls, {'solver': solver, 'observation_model': poisson_observation_model}, error, match_str) + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + model.solver = solver + _test_class_method(model, "fit", + [X, y], + {"init_params": true_params}, + error, match_str) @pytest.mark.parametrize( "observation, error, match_str", diff --git a/tests/test_solver.py b/tests/test_solver.py index 47a36fe0..ffb1bc75 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -70,15 +70,15 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - def test_loss_is_callable(self, loss): - """Test that the loss function is a callable""" - raise_exception = not callable(loss) - if raise_exception: - with pytest.raises(TypeError, match="The loss function must be a Callable"): - self.cls("GradientDescent").instantiate_solver(loss) - else: - self.cls("GradientDescent").instantiate_solver(loss) + # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + # def test_loss_is_callable(self, loss): + # """Test that the loss function is a callable""" + # raise_exception = not callable(loss) + # if raise_exception: + # with pytest.raises(TypeError, match="The loss function must be a Callable"): + # self.cls("GradientDescent").instantiate_solver(loss) + # else: + # self.cls("GradientDescent").instantiate_solver(loss) @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) def test_run_solver(self, solver_name, poissonGLM_model_instantiation): @@ -180,33 +180,33 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - def test_loss_is_callable(self, loss): - """Test that the loss function is a callable""" - raise_exception = not callable(loss) - if raise_exception: - with pytest.raises(TypeError, match="The loss function must be a Callable"): - self.cls("GradientDescent").instantiate_solver(loss) - else: - self.cls("GradientDescent").instantiate_solver(loss) - - @pytest.mark.parametrize("loss, raise_exception", [ - (lambda a, b, c: 0, False), - (lambda a, b: 0, True), - (lambda a, b, c, d: 0, True), - (lambda a, b, c, *d: 0, False), - (lambda a, b, c, **d: 0, False), - (lambda a, b, c, *d, **e: 0, False) - ] - ) - def test_loss_has_three_parameter_callable(self, loss, raise_exception): - """Test that the loss function is a callable""" - pass - if raise_exception: - with pytest.raises(TypeError, match="The loss function must require 3 inputs"): - self.cls("GradientDescent").instantiate_solver(loss) - else: - self.cls("GradientDescent").instantiate_solver(loss) + # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + # def test_loss_is_callable(self, loss): + # """Test that the loss function is a callable""" + # raise_exception = not callable(loss) + # if raise_exception: + # with pytest.raises(TypeError, match="The loss function must be a Callable"): + # self.cls("GradientDescent").instantiate_solver(loss) + # else: + # self.cls("GradientDescent").instantiate_solver(loss) + + # @pytest.mark.parametrize("loss, raise_exception", [ + # (lambda a, b, c: 0, False), + # (lambda a, b: 0, True), + # (lambda a, b, c, d: 0, True), + # (lambda a, b, c, *d: 0, False), + # (lambda a, b, c, **d: 0, False), + # (lambda a, /, b, c, *d, **e: 0, False) + # ] + # ) + # def test_loss_has_three_parameter_callable(self, loss, raise_exception): + # """Test that the loss function is a callable""" + # pass + # if raise_exception: + # with pytest.raises(TypeError, match="The loss function must require 3 positional"): + # self.cls("GradientDescent").instantiate_solver(loss) + # else: + # self.cls("GradientDescent").instantiate_solver(loss) @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) def test_run_solver(self, solver_name, poissonGLM_model_instantiation): @@ -294,15 +294,15 @@ def test_init_solver_kwargs(self, solver_kwargs): else: self.cls("ProximalGradient", solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - def test_loss_callable(self, loss): - """Test that the loss function is a callable""" - raise_exception = not callable(loss) - if raise_exception: - with pytest.raises(TypeError, match="The loss function must be a Callable"): - self.cls("ProximalGradient").instantiate_solver(loss) - else: - self.cls("ProximalGradient").instantiate_solver(loss) + # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + # def test_loss_callable(self, loss): + # """Test that the loss function is a callable""" + # raise_exception = not callable(loss) + # if raise_exception: + # with pytest.raises(TypeError, match="The loss function must be a Callable"): + # self.cls("ProximalGradient").instantiate_solver(loss) + # else: + # self.cls("ProximalGradient").instantiate_solver(loss) def test_run_solver(self, poissonGLM_model_instantiation): """Test that the solver runs.""" @@ -401,22 +401,22 @@ def test_init_solver_kwargs(self, solver_kwargs): else: self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) - @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - def test_loss_callable(self, loss): - """Test that the loss function is a callable""" - raise_exception = not callable(loss) - - # create a valid mask - mask = np.zeros((2, 10)) - mask[0, :5] = 1 - mask[1, 5:] = 1 - mask = jnp.asarray(mask) - - if raise_exception: - with pytest.raises(TypeError, match="The loss function must be a Callable"): - self.cls("ProximalGradient", mask).instantiate_solver(loss) - else: - self.cls("ProximalGradient", mask).instantiate_solver(loss) + # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + # def test_loss_callable(self, loss): + # """Test that the loss function is a callable""" + # raise_exception = not callable(loss) + # + # # create a valid mask + # mask = np.zeros((2, 10)) + # mask[0, :5] = 1 + # mask[1, 5:] = 1 + # mask = jnp.asarray(mask) + # + # if raise_exception: + # with pytest.raises(TypeError, match="The loss function must be a Callable"): + # self.cls("ProximalGradient", mask).instantiate_solver(loss) + # else: + # self.cls("ProximalGradient", mask).instantiate_solver(loss) def test_run_solver(self, poissonGLM_model_instantiation): """Test that the solver runs.""" From bdefe97fb8a16222720f937997293d3369692459 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 12:58:31 -0400 Subject: [PATCH 154/250] simplified get_runner --- src/neurostatslib/solver.py | 31 +++++++++++-------------------- tests/test_glm.py | 2 +- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 569d32bc..5736cb6a 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -144,7 +144,7 @@ def instantiate_solver( def get_runner( self, - solver_kwargs: dict, + loss: Callable, *run_args: Any, **run_kwargs: dict, ) -> Callable[ @@ -155,8 +155,8 @@ def get_runner( Parameters ---------- - solver_kwargs : - Additional keyword arguments for the solver instantiation. + loss : + The loss funciton. run_kwargs : Additional keyword arguments for the solver run. @@ -165,8 +165,10 @@ def get_runner( : The solver runner. """ - # get the arguments - solver = getattr(jaxopt, self.solver_name)(**solver_kwargs) + # get the solver with given arguments. + # The "fun" argument is not always the first one, but it is always KEYWORD + # see jaxopt.EqualityConstrainedQP for example. The most general way is to pass it as keyword. + solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) def solver_run( init_params: Tuple[jnp.ndarray, jnp.ndarray], *args: jnp.ndarray @@ -237,11 +239,7 @@ def instantiate_solver( A runner function that uses the specified optimization algorithm to minimize the given loss function. """ - # check the loss (including the number of arguments) - solver_kwargs = self.solver_kwargs.copy() - solver_kwargs["fun"] = loss - - return self.get_runner(solver_kwargs) + return self.get_runner(loss) class RidgeSolver(Solver): @@ -320,10 +318,7 @@ def instantiate_solver( """ def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) - - solver_kwargs = self.solver_kwargs.copy() - solver_kwargs["fun"] = penalized_loss - return self.get_runner(solver_kwargs) + return self.get_runner(penalized_loss) class ProxGradientSolver(Solver, abc.ABC): @@ -388,12 +383,8 @@ def instantiate_solver( : A function that runs the solver with the provided loss and proximal operator. """ - - solver_kwargs = self.solver_kwargs.copy() - solver_kwargs["fun"] = loss - solver_kwargs["prox"] = self.get_prox_operator() - - return self.get_runner(solver_kwargs, self.regularizer_strength) + self.solver_kwargs["prox"] = self.get_prox_operator() + return self.get_runner(loss, self.regularizer_strength) class LassoSolver(ProxGradientSolver): diff --git a/tests/test_glm.py b/tests/test_glm.py index 4ba3f38f..367c3aa3 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -38,7 +38,7 @@ class TestGLM: [ (nsl.solver.RidgeSolver("BFGS"), None, None), (None, AttributeError, "'NoneType' object has no attribute 'instantiate_solver'"), - (nsl.solver.RidgeSolver, TypeError, "RidgeSolver.instantiate_solver() missing 1 required") + (nsl.solver.RidgeSolver, TypeError, r"RidgeSolver.instantiate_solver\(\) missing 1") ] ) def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiation): From 86c4df85bc9ff6a23accdacc8d7ac091f97de2ca Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 13:01:28 -0400 Subject: [PATCH 155/250] simplified get_runner --- src/neurostatslib/solver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 5736cb6a..3f9a6cc6 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -343,6 +343,10 @@ def __init__( regularizer_strength: float = 1.0, **kwargs, ): + if solver_kwargs is None: + solver_kwargs = dict(prox=self.get_prox_operator()) + else: + solver_kwargs["prox"] = self.get_prox_operator() super().__init__(solver_name, solver_kwargs=solver_kwargs) self.regularizer_strength = regularizer_strength @@ -383,7 +387,6 @@ def instantiate_solver( : A function that runs the solver with the provided loss and proximal operator. """ - self.solver_kwargs["prox"] = self.get_prox_operator() return self.get_runner(loss, self.regularizer_strength) From 8af2a210117f36633198782319f0eef1d64432ab Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 14:33:14 -0400 Subject: [PATCH 156/250] solver set --- src/neurostatslib/glm.py | 46 +++++++++++++++++++++++++++++-------- src/neurostatslib/solver.py | 3 +-- tests/test_glm.py | 12 ++++------ 3 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 60436c0f..36aa1d37 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -6,7 +6,7 @@ import jax.numpy as jnp from numpy.typing import NDArray -from . import observation_models as nsm +from . import observation_models as obs from . import solver as slv from .base_class import BaseRegressor from .exceptions import NotFittedError @@ -49,18 +49,11 @@ class GLM(BaseRegressor): def __init__( self, - observation_model: nsm.Observations = nsm.PoissonObservations(), + observation_model: obs.Observations = obs.PoissonObservations(), solver: slv.Solver = slv.RidgeSolver("GradientDescent"), ): super().__init__() - if observation_model.__class__.__name__ not in nsm.__all__: - raise TypeError( - "The provided `observation_model` should be one of the implemented models in " - "`neurostatslib.observation_models`. " - f"Available options are: {nsm.__all__}." - ) - self.observation_model = observation_model self.solver = solver @@ -69,6 +62,39 @@ def __init__( self.basis_coeff_ = None self.solver_state = None + @property + def solver(self): + return self._solver + + @solver.setter + def solver(self, solver: slv.Solver): + if not hasattr(solver, "instantiate_solver"): + raise AttributeError( + "The provided `solver` doesn't implement the `instantiate_sovler` method." + ) + # test solver instantiation on the GLM loss + try: + solver.instantiate_solver(self._score) + except Exception: + raise ValueError(f"The provided `solver` cannot be instantiated on " + f"the GLM log-likelihood.") + + self._solver = solver + + @property + def observation_model(self): + return self._observation_model + + @observation_model.setter + def observation_model(self, observation: obs.Observations): + if observation.__class__.__name__ not in obs.__all__: + raise TypeError( + "The provided `observation_model` should be one of the implemented models in " + "`neurostatslib.observation_models`. " + f"Available options are: {obs.__all__}." + ) + self._observation_model = observation + def _check_is_fit(self): """Ensure the instance has been fitted.""" if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): @@ -425,7 +451,7 @@ class GLMRecurrent(GLM): def __init__( self, - observation_model: nsm.Observations = nsm.PoissonObservations(), + observation_model: obs.Observations = obs.PoissonObservations(), solver: slv.Solver = slv.RidgeSolver(), ): super().__init__(observation_model=observation_model, solver=solver) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 3f9a6cc6..2e50c542 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -15,7 +15,6 @@ from .base_class import Base from .proximal_operator import prox_group_lasso -from.utils import check_loss __all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] @@ -82,7 +81,7 @@ def solver_kwargs(self): @solver_kwargs.setter def solver_kwargs(self, solver_kwargs: dict): self._check_solver_kwargs(self.solver_name, solver_kwargs) - return self._solver_kwargs + self._solver_kwargs = solver_kwargs def _check_solver(self, solver_name: str): """ diff --git a/tests/test_glm.py b/tests/test_glm.py index 367c3aa3..2f46990f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -37,20 +37,16 @@ class TestGLM: "solver, error, match_str", [ (nsl.solver.RidgeSolver("BFGS"), None, None), - (None, AttributeError, "'NoneType' object has no attribute 'instantiate_solver'"), - (nsl.solver.RidgeSolver, TypeError, r"RidgeSolver.instantiate_solver\(\) missing 1") + (None, AttributeError, "The provided `solver` doesn't implement "), + (nsl.solver.RidgeSolver, TypeError, "The provided `solver` cannot be instantiated") ] ) def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiation): """ Test that an error is raised if a non-compatible solver is passed. """ - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - model.solver = solver - _test_class_method(model, "fit", - [X, y], - {"init_params": true_params}, - error, match_str) + _test_class_initialization(self.cls, {'solver': solver}, error, match_str) + @pytest.mark.parametrize( "observation, error, match_str", From b27cd31665553919f3845366b27870fdc891099a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 14:38:24 -0400 Subject: [PATCH 157/250] modified glm moving the checks to the setter --- src/neurostatslib/glm.py | 7 ++++--- src/neurostatslib/solver.py | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 36aa1d37..aed181ff 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -76,9 +76,10 @@ def solver(self, solver: slv.Solver): try: solver.instantiate_solver(self._score) except Exception: - raise ValueError(f"The provided `solver` cannot be instantiated on " - f"the GLM log-likelihood.") - + raise TypeError( + f"The provided `solver` cannot be instantiated on " + f"the GLM log-likelihood." + ) self._solver = solver @property diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 2e50c542..948cb87e 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -128,7 +128,6 @@ def _check_solver_kwargs(solver_name, solver_kwargs): f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" ) - @abc.abstractmethod def instantiate_solver( self, @@ -315,8 +314,10 @@ def instantiate_solver( Callable A function that runs the solver with the penalized loss. """ + def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) + return self.get_runner(penalized_loss) From 3baaefb3703515c172f43162f58e528c9c3f7928 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 16:36:23 -0400 Subject: [PATCH 158/250] added logistic as example --- src/neurostatslib/glm.py | 33 +++++---- src/neurostatslib/observation_models.py | 89 ++++++++++++++++++++++++- src/neurostatslib/utils.py | 67 +++++++++++++------ 3 files changed, 150 insertions(+), 39 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index aed181ff..468b8aa1 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -8,9 +8,9 @@ from . import observation_models as obs from . import solver as slv +from . import utils from .base_class import BaseRegressor from .exceptions import NotFittedError -from .utils import convert_to_jnp_ndarray, convolve_1d_trials class GLM(BaseRegressor): @@ -88,12 +88,9 @@ def observation_model(self): @observation_model.setter def observation_model(self, observation: obs.Observations): - if observation.__class__.__name__ not in obs.__all__: - raise TypeError( - "The provided `observation_model` should be one of the implemented models in " - "`neurostatslib.observation_models`. " - f"Available options are: {obs.__all__}." - ) + # check that the model has the required attributes + # and that the attribute can be called + obs.check_observation_model(observation) self._observation_model = observation def _check_is_fit(self): @@ -122,7 +119,7 @@ def _predict( The predicted rates. Shape (n_time_bins, n_neurons). """ Ws, bs = params - return self.observation_model.inverse_link_function( + return self._observation_model.inverse_link_function( jnp.einsum("ik,tik->ti", Ws, X) + bs[None, :] ) @@ -166,7 +163,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - (X,) = convert_to_jnp_ndarray(X) + (X,) = utils.convert_to_jnp_ndarray(X) # check input dimensionality self._check_input_dimensionality(X=X) @@ -203,7 +200,7 @@ def _score( """ predicted_rate = self._predict(params, X) - return self.observation_model.negative_log_likelihood(predicted_rate, y) + return self._observation_model.negative_log_likelihood(predicted_rate, y) def score( self, @@ -286,7 +283,7 @@ def score( Ws = self.basis_coeff_ bs = self.baseline_link_fr_ - X, y = convert_to_jnp_ndarray(X, y) + X, y = utils.convert_to_jnp_ndarray(X, y) self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) @@ -295,7 +292,7 @@ def score( norm_constant = jax.scipy.special.gammaln(y + 1).mean() score = -self._score((Ws, bs), X, y) - norm_constant else: - score = self.observation_model.pseudo_r2(self._predict((Ws, bs), X), y) + score = self._observation_model.pseudo_r2(self._predict((Ws, bs), X), y) return score @@ -405,7 +402,7 @@ def simulate( ) predicted_rate = self._predict((Ws, bs), feedforward_input) return ( - self.observation_model.sample_generator( + self._observation_model.sample_generator( key=random_key, predicted_rate=predicted_rate ), predicted_rate, @@ -524,7 +521,7 @@ def simulate_recurrent( self._check_is_fit() # convert to jnp.ndarray - (coupling_basis_matrix,) = convert_to_jnp_ndarray(coupling_basis_matrix) + (coupling_basis_matrix,) = utils.convert_to_jnp_ndarray(coupling_basis_matrix) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] @@ -571,7 +568,9 @@ def scan_fn( activity, chunk = data # Convolve the neural activity with the coupling basis matrix - conv_act = convolve_1d_trials(coupling_basis_matrix, activity[None])[0] + conv_act = utils.convolve_1d_trials(coupling_basis_matrix, activity[None])[ + 0 + ] # Extract the slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( @@ -588,12 +587,12 @@ def scan_fn( # Predict the firing rate using the model coefficients # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input - firing_rate = self.observation_model.inverse_link_function( + firing_rate = self._observation_model.inverse_link_function( jnp.einsum("ik,tik->ti", Wr, conv_act) + input_slice + bs[None, :] ) # Simulate activity based on the predicted firing rate - new_act = self.observation_model.sample_generator(key, firing_rate) + new_act = self._observation_model.sample_generator(key, firing_rate) # Prepare the spikes for the next iteration (keeping the most recent spikes) concat_act = jnp.row_stack((activity[1:], new_act)), chunk + 1 diff --git a/src/neurostatslib/observation_models.py b/src/neurostatslib/observation_models.py index 99906336..61eb84b2 100644 --- a/src/neurostatslib/observation_models.py +++ b/src/neurostatslib/observation_models.py @@ -6,6 +6,7 @@ import jax import jax.numpy as jnp +from . import utils from .base_class import Base KeyArray = Union[jnp.ndarray, jax.random.PRNGKeyArray] @@ -54,7 +55,7 @@ def inverse_link_function(self): @inverse_link_function.setter def inverse_link_function(self, inverse_link_function: Callable): """Setter for the inverse link function for the model.""" - self._check_inverse_link_function(inverse_link_function) + self.check_inverse_link_function(inverse_link_function) self._inverse_link_function = inverse_link_function @property @@ -70,7 +71,7 @@ def scale(self, value: Union[int, float]): self._scale = value @staticmethod - def _check_inverse_link_function(inverse_link_function: Callable): + def check_inverse_link_function(inverse_link_function: Callable): """ Check if the provided inverse_link_function is usable. @@ -415,3 +416,87 @@ def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: but is retained for compatibility with the abstract method signature. """ self.scale = 1.0 + + +def check_observation_model(observation_model): + """ + Check the attributes of an observation model for compliance. + + This function ensures that the observation model has the required attributes and that each + attribute is a callable function. Additionally, it checks if these functions return + jax.numpy.ndarray objects, and in the case of 'inverse_link_function', whether it is + differentiable. + + Parameters + ---------- + observation_model : object + An instance of an observation model that should have specific attributes. + + Raises + ------ + AttributeError + If the `observation_model` does not have one of the required attributes. + + TypeError + - If an attribute is not a callable function. + - If a function does not return a jax.numpy.ndarray. + - If 'inverse_link_function' is not differentiable. + + Examples + -------- + >>> class MyObservationModel: + ... def inverse_link_function(self, x): + ... return jax.scipy.special.expit(x) + ... def negative_log_likelihood(self, params, y_true): + ... return -jnp.sum(y_true * jax.scipy.special.logit(params) + (1 - y_true) * jax.scipy.special.logit(1 - params)) + ... def pseudo_r2(self, params, y_true): + ... return 1 - self.negative_log_likelihood(params, y_true) / jnp.sum((y_true - y_true.mean()) ** 2) + ... def sample_generator(self, key, params): + ... return jax.random.bernoulli(key, params) + >>> model = MyObservationModel() + >>> check_observation_model(model) # Should pass without error if the model is correctly implemented. + """ + # Define the checks to be made on each attribute + checks = { + "inverse_link_function": { + "input": [jnp.array([1.0, 1.0, 1.0])], + "test_differentiable": True, + "test_preserve_shape": 0, + }, + "negative_log_likelihood": { + "input": [0.5 * jnp.array([1.0, 1.0, 1.0]), jnp.array([1.0, 1.0, 1.0])], + "test_scalar_func": True, + }, + "pseudo_r2": { + "input": [0.5 * jnp.array([1.0, 1.0, 1.0]), jnp.array([1.0, 1.0, 1.0])], + "test_scalar_func": True, + }, + "sample_generator": { + "input": [jax.random.PRNGKey(123), 0.5 * jnp.array([1.0, 1.0, 1.0])], + "test_preserve_shape": 1, + }, + } + + # Perform checks for each attribute + for attr_name, check_info in checks.items(): + # check if the observation model has the attribute + utils.assert_has_attribute(observation_model, attr_name) + + # check if the attribute is a callable + func = getattr(observation_model, attr_name) + utils.assert_is_callable(func, attr_name) + + # check that the callable returns an array + utils.assert_returns_ndarray(func, check_info["input"], attr_name) + + if check_info.get("test_differentiable"): + utils.assert_differentiable(func, attr_name) + + if "test_preserve_shape" in check_info: + index = check_info["test_preserve_shape"] + utils.assert_preserve_shape( + func, check_info["input"], attr_name, input_index=index + ) + + if check_info.get("test_scalar_func"): + utils.assert_scalar_func(func, check_info["input"], attr_name) diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index 4d9cb9ba..3aa28647 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -4,7 +4,7 @@ import inspect from functools import partial -from typing import Callable, Iterable, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -494,28 +494,55 @@ def count_positional_params(func: Callable) -> int: ) -def check_loss(func: Callable): - """ - Check if the provided loss function is callable with 3 arguments. +def assert_has_attribute(obj: Any, attr_name: str): + """Ensure the object has the given attribute.""" + if not hasattr(obj, attr_name): + raise AttributeError( + f"The provided object does not have the required `{attr_name}` attribute!" + ) - Ensures that the given function is not only callable, but also that it - requires 3 positinal arguments. - Parameters - ---------- - func : - The function to check. - - Raises - ------ - TypeError - - If the provided loss is not callable. - - If the loss does not require 3 arguments. - """ +def assert_is_callable(func: Callable, func_name: str): + """Ensure the provided function is callable.""" if not callable(func): - raise TypeError("The loss function must be a Callable!") + raise TypeError(f"The `{func_name}` must be a Callable!") - if count_positional_params(func) != 3: + +def assert_returns_ndarray( + func: Callable, inputs: Union[List[jnp.ndarray], List[float]], func_name: str +): + """Ensure the function returns a jax.numpy.ndarray.""" + array_out = func(*inputs) + if not isinstance(array_out, jnp.ndarray): + raise TypeError(f"The `{func_name}` must return a jax.numpy.ndarray!") + + +def assert_differentiable(func: Callable, func_name: str): + """Ensure the function is differentiable.""" + try: + gradient_fn = jax.grad(func) + gradient_fn(jnp.array(1.0)) + except Exception as e: + raise TypeError(f"The `{func_name}` is not differentiable. Error: {str(e)}") + + +def assert_preserve_shape( + func: Callable, inputs: List[jnp.ndarray], func_name: str, input_index: int +): + """Check that the function preserve the input shape.""" + result = func(*inputs) + if not result.shape == inputs[input_index].shape: + raise ValueError(f"The `{func_name}` must preserve the input array shape!") + + +def assert_scalar_func(func: Callable, inputs: List[jnp.ndarray], func_name: str): + """Check that `func` return an array containing a single scalar.""" + assert_returns_ndarray(func, inputs, func_name) + array_out = func(*inputs) + try: + float(array_out) + except TypeError: raise TypeError( - "The loss function must require 3 positional inputs, usually (params, X, y)." + f"The `{func_name}` should return a scalar! " + f"Array of shape {array_out.shape} returned instead!" ) From d3664c337719dd87a76895bdc6f91d38ea6f7bda Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 16:49:07 -0400 Subject: [PATCH 159/250] improved docstring for score --- src/neurostatslib/glm.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 468b8aa1..6a84c8a8 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -104,7 +104,13 @@ def _predict( self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray ) -> jnp.ndarray: """ - Predict firing rates given predictors and parameters. + Predicts firing rates based on given parameters and design matrix. + + This function computes the predicted firing rates using the provided parameters + and model design matrix `X`. It is a streamlined version used internally within + optimization routines, where it serves as the loss function. Unlike the `GLM.predict` + method, it does not perform any input validation, assuming that the inputs are pre-validated. + Parameters ---------- @@ -181,9 +187,6 @@ def _score( This computes the negative log-likelihood up to a constant term. - Note that you can end up with infinities in here if there are zeros in - ``predicted_rates``. We raise a warning in that case. - Parameters ---------- params : @@ -208,10 +211,13 @@ def score( y: Union[NDArray, jnp.ndarray], score_type: Literal["log-likelihood", "pseudo-r2"] = "pseudo-r2", ) -> jnp.ndarray: - r"""Score the predicted firing rates (based on fit) to the target spike counts. + r"""Evaluates the goodness-of-fit of the model to the observed neural data. - This computes the GLM pseudo-$R^2$ or the mean log-likelihood, thus the higher the - number the better. + This method computes the goodness-of-fit score, which can either be the mean + log-likelihood or the pseudo-R^2. The scoring process includes validation of + input compatibility with the model's parameters, ensuring that the model + has been previously fitted and the input data are appropriate for scoring. A + higher score indicates a better fit of the model to the observed data. Parameters From 10255c6cb0ea59148fb77c3af7d404a74bf3fd18 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 18:09:48 -0400 Subject: [PATCH 160/250] bugfixed deviance --- src/neurostatslib/glm.py | 30 ++++++++++++++++++++----- src/neurostatslib/observation_models.py | 1 + 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 6a84c8a8..a9c3a3e4 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -250,9 +250,31 @@ def score( among which the number of model parameters. The log-likelihood can assume both positive and negative values. - The Pseudo-$R^2$ is not equivalent to the $R^2$ value in linear regression. While both provide a measure - of model fit, and assume values in the [0,1] range, the methods and interpretations can differ. - The Pseudo-$R^2$ is particularly useful for generalized linear models where a traditional $R^2$ doesn't apply. + The Pseudo-$ R^2 $ is not equivalent to the $ R^2 $ value in linear regression. While both + provide a measure of model fit, and assume values in the [0,1] range, the methods and + interpretations can differ. The Pseudo-$ R^2 $ is particularly useful for generalized linear + models when the interpretation of the $ R^2 $ as explained variance does not apply + (i.e., when the observations are not Gaussian distributed). + + Why does the traditional $R^2$ is usually a poor measure of performance in GLMs? + + 1. In the context of GLMs the variance and the mean of the observations are related. + Ignoring the relation between them can result in underestimating the model + performance; for instance, when we model a Poisson variable with large mean we expect an + equally large variance. In this scenario, even if our model perfectly captures the mean, + the high-variance will result in large residuals and low $R^2$. + Additionally, when the mean of the observations varies, the variance will vary too. This + violates the "homoschedasticity" assumption, necessary for interpreting the $R^2$ as + variance explained. + 2. The $R^2$ capture the variance explained when the relationship between the observations and + the predictors is linear. In GLMs, the link function sets a non-linear mapping between the predictors + and the mean of the observations, compromising the interpretation of the $R^2$. + + Note that it is possible to re-normalized the residuals by a mean-dependent quantity proportional + to the model standard deviation (i.e. Pearson residuals). This "rescaled" residual distribution however + deviates substantially from normality for counting data with low mean (common for spike counts). + Therefore, even the Pearson residuals performs poorly as a measure of fit quality, especially + for GLM modeling counting data. The pseudo-$R^2$ can be computed as follows, @@ -283,8 +305,6 @@ def score( f"Scoring method {score_type} not implemented! " f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." ) - # ignore the last time point from predict, because that corresponds to - # the next time step, which we have no observed data for self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ diff --git a/src/neurostatslib/observation_models.py b/src/neurostatslib/observation_models.py index 61eb84b2..93559216 100644 --- a/src/neurostatslib/observation_models.py +++ b/src/neurostatslib/observation_models.py @@ -395,6 +395,7 @@ def residual_deviance( resid_dev = 2 * ( spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) ) + resid_dev = jnp.sign(spike_counts - predicted_rate) * jnp.sqrt(jnp.clip(resid_dev, 0., jnp.inf)) return resid_dev def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: From ecb069e4358fcc6544cb567a672e2c4889de5cdb Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 3 Nov 2023 18:36:33 -0400 Subject: [PATCH 161/250] renamed deviance --- src/neurostatslib/glm.py | 5 ++--- src/neurostatslib/observation_models.py | 21 ++++++++++----------- tests/test_glm.py | 6 +++--- tests/test_observation_models.py | 2 +- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index a9c3a3e4..93dd6b4c 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -1,5 +1,4 @@ """GLM core module.""" -import inspect from typing import Literal, Optional, Tuple, Union import jax @@ -77,8 +76,8 @@ def solver(self, solver: slv.Solver): solver.instantiate_solver(self._score) except Exception: raise TypeError( - f"The provided `solver` cannot be instantiated on " - f"the GLM log-likelihood." + "The provided `solver` cannot be instantiated on " + "the GLM log-likelihood." ) self._solver = solver diff --git a/src/neurostatslib/observation_models.py b/src/neurostatslib/observation_models.py index 93559216..f7610aea 100644 --- a/src/neurostatslib/observation_models.py +++ b/src/neurostatslib/observation_models.py @@ -166,7 +166,7 @@ def sample_generator( pass @abc.abstractmethod - def residual_deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray): + def deviance(self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray): r"""Compute the residual deviance for the observation model. Parameters @@ -254,11 +254,11 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): *Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences*. 3rd edition. Routledge, 2002. p.502. ISBN 978-0-8058-2223-6. (May 2012) """ - res_dev_t = self.residual_deviance(predicted_rate, y) + res_dev_t = self.deviance(predicted_rate, y) resid_deviance = jnp.sum(res_dev_t**2) null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() - null_dev_t = self.residual_deviance(null_mu, y) + null_dev_t = self.deviance(null_mu, y) null_deviance = jnp.sum(null_dev_t**2) return (null_deviance - resid_deviance) / null_deviance @@ -357,7 +357,7 @@ def sample_generator( """ return jax.random.poisson(key, predicted_rate) - def residual_deviance( + def deviance( self, predicted_rate: jnp.ndarray, spike_counts: jnp.ndarray ) -> jnp.ndarray: r"""Compute the residual deviance for a Poisson model. @@ -392,11 +392,8 @@ def residual_deviance( """ # this takes care of 0s in the log ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) - resid_dev = 2 * ( - spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate) - ) - resid_dev = jnp.sign(spike_counts - predicted_rate) * jnp.sqrt(jnp.clip(resid_dev, 0., jnp.inf)) - return resid_dev + deviance = 2 * (spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate)) + return deviance def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: r""" @@ -449,9 +446,11 @@ def check_observation_model(observation_model): ... def inverse_link_function(self, x): ... return jax.scipy.special.expit(x) ... def negative_log_likelihood(self, params, y_true): - ... return -jnp.sum(y_true * jax.scipy.special.logit(params) + (1 - y_true) * jax.scipy.special.logit(1 - params)) + ... return -jnp.sum(y_true * jax.scipy.special.logit(params) + \ + ... (1 - y_true) * jax.scipy.special.logit(1 - params)) ... def pseudo_r2(self, params, y_true): - ... return 1 - self.negative_log_likelihood(params, y_true) / jnp.sum((y_true - y_true.mean()) ** 2) + ... return 1 - (self.negative_log_likelihood(params, y_true) / + ... jnp.sum((y_true - y_true.mean()) ** 2)) ... def sample_generator(self, key, params): ... return jax.random.bernoulli(key, params) >>> model = MyObservationModel() diff --git a/tests/test_glm.py b/tests/test_glm.py index 2f46990f..d32e0d0c 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -52,8 +52,8 @@ def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiat "observation, error, match_str", [ (nsl.observation_models.PoissonObservations(), None, None), - (nsl.solver.Solver, TypeError, "The provided `observation_model` should be one of the implemented"), - (1, TypeError, "The provided `observation_model` should be one of the implemented") + (nsl.solver.Solver, AttributeError, "The provided object does not have the required"), + (1, AttributeError, "The provided object does not have the required") ] ) def test_init_observation_type(self, observation, error, match_str, ridge_solver): @@ -1013,7 +1013,7 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): model.baseline_link_fr_ = true_params[1] # get the rate dev = sm.families.Poisson().deviance(y, firing_rate) - dev_model = model.observation_model.residual_deviance(firing_rate, y).sum() + dev_model = model.observation_model.deviance(firing_rate, y).sum() if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") diff --git a/tests/test_observation_models.py b/tests/test_observation_models.py index e857631e..cd94980d 100644 --- a/tests/test_observation_models.py +++ b/tests/test_observation_models.py @@ -74,7 +74,7 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): """ _, y, model, _, firing_rate = poissonGLM_model_instantiation dev = sm.families.Poisson().deviance(y, firing_rate) - dev_model = model.observation_model.residual_deviance(firing_rate, y).sum() + dev_model = model.observation_model.deviance(firing_rate, y).sum() if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") From 662dfbc11881ff5fc92b5053b0e48c2802eacf0e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Sat, 4 Nov 2023 18:11:51 -0400 Subject: [PATCH 162/250] moved raise in the if close --- src/neurostatslib/glm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 93dd6b4c..418d05c4 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -299,11 +299,6 @@ def score( Routledge, 2013. """ - if score_type not in ["log-likelihood", "pseudo-r2"]: - raise NotImplementedError( - f"Scoring method {score_type} not implemented! " - f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." - ) self._check_is_fit() Ws = self.basis_coeff_ bs = self.baseline_link_fr_ @@ -313,12 +308,17 @@ def score( self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) self._check_input_and_params_consistency((Ws, bs), X=X, y=y) + if score_type == "log-likelihood": norm_constant = jax.scipy.special.gammaln(y + 1).mean() score = -self._score((Ws, bs), X, y) - norm_constant - else: + elif score_type == "pseudo-r2": score = self._observation_model.pseudo_r2(self._predict((Ws, bs), X), y) - + else: + raise NotImplementedError( + f"Scoring method {score_type} not implemented! " + f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." + ) return score def fit( From 3c0fb3f4d1aa87642ad89c6518f75f977ed860d4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Sat, 4 Nov 2023 18:59:43 -0400 Subject: [PATCH 163/250] commented scanf --- src/neurostatslib/glm.py | 53 +++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 418d05c4..9b89413e 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -329,7 +329,7 @@ def fit( ): """Fit GLM to neural activity. - Following scikit-learn API, the solutions are stored as attributes + Fit and store the model parameters as attributes ``basis_coeff_`` and ``baseline_link_fr``. Parameters @@ -362,9 +362,9 @@ def fit( # Run optimization runner = self.solver.instantiate_solver(self._score) params, state = runner(init_params, X, y) - # if any observation model other than Poisson are used - # one should set the scale parameter too. - # self.observation_model.set_scale(params) + + # estimate the GLM scale + self.observation_model.estimate_scale(self._predict(params, X)) if jnp.isnan(params[0]).any() or jnp.isnan(params[1]).any(): raise ValueError( @@ -438,10 +438,10 @@ class GLMRecurrent(GLM): """ A Generalized Linear Model (GLM) with recurrent dynamics. - This class extends the basic GLM to capture recurrent dynamics between neurons, - making it more suitable for simulating the activity of interconnected neural populations. - The recurrent GLM combines both feedforward inputs (like sensory stimuli) and past - neural activity to simulate or predict future neural activity. + This class extends the basic GLM to capture recurrent dynamics between neurons and + self-connectivity, making it more suitable for simulating the activity of interconnected + neural populations. The recurrent GLM combines both feedforward inputs (like sensory + stimuli) and past neural activity to simulate or predict future neural activity. Parameters ---------- @@ -461,15 +461,8 @@ class GLMRecurrent(GLM): inputs and the past activity of the same and other neurons. This makes it particularly powerful for capturing the dynamics of neural networks where neurons are interconnected. - - The attributes of `GLMRecurrent` are inherited from the parent `GLM` class, and might include + - The attributes of `GLMRecurrent` are inherited from the parent `GLM` class, and include coefficients, fitted status, and other model-related attributes. - - Examples - -------- - >>> # Initialize the recurrent GLM with default parameters - >>> model = GLMRecurrent() - >>> # ... your code for training and simulating using the model ... - """ def __init__( @@ -546,27 +539,27 @@ def simulate_recurrent( self._check_is_fit() # convert to jnp.ndarray - (coupling_basis_matrix,) = utils.convert_to_jnp_ndarray(coupling_basis_matrix) + (coupling_basis_matrix,) = utils.convert_to_jnp_ndarray( + coupling_basis_matrix, + data_type=jnp.float_ + ) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] - if init_y is None: - init_y = jnp.zeros((coupling_basis_matrix.shape[0], n_neurons)) - - Wf = self.basis_coeff_[:, n_basis_coupling * n_neurons :] - Wr = self.basis_coeff_[:, : n_basis_coupling * n_neurons] + w_feedforward = self.basis_coeff_[:, n_basis_coupling * n_neurons:] + w_recurrent = self.basis_coeff_[:, : n_basis_coupling * n_neurons] bs = self.baseline_link_fr_ feedforward_input, init_y = self._preprocess_simulate( feedforward_input, - params_feedforward=(Wf, bs), + params_feedforward=(w_feedforward, bs), init_y=init_y, - params_recurrent=(Wr, bs), + params_recurrent=(w_recurrent, bs), ) self._check_input_and_params_consistency( - (Wr, bs), + (w_recurrent, bs), y=init_y, ) @@ -580,7 +573,7 @@ def simulate_recurrent( subkeys = jax.random.split(random_key, num=feedforward_input.shape[0]) # (n_samples, n_neurons) - feed_forward_contrib = jnp.einsum("ik,tik->ti", Wf, feedforward_input) + feed_forward_contrib = jnp.einsum("ik,tik->ti", w_feedforward, feedforward_input) def scan_fn( data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray @@ -604,7 +597,11 @@ def scan_fn( (1, feed_forward_contrib.shape[1]), ) - # Reshape the convolved activity and concatenate with the input slice to form the model input + # Repeat the convolutoin `n_neuron` times with the following steps + # 1) Initial convolution output shape (1, n_neuron, n_basis_coupling) + # 2) Reshape that to (1, n_neuron * n_basis_coupling) + # 3) Tile it n_neuron times, shape (1, n_neurons**2 * n_basis_coupling) + # 4) Reshape it to (1, n_neurons, n_neuron * n_basis_coupling) conv_act = jnp.tile( conv_act.reshape(conv_act.shape[0], -1), conv_act.shape[1] ).reshape(conv_act.shape[0], conv_act.shape[1], -1) @@ -613,7 +610,7 @@ def scan_fn( # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input firing_rate = self._observation_model.inverse_link_function( - jnp.einsum("ik,tik->ti", Wr, conv_act) + input_slice + bs[None, :] + jnp.einsum("ik,tik->ti", w_recurrent, conv_act) + input_slice + bs[None, :] ) # Simulate activity based on the predicted firing rate From dbd3bbdbdc45910c959b0638a33e259bd06568d7 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 6 Nov 2023 12:32:00 -0500 Subject: [PATCH 164/250] improved scanf algorithm --- src/neurostatslib/glm.py | 58 ++++++++++++------------ src/neurostatslib/proximal_operator.py | 62 +++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 29 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 9b89413e..065f2767 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -540,14 +540,13 @@ def simulate_recurrent( # convert to jnp.ndarray (coupling_basis_matrix,) = utils.convert_to_jnp_ndarray( - coupling_basis_matrix, - data_type=jnp.float_ + coupling_basis_matrix, data_type=jnp.float_ ) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.baseline_link_fr_.shape[0] - w_feedforward = self.basis_coeff_[:, n_basis_coupling * n_neurons:] + w_feedforward = self.basis_coeff_[:, n_basis_coupling * n_neurons :] w_recurrent = self.basis_coeff_[:, : n_basis_coupling * n_neurons] bs = self.baseline_link_fr_ @@ -573,7 +572,9 @@ def simulate_recurrent( subkeys = jax.random.split(random_key, num=feedforward_input.shape[0]) # (n_samples, n_neurons) - feed_forward_contrib = jnp.einsum("ik,tik->ti", w_feedforward, feedforward_input) + feed_forward_contrib = jnp.einsum( + "ik,tik->ti", w_feedforward, feedforward_input + ) def scan_fn( data: Tuple[jnp.ndarray, int], key: jax.random.PRNGKeyArray @@ -583,45 +584,46 @@ def scan_fn( This function simulates the neural activity and firing rates for each time step based on the previous activity, feedforward input, and model coefficients. """ - activity, chunk = data + activity, t_sample = data # Convolve the neural activity with the coupling basis matrix - conv_act = utils.convolve_1d_trials(coupling_basis_matrix, activity[None])[ - 0 - ] + # squeeze the first dimension (time) because by construction + # is going to be 1 time sample + conv_act = utils.convolve_1d_trials( + coupling_basis_matrix, + activity[None] + )[0].squeeze(axis=0) + + # Initial convolution output shape (n_neuron, n_basis_coupling) + # Flatten to (n_neuron * n_basis_coupling, ) + conv_act = conv_act.flatten() # Extract the slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( feed_forward_contrib, - (chunk, 0), + (t_sample, 0), (1, feed_forward_contrib.shape[1]), - ) - - # Repeat the convolutoin `n_neuron` times with the following steps - # 1) Initial convolution output shape (1, n_neuron, n_basis_coupling) - # 2) Reshape that to (1, n_neuron * n_basis_coupling) - # 3) Tile it n_neuron times, shape (1, n_neurons**2 * n_basis_coupling) - # 4) Reshape it to (1, n_neurons, n_neuron * n_basis_coupling) - conv_act = jnp.tile( - conv_act.reshape(conv_act.shape[0], -1), conv_act.shape[1] - ).reshape(conv_act.shape[0], conv_act.shape[1], -1) + ).squeeze(axis=0) # Predict the firing rate using the model coefficients # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input firing_rate = self._observation_model.inverse_link_function( - jnp.einsum("ik,tik->ti", w_recurrent, conv_act) + input_slice + bs[None, :] + w_recurrent.dot(conv_act) + + input_slice + + bs ) # Simulate activity based on the predicted firing rate new_act = self._observation_model.sample_generator(key, firing_rate) - # Prepare the spikes for the next iteration (keeping the most recent spikes) - concat_act = jnp.row_stack((activity[1:], new_act)), chunk + 1 - return concat_act, (new_act, firing_rate) - - _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) + # Shift of one sample the spike count window + # for the next iteration (i.e. remove the first counts, and + # stack the newly generated sample) + # Increase the t_sample by one + carry = jnp.row_stack((activity[1:], new_act)), t_sample + 1 + return carry, (new_act, firing_rate) + with jax.disable_jit(True): + _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) simulated_activity, firing_rates = outputs - return jnp.squeeze(simulated_activity, axis=1), jnp.squeeze( - firing_rates, axis=1 - ) + return simulated_activity, firing_rates diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index a20b8d10..b0bd1ec8 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -1,4 +1,64 @@ -"""Collection of proximal operators.""" +r"""Collection of proximal operators. + +## Definition + +In optimization theory, the proximal operator is a mathematical tool used to solve non-differentiable optimization +problems or to simplify complex ones. + +The proximal operator of a function $ f: \mathbb{R}^n \rightarrow \mathbb{R} \cup \{+\infty\} $ is defined as follows: + +$$ +\text{prox}_f(v) = \arg\min_x \left( f(x) + \frac{1}{2}\Vert x - v\Vert_2 ^2 \right) +$$ + +Here $ \text{prox}_f(v) $ is the value of $ x $ that minimizes the sum of the function $ f(x) $ and the +squared Euclidean distance between $ x $ and some point $ v $. The parameter $ f $ typically represents +a regularization term or a penalty in the optimization problem, and $ v $ is typically a vector +in the domain of $ f $. + +The proximal operator can be thought of as a generalization of the projection operator. When $ f $ is the +indicator function of a convex set $ C $, then $ \text{prox}_f $ is the projection onto $ C $, since +it finds the point in $ C $ closest to $ v $. + +Proximal operators are central to the implementation of proximal gradient[^1] methods and algorithms like where they +help to break down complex optimization problems into simpler sub-problems that can be solved iteratively. + +## Proximal Operators in Proximal Gradient Algorithms + +Proximal gradient algorithms are designed to solve optimization problems of the form: + +$$ +\min_{x \in \mathbb{R}^n} g(x) + f(x) +$$ + +where $ g $ is a differentiable (and typically convex) function, and $ f $ is a (possibly non-differentiable) convex +function that imposes certain structure or sparsity in the solution. The proximal gradient method updates the +solution iteratively through a two-step process: + +1. **Gradient Step on $ g $**: Take a step towards the direction of the negative gradient of $ g $ at the current +estimate $ x_k $, with a step size $ \alpha_k $, leading to an intermediate estimate $ y_k $: + $$ + y_k = x_k - \alpha_k \nabla g(x_k) + $$ +2. **Proximal Step on $ f $**: Apply the proximal operator of $ f $ to the intermediate +estimate $ y_k $ to obtain the new estimate $ x_{k+1} $: + + $$ + x_{k+1} = \text{prox}_{ f}(y_k) = \arg\min_x \left( f(x) + \frac{1}{2\alpha_k}\Vert x - y_k \Vert_2 ^2 \right) + $$ + +The gradient step aims to reduce the value of the smooth part of the objective $ g $, and the proximal step +takes care of the non-smooth part $ f $, often enforcing properties like sparsity due to regularization terms +such as the $ \ell_1 $ norm. + +By iteratively performing these two steps, the proximal gradient algorithm converges to a solution that +balances minimizing the differentiable part $ g $ while respecting the structure imposed by the non-differentiable +part $ f $. The proximal operator effectively "proximates" the solution at each iteration, +taking into account the influence of the non-smooth term $ f $, which would be otherwise challenging to +handle due to its potential non-differentiability. + +[^1]: Parikh, Neal, and Stephen Boyd. "Proximal Algorithms, ser. Foundations and Trends (r) in Optimization." (2013). +""" from typing import Tuple import jax From 5151f7aa5992e6673867f20be32fa8350c7e79d4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 6 Nov 2023 12:41:36 -0500 Subject: [PATCH 165/250] improved comments --- src/neurostatslib/glm.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 065f2767..1085f704 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -587,16 +587,14 @@ def scan_fn( activity, t_sample = data # Convolve the neural activity with the coupling basis matrix - # squeeze the first dimension (time) because by construction - # is going to be 1 time sample + # Output of shape (1, n_neuron, n_basis_coupling) + # 1. The first dimension is time, and 1 is by construction since we are simulating 1 + # sample + # 2. Flatten to shape (n_neuron * n_basis_coupling, ) conv_act = utils.convolve_1d_trials( coupling_basis_matrix, activity[None] - )[0].squeeze(axis=0) - - # Initial convolution output shape (n_neuron, n_basis_coupling) - # Flatten to (n_neuron * n_basis_coupling, ) - conv_act = conv_act.flatten() + )[0].flatten() # Extract the slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( From b7aaa25745a34fd0464c902fc13fa7068b012fa5 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 6 Nov 2023 16:48:41 -0500 Subject: [PATCH 166/250] add test loss is callable --- src/neurostatslib/proximal_operator.py | 55 +++++++++++-- src/neurostatslib/solver.py | 92 +++++++++++----------- src/neurostatslib/utils.py | 9 ++- tests/test_solver.py | 104 ++++++++++--------------- 4 files changed, 144 insertions(+), 116 deletions(-) diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index b0bd1ec8..f3bed670 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -66,7 +66,15 @@ def _norm2_masked(weight_neuron: jnp.ndarray, mask: jnp.ndarray) -> jnp.ndarray: - """Group-by-group norm 2. + """Euclidean norm of the group. + + Calculate the Euclidean norm of the weights for a specified group within a + neuron's feature vector. + + This function computes the norm of elements that are indicated by the mask array. + If 'mask' were of boolean type, this operation would be equivalent to performing + `jnp.linalg.norm(weight_neuron[mask], 2)` followed by dividing the result by the + square root of the sum of the mask (assuming each group has at least 1 feature). Parameters ---------- @@ -83,17 +91,19 @@ def _norm2_masked(weight_neuron: jnp.ndarray, mask: jnp.ndarray) -> jnp.ndarray: Notes ----- - The proximal gradient operator is described in article [1], Proposition 1. + The proximal gradient operator is described in Ming at al.[^1], Proposition 1. - .. [1] Yuan, Ming, and Yi Lin. "Model selection and estimation in regression with grouped variables." - Journal of the Royal Statistical Society Series B: Statistical Methodology 68.1 (2006): 49-67. + [^1]: + Yuan, Ming, and Yi Lin. "Model selection and estimation in regression with grouped variables." + Journal of the Royal Statistical Society Series B: Statistical Methodology 68.1 (2006): 49-67. """ return jnp.linalg.norm(weight_neuron * mask, 2) / jnp.sqrt(mask.sum()) # vectorize the norm function above -# [(n_neurons, n_features), (n_groups, n_features)] -> (n_neurons, n_groups) +# [(n_neurons, n_features), (n_features)] -> (n_neurons, ) _vmap_norm2_masked_1 = jax.vmap(_norm2_masked, in_axes=(0, None), out_axes=0) +# [(n_neurons, n_features), (n_groups, n_features)] -> (n_neurons, n_groups) _vmap_norm2_masked_2 = jax.vmap(_vmap_norm2_masked_1, in_axes=(None, 0), out_axes=1) @@ -103,7 +113,7 @@ def prox_group_lasso( mask: jnp.ndarray, scaling: float = 1.0, ) -> Tuple[jnp.ndarray, jnp.ndarray]: - """Proximal gradient operator for group lasso. + r"""Proximal gradient operator for group Lasso. Parameters ---------- @@ -121,9 +131,40 @@ def prox_group_lasso( ------- : The rescaled weights. + + Notes + ----- + This function implements the proximal operator for a group-Lasso penalization which + can be derived in analytical form. + The proximal operator equation are, + + $$ + \text{prox}(\beta_g) = \text{min}_{\beta} \left[ \lambda \sum\_{g=1}^G \Vert \beta_g \Vert_2 + + \frac{1}{2} \Vert \hat{\beta} - \beta \Vert_2^2 + \right], + $$ + where $G$ is the number of groups, and $\beta_g$ is the parameter vector + associated with the $g$-th group. + The analytical solution[^1] for the beta is, + + $$ + \text{prox}(\beta\_g) = \max \left(1 - \frac{\lambda \sqrt{p\_g}}{\Vert \hat{\beta}\_g \Vert_2}, + 0\right) \cdot \hat{\beta}\_g, + $$ + where $p_g$ is the dimensionality of $\beta\_g$ and $\hat{\beta}$ is typically the gradient step + of the un-regularized optimization objective function. It's easy to see how the group-Lasso + proximal operator acts as a shrinkage factor for the un-penalize update, and the half-rectification + non-linearity that effectively sets to zero group of coefficients satisfying, + $$ + \Vert \hat{\beta}\_g \Vert_2 \le \frac{1}{\lambda \sqrt{p\_g}}. + $$ + + [^1]: + Yuan, Ming, and Yi Lin. "Model selection and estimation in regression with grouped variables." + Journal of the Royal Statistical Society Series B: Statistical Methodology 68.1 (2006): 49-67. """ weights, intercepts = params - # returns a (n_neurons, n_groups) matrix of norm 2s. + # [(n_neurons, n_features), (n_groups, n_features)] -> (n_neurons, n_groups) l2_norm = _vmap_norm2_masked_2(weights, mask) factor = 1 - regularizer_strength * scaling / l2_norm factor = jax.nn.relu(factor) diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 948cb87e..beac5544 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -1,5 +1,5 @@ """ -## A Module for Optimization with Various Regularizations. +A Module for Optimization with Various Regularizations. This module provides a series of classes that facilitate the optimization of models with different types of regularizations. Each solver class in this module interfaces @@ -15,6 +15,23 @@ from .base_class import Base from .proximal_operator import prox_group_lasso +from . import utils + +SolverRunner = Callable[ + [ + Tuple[jnp.ndarray, jnp.ndarray], # Model parameters (for now tuple, eventually pytree) + jnp.ndarray, # Predictors (i.e. model design for GLM) + jnp.ndarray], # Output (neural activity) + jaxopt.OptStep + ] + +ProximalOperator = Callable[ + [ + Tuple[jnp.ndarray, jnp.ndarray], # Model parameters (for now tuple, eventually pytree) + float, # Regularizer strength (for now float, eventually pytree) + float], # Step-size for optimization (must be a float) + Tuple[jnp.ndarray, jnp.ndarray] +] __all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] @@ -33,7 +50,7 @@ class Solver(Base, abc.ABC): Attributes ---------- - allowed_optimizers : + allowed_algorithms : List of optimizer names that are allowed for use with this solver. solver_name : Name of the solver being used. @@ -48,7 +65,7 @@ class Solver(Base, abc.ABC): Get the solver runner with provided arguments. """ - allowed_optimizers: List[str] = [] + allowed_algorithms: List[str] = [] def __init__( self, @@ -97,11 +114,11 @@ def _check_solver(self, solver_name: str): ValueError If the provided solver name is not in the list of allowed optimizers. """ - if solver_name not in self.allowed_optimizers: + if solver_name not in self.allowed_algorithms: raise ValueError( f"Solver `{solver_name}` not allowed for " f"{self.__class__} regularization. " - f"Allowed solvers are {self.allowed_optimizers}." + f"Allowed solvers are {self.allowed_algorithms}." ) @staticmethod @@ -131,12 +148,8 @@ def _check_solver_kwargs(solver_name, solver_kwargs): @abc.abstractmethod def instantiate_solver( self, - loss: Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray - ], - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep - ]: + loss: Callable, + ) -> SolverRunner: """Abstract method to instantiate a solver with a given loss function.""" pass @@ -145,9 +158,7 @@ def get_runner( loss: Callable, *run_args: Any, **run_kwargs: dict, - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep - ]: + ) -> SolverRunner: """ Get the solver runner with provided arguments. @@ -162,6 +173,11 @@ def get_runner( ------- : The solver runner. + + Raises + ------ + TypeError + If the loss function is not a callable. """ # get the solver with given arguments. # The "fun" argument is not always the first one, but it is always KEYWORD @@ -200,7 +216,7 @@ class are defined in the `allowed_optimizers` attribute. [Solver](./#neurostatslib.solver.Solver) : Base solver class from which this class inherits. """ - allowed_optimizers = [ + allowed_algorithms = [ "GradientDescent", "BFGS", "LBFGS", @@ -217,12 +233,8 @@ def __init__( def instantiate_solver( self, - loss: Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray - ], - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep - ]: + loss: Callable, + ) -> SolverRunner: """ Instantiate the optimization algorithm for a given loss function. @@ -237,6 +249,8 @@ def instantiate_solver( A runner function that uses the specified optimization algorithm to minimize the given loss function. """ + # check that the loss is Callable + utils.assert_is_callable(loss, "loss") return self.get_runner(loss) @@ -253,7 +267,7 @@ class RidgeSolver(Solver): A list of optimizer names that are allowed to be used with this solver. """ - allowed_optimizers = [ + allowed_algorithms = [ "GradientDescent", "BFGS", "LBFGS", @@ -295,12 +309,8 @@ def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: def instantiate_solver( self, - loss: Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray - ], - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep - ]: + loss: Callable, + ) -> SolverRunner: """ Instantiate the solver with a penalized loss function. @@ -314,6 +324,8 @@ def instantiate_solver( Callable A function that runs the solver with the penalized loss. """ + # check that the loss is Callable + utils.assert_is_callable(loss, "loss") def penalized_loss(params, X, y): return loss(params, X, y) + self.penalization(params) @@ -334,7 +346,7 @@ class ProxGradientSolver(Solver, abc.ABC): A list of optimizer names that are allowed to be used with this solver. """ - allowed_optimizers = ["ProximalGradient"] + allowed_algorithms = ["ProximalGradient"] def __init__( self, @@ -353,9 +365,7 @@ def __init__( @abc.abstractmethod def get_prox_operator( self, - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] - ]: + ) -> ProximalOperator: """ Abstract method to retrieve the proximal operator for this solver. @@ -368,12 +378,8 @@ def get_prox_operator( def instantiate_solver( self, - loss: Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jnp.ndarray - ], - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], jnp.ndarray, jnp.ndarray], jaxopt.OptStep - ]: + loss: Callable, + ) -> SolverRunner: """ Instantiate the solver with the provided loss function and proximal operator. @@ -387,6 +393,8 @@ def instantiate_solver( : A function that runs the solver with the provided loss and proximal operator. """ + # check that the loss is Callable + utils.assert_is_callable(loss, "loss") return self.get_runner(loss, self.regularizer_strength) @@ -409,9 +417,7 @@ def __init__( def get_prox_operator( self, - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] - ]: + ) -> ProximalOperator: """ Retrieve the proximal operator for Lasso regularization (L1 penalty). @@ -522,9 +528,7 @@ def _check_mask(mask: jnp.ndarray): def get_prox_operator( self, - ) -> Callable[ - [Tuple[jnp.ndarray, jnp.ndarray], float, float], Tuple[jnp.ndarray, jnp.ndarray] - ]: + ) -> ProximalOperator: """ Retrieve the proximal operator for Group Lasso regularization. diff --git a/src/neurostatslib/utils.py b/src/neurostatslib/utils.py index 3aa28647..1bf495d9 100644 --- a/src/neurostatslib/utils.py +++ b/src/neurostatslib/utils.py @@ -343,7 +343,11 @@ def row_wise_kron(A: jnp.array, C: jnp.array, jit=False, transpose=True) -> jnp. """Compute the row-wise Kronecker product. Compute the row-wise Kronecker product between two matrices using JAX. - See [1] for more details on the Kronecker product. + See[^1] for more details on the Kronecker product. + + [^1]: + Petersen, Kaare Brandt, and Michael Syskind Pedersen. "The matrix cookbook." + Technical University of Denmark 7.15 (2008): 510. Parameters ---------- @@ -366,9 +370,6 @@ def row_wise_kron(A: jnp.array, C: jnp.array, jit=False, transpose=True) -> jnp. This function computes the row-wise Kronecker product between dense matrices A and C using JAX for automatic differentiation and GPU acceleration. - .. [1] Petersen, Kaare Brandt, and Michael Syskind Pedersen. "The matrix cookbook." - Technical University of Denmark 7.15 (2008): 510. - """ if transpose: A = A.T diff --git a/tests/test_solver.py b/tests/test_solver.py index ffb1bc75..1e03cd04 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -70,15 +70,15 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - # def test_loss_is_callable(self, loss): - # """Test that the loss function is a callable""" - # raise_exception = not callable(loss) - # if raise_exception: - # with pytest.raises(TypeError, match="The loss function must be a Callable"): - # self.cls("GradientDescent").instantiate_solver(loss) - # else: - # self.cls("GradientDescent").instantiate_solver(loss) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + def test_loss_is_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + if raise_exception: + with pytest.raises(TypeError, match="The `loss` must be a Callable"): + self.cls("GradientDescent").instantiate_solver(loss) + else: + self.cls("GradientDescent").instantiate_solver(loss) @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) def test_run_solver(self, solver_name, poissonGLM_model_instantiation): @@ -180,33 +180,15 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): else: self.cls(solver_name, solver_kwargs=solver_kwargs) - # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - # def test_loss_is_callable(self, loss): - # """Test that the loss function is a callable""" - # raise_exception = not callable(loss) - # if raise_exception: - # with pytest.raises(TypeError, match="The loss function must be a Callable"): - # self.cls("GradientDescent").instantiate_solver(loss) - # else: - # self.cls("GradientDescent").instantiate_solver(loss) - - # @pytest.mark.parametrize("loss, raise_exception", [ - # (lambda a, b, c: 0, False), - # (lambda a, b: 0, True), - # (lambda a, b, c, d: 0, True), - # (lambda a, b, c, *d: 0, False), - # (lambda a, b, c, **d: 0, False), - # (lambda a, /, b, c, *d, **e: 0, False) - # ] - # ) - # def test_loss_has_three_parameter_callable(self, loss, raise_exception): - # """Test that the loss function is a callable""" - # pass - # if raise_exception: - # with pytest.raises(TypeError, match="The loss function must require 3 positional"): - # self.cls("GradientDescent").instantiate_solver(loss) - # else: - # self.cls("GradientDescent").instantiate_solver(loss) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + def test_loss_is_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + if raise_exception: + with pytest.raises(TypeError, match="The `loss` must be a Callable"): + self.cls("GradientDescent").instantiate_solver(loss) + else: + self.cls("GradientDescent").instantiate_solver(loss) @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) def test_run_solver(self, solver_name, poissonGLM_model_instantiation): @@ -294,15 +276,15 @@ def test_init_solver_kwargs(self, solver_kwargs): else: self.cls("ProximalGradient", solver_kwargs=solver_kwargs) - # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - # def test_loss_callable(self, loss): - # """Test that the loss function is a callable""" - # raise_exception = not callable(loss) - # if raise_exception: - # with pytest.raises(TypeError, match="The loss function must be a Callable"): - # self.cls("ProximalGradient").instantiate_solver(loss) - # else: - # self.cls("ProximalGradient").instantiate_solver(loss) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + def test_loss_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + if raise_exception: + with pytest.raises(TypeError, match="The `loss` must be a Callable"): + self.cls("ProximalGradient").instantiate_solver(loss) + else: + self.cls("ProximalGradient").instantiate_solver(loss) def test_run_solver(self, poissonGLM_model_instantiation): """Test that the solver runs.""" @@ -401,22 +383,22 @@ def test_init_solver_kwargs(self, solver_kwargs): else: self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) - # @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) - # def test_loss_callable(self, loss): - # """Test that the loss function is a callable""" - # raise_exception = not callable(loss) - # - # # create a valid mask - # mask = np.zeros((2, 10)) - # mask[0, :5] = 1 - # mask[1, 5:] = 1 - # mask = jnp.asarray(mask) - # - # if raise_exception: - # with pytest.raises(TypeError, match="The loss function must be a Callable"): - # self.cls("ProximalGradient", mask).instantiate_solver(loss) - # else: - # self.cls("ProximalGradient", mask).instantiate_solver(loss) + @pytest.mark.parametrize("loss", [lambda a, b, c: 0, 1, None, {}]) + def test_loss_callable(self, loss): + """Test that the loss function is a callable""" + raise_exception = not callable(loss) + + # create a valid mask + mask = np.zeros((2, 10)) + mask[0, :5] = 1 + mask[1, 5:] = 1 + mask = jnp.asarray(mask) + + if raise_exception: + with pytest.raises(TypeError, match="The `loss` must be a Callable"): + self.cls("ProximalGradient", mask).instantiate_solver(loss) + else: + self.cls("ProximalGradient", mask).instantiate_solver(loss) def test_run_solver(self, poissonGLM_model_instantiation): """Test that the solver runs.""" From 355199d63403407b52f1fc27089b1e99ba0ede00 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 6 Nov 2023 17:02:38 -0500 Subject: [PATCH 167/250] linted --- src/neurostatslib/glm.py | 12 ++++----- src/neurostatslib/solver.py | 53 ++++++++++++++----------------------- 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 1085f704..0eaa98eb 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -591,10 +591,9 @@ def scan_fn( # 1. The first dimension is time, and 1 is by construction since we are simulating 1 # sample # 2. Flatten to shape (n_neuron * n_basis_coupling, ) - conv_act = utils.convolve_1d_trials( - coupling_basis_matrix, - activity[None] - )[0].flatten() + conv_act = utils.convolve_1d_trials(coupling_basis_matrix, activity[None])[ + 0 + ].flatten() # Extract the slice of the feedforward input for the current time step input_slice = jax.lax.dynamic_slice( @@ -607,9 +606,7 @@ def scan_fn( # Doesn't use predict because the non-linearity needs # to be applied after we add the feed forward input firing_rate = self._observation_model.inverse_link_function( - w_recurrent.dot(conv_act) - + input_slice - + bs + w_recurrent.dot(conv_act) + input_slice + bs ) # Simulate activity based on the predicted firing rate @@ -621,6 +618,7 @@ def scan_fn( # Increase the t_sample by one carry = jnp.row_stack((activity[1:], new_act)), t_sample + 1 return carry, (new_act, firing_rate) + with jax.disable_jit(True): _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) simulated_activity, firing_rates = outputs diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index beac5544..59e902e5 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -13,24 +13,30 @@ import jaxopt from numpy.typing import NDArray +from . import utils from .base_class import Base from .proximal_operator import prox_group_lasso -from . import utils SolverRunner = Callable[ - [ - Tuple[jnp.ndarray, jnp.ndarray], # Model parameters (for now tuple, eventually pytree) - jnp.ndarray, # Predictors (i.e. model design for GLM) - jnp.ndarray], # Output (neural activity) - jaxopt.OptStep - ] + [ + Tuple[ + jnp.ndarray, jnp.ndarray + ], # Model parameters (for now tuple, eventually pytree) + jnp.ndarray, # Predictors (i.e. model design for GLM) + jnp.ndarray, + ], # Output (neural activity) + jaxopt.OptStep, +] ProximalOperator = Callable[ - [ - Tuple[jnp.ndarray, jnp.ndarray], # Model parameters (for now tuple, eventually pytree) - float, # Regularizer strength (for now float, eventually pytree) - float], # Step-size for optimization (must be a float) - Tuple[jnp.ndarray, jnp.ndarray] + [ + Tuple[ + jnp.ndarray, jnp.ndarray + ], # Model parameters (for now tuple, eventually pytree) + float, # Regularizer strength (for now float, eventually pytree) + float, + ], # Step-size for optimization (must be a float) + Tuple[jnp.ndarray, jnp.ndarray], ] __all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] @@ -56,13 +62,6 @@ class Solver(Base, abc.ABC): Name of the solver being used. solver_kwargs : Additional keyword arguments to be passed to the solver during instantiation. - - Methods - ------- - instantiate_solver(loss) : - Abstract method to instantiate a solver with a given loss function. - get_runner(solver_kwargs, run_kwargs) : - Get the solver runner with provided arguments. """ allowed_algorithms: List[str] = [] @@ -206,11 +205,6 @@ class are defined in the `allowed_optimizers` attribute. allowed_optimizers : list of str List of optimizer names that are allowed for this solver class. - Methods - ------- - instantiate_solver(loss) - Instantiates the optimization algorithm with the given loss function. - See Also -------- [Solver](./#neurostatslib.solver.Solver) : Base solver class from which this class inherits. @@ -286,7 +280,7 @@ def __init__( super().__init__(solver_name, solver_kwargs=solver_kwargs) self.regularizer_strength = regularizer_strength - def penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: + def _penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: """ Compute the Ridge penalization for given parameters. @@ -328,7 +322,7 @@ def instantiate_solver( utils.assert_is_callable(loss, "loss") def penalized_loss(params, X, y): - return loss(params, X, y) + self.penalization(params) + return loss(params, X, y) + self._penalization(params) return self.get_runner(penalized_loss) @@ -450,13 +444,6 @@ class GroupLassoSolver(ProxGradientSolver): Each row represents a group of features. Each column corresponds to a feature, where a value of 1 indicates that the feature belongs to the group, and a value of 0 indicates it doesn't. - - Methods - ------- - _check_mask(): - Validate the mask array to ensure it meets the requirements for Group Lasso regularization. - get_prox_operator(): - Retrieve the proximal operator for Group Lasso regularization. """ def __init__( From dc02446218fed227220d05ecd85ba5bfaf347d83 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 10:52:59 -0500 Subject: [PATCH 168/250] completed first round code revisions --- docs/theory/proximal_operators.md | 60 ++++++++++++++++++++++++++ mkdocs.yml | 2 + src/neurostatslib/glm.py | 3 +- src/neurostatslib/proximal_operator.py | 60 +------------------------- src/neurostatslib/solver.py | 53 ++++++----------------- tests/test_solver.py | 8 ---- 6 files changed, 79 insertions(+), 107 deletions(-) create mode 100644 docs/theory/proximal_operators.md diff --git a/docs/theory/proximal_operators.md b/docs/theory/proximal_operators.md new file mode 100644 index 00000000..a7da8335 --- /dev/null +++ b/docs/theory/proximal_operators.md @@ -0,0 +1,60 @@ +# Proximal Methods in Optimization + +## Introduction + +In optimization theory, the proximal operator is a mathematical tool used to solve non-differentiable optimization +problems or to simplify complex ones. + +The proximal operator of a function $ f: \mathbb{R}^n \rightarrow \mathbb{R} \cup \{+\infty\} $ is defined as follows: + +$$ +\text{prox}_f(v) = \arg\min_x \left( f(x) + \frac{1}{2}\Vert x - v\Vert_2 ^2 \right) +$$ + +Here $ \text{prox}_f(v) $ is the value of $ x $ that minimizes the sum of the function $ f(x) $ and the +squared Euclidean distance between $ x $ and some point $ v $. The parameter $ f $ typically represents +a regularization term or a penalty in the optimization problem, and $ v $ is typically a vector +in the domain of $ f $. + +The proximal operator can be thought of as a generalization of the projection operator. When $ f $ is the +indicator function of a convex set $ C $, then $ \text{prox}_f $ is the projection onto $ C $, since +it finds the point in $ C $ closest to $ v $. + +Proximal operators are central to the implementation of proximal gradient[^1] methods and algorithms like where they +help to break down complex optimization problems into simpler sub-problems that can be solved iteratively. + +## Proximal Operators in Proximal Gradient Algorithms + +Proximal gradient algorithms are designed to solve optimization problems of the form: + +$$ +\min_{x \in \mathbb{R}^n} g(x) + f(x) +$$ + +where $ g $ is a differentiable (and typically convex) function, and $ f $ is a (possibly non-differentiable) convex +function that imposes certain structure or sparsity in the solution. The proximal gradient method updates the +solution iteratively through a two-step process: + +1. **Gradient Step on $ g $**: Take a step towards the direction of the negative gradient of $ g $ at the current +estimate $ x_k $, with a step size $ \alpha_k $, leading to an intermediate estimate $ y_k $: + $$ + y_k = x_k - \alpha_k \nabla g(x_k) + $$ +2. **Proximal Step on $ f $**: Apply the proximal operator of $ f $ to the intermediate +estimate $ y_k $ to obtain the new estimate $ x_{k+1} $: + + $$ + x_{k+1} = \text{prox}_{ f}(y_k) = \arg\min_x \left( f(x) + \frac{1}{2\alpha_k}\Vert x - y_k \Vert_2 ^2 \right) + $$ + +The gradient step aims to reduce the value of the smooth part of the objective $ g $, and the proximal step +takes care of the non-smooth part $ f $, often enforcing properties like sparsity due to regularization terms +such as the $ \ell_1 $ norm. + +By iteratively performing these two steps, the proximal gradient algorithm converges to a solution that +balances minimizing the differentiable part $ g $ while respecting the structure imposed by the non-differentiable +part $ f $. The proximal operator effectively "proximates" the solution at each iteration, +taking into account the influence of the non-smooth term $ f $, which would be otherwise challenging to +handle due to its potential non-differentiability. + +[^1]: Parikh, Neal, and Stephen Boyd. "Proximal Algorithms, ser. Foundations and Trends (r) in Optimization." (2013). diff --git a/mkdocs.yml b/mkdocs.yml index bf0cc086..79df1891 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,5 +49,7 @@ extra_css: nav: - Home: index.md # Link to the index.md file (home page) - Tutorials: generated/gallery # Link to the generated gallery as Tutorials + - Theory: + - Proximal Methods: theory/proximal_operators.md - For Developers: developers_notes/ # Link to the developers notes - Code References: reference/ # Link to the reference/ directory diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index 0eaa98eb..e9e326ef 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -619,7 +619,6 @@ def scan_fn( carry = jnp.row_stack((activity[1:], new_act)), t_sample + 1 return carry, (new_act, firing_rate) - with jax.disable_jit(True): - _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) + _, outputs = jax.lax.scan(scan_fn, (init_y, 0), subkeys) simulated_activity, firing_rates = outputs return simulated_activity, firing_rates diff --git a/src/neurostatslib/proximal_operator.py b/src/neurostatslib/proximal_operator.py index f3bed670..9049ee1c 100644 --- a/src/neurostatslib/proximal_operator.py +++ b/src/neurostatslib/proximal_operator.py @@ -1,63 +1,7 @@ r"""Collection of proximal operators. -## Definition - -In optimization theory, the proximal operator is a mathematical tool used to solve non-differentiable optimization -problems or to simplify complex ones. - -The proximal operator of a function $ f: \mathbb{R}^n \rightarrow \mathbb{R} \cup \{+\infty\} $ is defined as follows: - -$$ -\text{prox}_f(v) = \arg\min_x \left( f(x) + \frac{1}{2}\Vert x - v\Vert_2 ^2 \right) -$$ - -Here $ \text{prox}_f(v) $ is the value of $ x $ that minimizes the sum of the function $ f(x) $ and the -squared Euclidean distance between $ x $ and some point $ v $. The parameter $ f $ typically represents -a regularization term or a penalty in the optimization problem, and $ v $ is typically a vector -in the domain of $ f $. - -The proximal operator can be thought of as a generalization of the projection operator. When $ f $ is the -indicator function of a convex set $ C $, then $ \text{prox}_f $ is the projection onto $ C $, since -it finds the point in $ C $ closest to $ v $. - -Proximal operators are central to the implementation of proximal gradient[^1] methods and algorithms like where they -help to break down complex optimization problems into simpler sub-problems that can be solved iteratively. - -## Proximal Operators in Proximal Gradient Algorithms - -Proximal gradient algorithms are designed to solve optimization problems of the form: - -$$ -\min_{x \in \mathbb{R}^n} g(x) + f(x) -$$ - -where $ g $ is a differentiable (and typically convex) function, and $ f $ is a (possibly non-differentiable) convex -function that imposes certain structure or sparsity in the solution. The proximal gradient method updates the -solution iteratively through a two-step process: - -1. **Gradient Step on $ g $**: Take a step towards the direction of the negative gradient of $ g $ at the current -estimate $ x_k $, with a step size $ \alpha_k $, leading to an intermediate estimate $ y_k $: - $$ - y_k = x_k - \alpha_k \nabla g(x_k) - $$ -2. **Proximal Step on $ f $**: Apply the proximal operator of $ f $ to the intermediate -estimate $ y_k $ to obtain the new estimate $ x_{k+1} $: - - $$ - x_{k+1} = \text{prox}_{ f}(y_k) = \arg\min_x \left( f(x) + \frac{1}{2\alpha_k}\Vert x - y_k \Vert_2 ^2 \right) - $$ - -The gradient step aims to reduce the value of the smooth part of the objective $ g $, and the proximal step -takes care of the non-smooth part $ f $, often enforcing properties like sparsity due to regularization terms -such as the $ \ell_1 $ norm. - -By iteratively performing these two steps, the proximal gradient algorithm converges to a solution that -balances minimizing the differentiable part $ g $ while respecting the structure imposed by the non-differentiable -part $ f $. The proximal operator effectively "proximates" the solution at each iteration, -taking into account the influence of the non-smooth term $ f $, which would be otherwise challenging to -handle due to its potential non-differentiability. - -[^1]: Parikh, Neal, and Stephen Boyd. "Proximal Algorithms, ser. Foundations and Trends (r) in Optimization." (2013). +See the theory note on "Proximal Methods" in the package documentation for an introduction to proximal +operators and the Proximal Gradient algorithm. """ from typing import Tuple diff --git a/src/neurostatslib/solver.py b/src/neurostatslib/solver.py index 59e902e5..07260af5 100644 --- a/src/neurostatslib/solver.py +++ b/src/neurostatslib/solver.py @@ -144,13 +144,13 @@ def _check_solver_kwargs(solver_name, solver_kwargs): f"kwargs {undefined_kwargs} in solver_kwargs not a kwarg for jaxopt.{solver_name}!" ) - @abc.abstractmethod def instantiate_solver( - self, - loss: Callable, + self, loss: Callable, *args: Any, **kwargs: Any ) -> SolverRunner: """Abstract method to instantiate a solver with a given loss function.""" - pass + # check that the loss is Callable + utils.assert_is_callable(loss, "loss") + return self.get_runner(loss, *args, **kwargs) def get_runner( self, @@ -225,28 +225,6 @@ def __init__( ): super().__init__(solver_name, solver_kwargs=solver_kwargs) - def instantiate_solver( - self, - loss: Callable, - ) -> SolverRunner: - """ - Instantiate the optimization algorithm for a given loss function. - - Parameters - ---------- - loss : - The loss function that needs to be minimized. - - Returns - ------- - : - A runner function that uses the specified optimization algorithm - to minimize the given loss function. - """ - # check that the loss is Callable - utils.assert_is_callable(loss, "loss") - return self.get_runner(loss) - class RidgeSolver(Solver): """ @@ -302,8 +280,7 @@ def _penalization(self, params: Tuple[jnp.ndarray, jnp.ndarray]) -> jnp.ndarray: ) def instantiate_solver( - self, - loss: Callable, + self, loss: Callable, *args: Any, **kwargs: Any ) -> SolverRunner: """ Instantiate the solver with a penalized loss function. @@ -318,7 +295,8 @@ def instantiate_solver( Callable A function that runs the solver with the penalized loss. """ - # check that the loss is Callable + # this check has be performed here because the penalized loss will + # always be a callable independently of which loss is passed! utils.assert_is_callable(loss, "loss") def penalized_loss(params, X, y): @@ -350,14 +328,14 @@ def __init__( **kwargs, ): if solver_kwargs is None: - solver_kwargs = dict(prox=self.get_prox_operator()) + solver_kwargs = dict(prox=self._get_proximal_operator()) else: - solver_kwargs["prox"] = self.get_prox_operator() + solver_kwargs["prox"] = self._get_proximal_operator() super().__init__(solver_name, solver_kwargs=solver_kwargs) self.regularizer_strength = regularizer_strength @abc.abstractmethod - def get_prox_operator( + def _get_proximal_operator( self, ) -> ProximalOperator: """ @@ -371,8 +349,7 @@ def get_prox_operator( pass def instantiate_solver( - self, - loss: Callable, + self, loss: Callable, *args: Any, **kwargs: Any ) -> SolverRunner: """ Instantiate the solver with the provided loss function and proximal operator. @@ -387,9 +364,7 @@ def instantiate_solver( : A function that runs the solver with the provided loss and proximal operator. """ - # check that the loss is Callable - utils.assert_is_callable(loss, "loss") - return self.get_runner(loss, self.regularizer_strength) + return super().instantiate_solver(loss, self.regularizer_strength) class LassoSolver(ProxGradientSolver): @@ -409,7 +384,7 @@ def __init__( super().__init__(solver_name, solver_kwargs=solver_kwargs) self.regularizer_strength = regularizer_strength - def get_prox_operator( + def _get_proximal_operator( self, ) -> ProximalOperator: """ @@ -513,7 +488,7 @@ def _check_mask(mask: jnp.ndarray): f"Data type {mask.dtype} provided instead!" ) - def get_prox_operator( + def _get_proximal_operator( self, ) -> ProximalOperator: """ diff --git a/tests/test_solver.py b/tests/test_solver.py index 1e03cd04..0bc59502 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -8,14 +8,6 @@ import neurostatslib as nsl -class TestSolver: - cls = nsl.solver.Solver - def test_abstract_nature_of_solver(self): - """Test that Solver can't be instantiated.""" - with pytest.raises(TypeError, match="Can't instantiate abstract class Solver"): - self.cls("GradientDescent") - - class TestUnRegularizedSolver: cls = nsl.solver.UnRegularizedSolver From c838e49722b683e8a6ec8724c00e89bf5b7f5f22 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 11:24:12 -0500 Subject: [PATCH 169/250] refractor parameter names --- docs/developers_notes/02-base_class.md | 4 +- docs/developers_notes/05-glm.md | 4 +- docs/examples/coupled_neurons_params.json | 4 +- docs/examples/plot_glm_demo.py | 20 +++---- src/neurostatslib/glm.py | 43 +++++++------- tests/conftest.py | 4 +- tests/simulate_coupled_neurons_params.json | 2 +- tests/test_glm.py | 66 +++++++++++----------- 8 files changed, 74 insertions(+), 73 deletions(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 3fedf458..51e2a3de 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -23,7 +23,7 @@ Abstract Class Base │ │ │ ... │ -├─ Abstract Subclass Sovler +├─ Abstract Subclass Solver │ │ │ ├─ Concrete Subclass UnRegularizedSolver │ │ @@ -47,7 +47,7 @@ Abstract Class Base ## The Class `model_base.Base` -The `Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. dditionally, the class provides auxiliary helper methods to identify available computational devices (such as GPUs and TPUs) and to facilitate data transfer to these devices. +The `Base` class aligns with the `scikit-learn` API for `base.BaseEstimator`. This alignment is achieved by implementing the `get_params` and `set_params` methods, essential for `scikit-learn` compatibility and foundational for all model implementations. Additionally, the class provides auxiliary helper methods to identify available computational devices (such as GPUs and TPUs) and to facilitate data transfer to these devices. For a detailed understanding, consult the [`scikit-learn` API Reference](https://scikit-learn.org/stable/modules/classes.html) and [`BaseEstimator`](https://scikit-learn.org/stable/modules/generated/sklearn.base.BaseEstimator.html). diff --git a/docs/developers_notes/05-glm.md b/docs/developers_notes/05-glm.md index 2b66c601..d04061e2 100644 --- a/docs/developers_notes/05-glm.md +++ b/docs/developers_notes/05-glm.md @@ -37,8 +37,8 @@ The `GLM` class provides a direct implementation of the GLM model and is designe - **`solver`**: Refers to the optimization solver - an object of the [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. - **`observation_models`**: Represents the GLM observation model, which is an object of the [`neurostatslib.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. -- **`basis_coeff_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. -- **`baseline_link_fr_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. +- **`coef_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. +- **`intercept_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`solver_state`**: Indicates the solver's state. For specific solver states, refer to the [`jaxopt` documentation](https://jaxopt.github.io/stable/index.html#). ### Public Methods diff --git a/docs/examples/coupled_neurons_params.json b/docs/examples/coupled_neurons_params.json index fef81ce4..a0f73586 100644 --- a/docs/examples/coupled_neurons_params.json +++ b/docs/examples/coupled_neurons_params.json @@ -1,9 +1,9 @@ { - "baseline_link_fr_": [ + "intercept_": [ -4.0, -4.0 ], - "basis_coeff_": [ + "coef_": [ [ -0.004372, -0.02786, diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index f7b7a4cd..120d69cc 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -57,7 +57,7 @@ # `neurostatslib.observation_models.Observations`. So far, only the `PoissonObservations` # model has been implemented. # - **Solver**: The desired solver, e.g. an object of the `neurostatslib.solver.Solver` class. -# Currently, we implemented the un-regulrized, Ridge, Lasso, and Group-Lasso solver. +# Currently, we implemented the un-regularized, Ridge, Lasso, and Group-Lasso solver. # # The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge solver. # Here is how to define the model. @@ -142,7 +142,7 @@ print("Ridge results") print("True weights: ", w_true) -print("Recovered weights: ", model.basis_coeff_) +print("Recovered weights: ", model.coef_) # %% # ## K-fold Cross Validation with `sklearn` @@ -161,7 +161,7 @@ print("Ridge results ") print("Best hyperparameter: ", cls.best_params_) print("True weights: ", w_true) -print("Recovered weights: ", cls.best_estimator_.basis_coeff_) +print("Recovered weights: ", cls.best_estimator_.coef_) # %% # We can compare the Ridge cross-validated results with other solvers. @@ -175,7 +175,7 @@ print("Lasso results ") print("Best hyperparameter: ", cls.best_params_) print("True weights: ", w_true) -print("Recovered weights: ", cls.best_estimator_.basis_coeff_) +print("Recovered weights: ", cls.best_estimator_.coef_) # %% # **Group Lasso** @@ -195,7 +195,7 @@ print(mask) print("Best hyperparameter: ", cls.best_params_) print("True weights: ", w_true) -print("Recovered weights: ", cls.best_estimator_.basis_coeff_) +print("Recovered weights: ", cls.best_estimator_.coef_) # %% # ## Simulate Spikes @@ -228,12 +228,12 @@ # basis weights & intercept for the GLM (both coupling and feedforward) # (the last coefficient is the weight of the feedforward input) -basis_coeff = np.asarray(config_dict["basis_coeff_"])[:, :-1] +basis_coeff = np.asarray(config_dict["coef_"])[:, :-1] # Mask the weights so that only the first neuron receives the imput basis_coeff[:, 40:] = np.abs(basis_coeff[:, 40:]) * np.array([[1.], [0.]]) -baseline_log_fr = np.asarray(config_dict["baseline_link_fr_"]) +intercept = np.asarray(config_dict["intercept_"]) # basis function, inputs and initial spikes coupling_basis = jax.numpy.asarray(config_dict["coupling_basis"]) @@ -263,8 +263,8 @@ # We can now simulate spikes by calling the `simulate_recurrent` method. model = nsl.glm.GLMRecurrent() -model.basis_coeff_ = jax.numpy.asarray(basis_coeff) -model.baseline_link_fr_ = jax.numpy.asarray(baseline_log_fr) +model.coef_ = jax.numpy.asarray(basis_coeff) +model.intercept_ = jax.numpy.asarray(intercept) # call simulate, with both the recurrent coupling @@ -293,7 +293,7 @@ plt.vlines(np.where(spikes[:, 0])[0], 0.00, 0.01, color=p0.get_color(), label="neu 0") plt.vlines(np.where(spikes[:, 1])[0], -0.01, 0.00, color=p1.get_color(), label="neu 1") -plt.plot(np.exp(basis_coeff[0, -1] * feedforward_input[:, 0, 0] + baseline_log_fr[0]), color='k', lw=0.8, label="stimulus") +plt.plot(np.exp(basis_coeff[0, -1] * feedforward_input[:, 0, 0] + intercept[0]), color='k', lw=0.8, label="stimulus") ax.add_patch(patch) plt.ylim(-0.011, .13) plt.ylabel("count/bin") diff --git a/src/neurostatslib/glm.py b/src/neurostatslib/glm.py index e9e326ef..2eaea0bb 100644 --- a/src/neurostatslib/glm.py +++ b/src/neurostatslib/glm.py @@ -32,9 +32,10 @@ class GLM(BaseRegressor): Attributes ---------- - baseline_link_fr_ : - Model baseline link firing rate parameters. - basis_coeff_ : + intercept_ : + Model baseline linked firing rate parameters, e.g. if the link is the logarithm, the baseline + firing rate will be `jnp.exp(model.intercept_)`. + coef_ : Basis coefficients for the model. solver_state : State of the solver after fitting. May include details like optimization error. @@ -57,8 +58,8 @@ def __init__( self.solver = solver # initialize to None fit output - self.baseline_link_fr_ = None - self.basis_coeff_ = None + self.intercept_ = None + self.coef_ = None self.solver_state = None @property @@ -94,7 +95,7 @@ def observation_model(self, observation: obs.Observations): def _check_is_fit(self): """Ensure the instance has been fitted.""" - if (self.basis_coeff_ is None) or (self.baseline_link_fr_ is None): + if (self.coef_ is None) or (self.intercept_ is None): raise NotFittedError( "This GLM instance is not fitted yet. Call 'fit' with appropriate arguments." ) @@ -165,8 +166,8 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: # check that the model is fitted self._check_is_fit() # extract model params - Ws = self.basis_coeff_ - bs = self.baseline_link_fr_ + Ws = self.coef_ + bs = self.intercept_ (X,) = utils.convert_to_jnp_ndarray(X) @@ -241,7 +242,7 @@ def score( ValueError If attempting to simulate a different number of neurons than were present during fitting (i.e., if ``init_y.shape[0] != - self.baseline_link_fr_.shape[0]``). + self.intercept_.shape[0]``). Notes ----- @@ -300,8 +301,8 @@ def score( """ self._check_is_fit() - Ws = self.basis_coeff_ - bs = self.baseline_link_fr_ + Ws = self.coef_ + bs = self.intercept_ X, y = utils.convert_to_jnp_ndarray(X, y) @@ -330,7 +331,7 @@ def fit( """Fit GLM to neural activity. Fit and store the model parameters as attributes - ``basis_coeff_`` and ``baseline_link_fr``. + ``coef_`` and ``coef_``. Parameters ---------- @@ -373,8 +374,8 @@ def fit( ) # Store parameters - self.basis_coeff_: jnp.ndarray = params[0] - self.baseline_link_fr_: jnp.ndarray = params[1] + self.coef_: jnp.ndarray = params[0] + self.intercept_: jnp.ndarray = params[1] # note that this will include an error value, which is not the same as # the output of loss. I believe it's the output of # solver.l2_optimality_error @@ -421,7 +422,7 @@ def simulate( """ # check if the model is fit self._check_is_fit() - Ws, bs = self.basis_coeff_, self.baseline_link_fr_ + Ws, bs = self.coef_, self.intercept_ (feedforward_input,) = self._preprocess_simulate( feedforward_input, params_feedforward=(Ws, bs) ) @@ -527,12 +528,12 @@ def simulate_recurrent( Notes ----- - The model coefficients (`self.basis_coeff_`) are structured such that the first set of coefficients + The model coefficients (`self.coef_`) are structured such that the first set of coefficients (of size `n_basis_coupling * n_neurons`) are interpreted as the weights for the recurrent couplings. The remaining coefficients correspond to the weights for the feed-forward input. - The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.basis_coeff_.shape[1]` + The sum of `n_basis_input` and `n_basis_coupling * n_neurons` should equal `self.coef_.shape[1]` to ensure consistency in the model's input feature dimensionality. """ # check if the model is fit @@ -544,11 +545,11 @@ def simulate_recurrent( ) n_basis_coupling = coupling_basis_matrix.shape[1] - n_neurons = self.baseline_link_fr_.shape[0] + n_neurons = self.intercept_.shape[0] - w_feedforward = self.basis_coeff_[:, n_basis_coupling * n_neurons :] - w_recurrent = self.basis_coeff_[:, : n_basis_coupling * n_neurons] - bs = self.baseline_link_fr_ + w_feedforward = self.coef_[:, n_basis_coupling * n_neurons:] + w_recurrent = self.coef_[:, : n_basis_coupling * n_neurons] + bs = self.intercept_ feedforward_input, init_y = self._preprocess_simulate( feedforward_input, diff --git a/tests/conftest.py b/tests/conftest.py index 39509a0d..0a60dcc0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -71,8 +71,8 @@ def poissonGLM_coupled_model_config_simulate(): observations = nsl.observation_models.PoissonObservations(jnp.exp) solver = nsl.solver.RidgeSolver("BFGS", regularizer_strength=0.1) model = nsl.glm.GLMRecurrent(observation_model=observations, solver=solver) - model.basis_coeff_ = jnp.asarray(config_dict["basis_coeff_"]) - model.baseline_link_fr_ = jnp.asarray(config_dict["baseline_link_fr_"]) + model.coef_ = jnp.asarray(config_dict["coef_"]) + model.intercept_ = jnp.asarray(config_dict["intercept_"]) coupling_basis = jnp.asarray(config_dict["coupling_basis"]) feedforward_input = jnp.asarray(config_dict["feedforward_input"]) init_spikes = jnp.asarray(config_dict["init_spikes"]) diff --git a/tests/simulate_coupled_neurons_params.json b/tests/simulate_coupled_neurons_params.json index a52460e1..a8465f12 100644 --- a/tests/simulate_coupled_neurons_params.json +++ b/tests/simulate_coupled_neurons_params.json @@ -1 +1 @@ -{"baseline_link_fr_": [-3.0, -3.0], "basis_coeff_": [[-0.004372, -0.02786, -0.04582, -0.0588, -0.06539, -0.06396, -0.05328, -0.03192, 0.0002296, 0.04143, 0.08794, 0.1483, 0.2053, 0.2483, 0.2892, 0.3093, 0.2917, 0.2225, 0.07357, -0.2711, -0.006235, -0.01047, 0.02189, 0.058, 0.09002, 0.1118, 0.1209, 0.1167, 0.09909, 0.07044, 0.03448, -0.01565, -0.06823, -0.1128, -0.1655, -0.2176, -0.2621, -0.2982, -0.3255, -0.3449, 0.5, 0.5], [-0.004637, 0.02223, 0.07071, 0.09572, 0.1012, 0.08923, 0.06464, 0.03076, -0.007911, -0.04737, -0.08429, -0.1249, -0.1582, -0.1827, -0.2081, -0.23, -0.2473, -0.2616, -0.2741, -0.287, 0.01127, 0.04864, 0.0544, 0.05082, 0.03975, 0.02393, 0.004725, -0.01763, -0.04202, -0.06744, -0.09269, -0.1231, -0.1522, -0.1763, -0.2051, -0.2348, -0.2629, -0.2896, -0.3149, -0.3389, 0.5, 0.5]], "coupling_basis": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0024979173609873673, 0.9975020826390129], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.11451325277931029, 0.8854867472206909, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25013898844998006, 0.7498610115500185, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3122501403134024, 0.687749859686596, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.28176761370807446, 0.7182323862919272, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17383844924397923, 0.8261615507560222, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04364762794083282, 0.9563523720591665, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9912618171282106, 0.008738182871789013, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7892946476427273, 0.21070535235727128, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3531647741677867, 0.6468352258322151, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.011883820048045501, 0.9881161799519544, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7841665801263835, 0.21583341987361648, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17688067665784446, 0.8231193233421555, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9253003862638604, 0.0746996137361397, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2549435480705588, 0.7450564519294413, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9205258993369989, 0.07947410066300109, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16827351931758228, 0.8317264806824178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7835282009408713, 0.21647179905912872, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.019118847416525586, 0.9808811525834744, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.4372031242218587, 0.5627968757781414, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.9120243919870162, 0.08797560801298382, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.044222034278324274, 0.9557779657216758, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.40793669708774605, 0.5920633029122541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.8283923698925478, 0.17160763010745222, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.9999802058373224, 1.9794162677666538e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.1458111022283093, 0.8541888977716907, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.4778824971400245, 0.5221175028599756, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.803486827077907, 0.19651317292209308, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.9824675828481839, 0.017532417151816082, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.029720664099906924, 0.9702793359000932, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.19724020774947038, 0.8027597922505296, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.44389603578613035, 0.5561039642138698, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.6909694421867117, 0.30903055781328825, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.8804498633788072, 0.1195501366211929, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.9828262050955638, 0.017173794904436157, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.005816278861877466, 0.9941837211381226, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.07171948190677246, 0.9282805180932275, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.19211081158089233, 0.8078891884191077, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.3422365913893123, 0.6577634086106878, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.49997219806462273, 0.5000278019353773, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.6481581380891199, 0.3518418619108801, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.775227808426499, 0.22477219157350103, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.8747644272334134, 0.12523557276658664, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9445228823471115, 0.05547711765288865, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9852942394771702, 0.014705760522829736, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9998405276097415, 0.00015947239025848603, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.00798856965539202, 0.9920114303446079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.03392307742054024, 0.9660769225794598, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.07373523476821137, 0.9262647652317886, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.12352988337197751, 0.8764701166280225, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.17990211564285485, 0.8200978843571451, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.2399997347398921, 0.7600002652601079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.3015222924967669, 0.6984777075032332, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.36268149196393995, 0.63731850803606, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.42214108290743424, 0.5778589170925659, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.47894873221112266, 0.5210512677888774, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5324679173051469, 0.46753208269485313, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5823146093533313, 0.4176853906466687, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.6283012081735033, 0.3716987918264968, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.6703886551778314, 0.32961134482216864, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7086466881407022, 0.2913533118592979, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7432216468423799, 0.25677835315762026, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7743109612271127, 0.22568903877288732, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.802143356101582, 0.197856643898418, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.82696381862707, 0.17303618137292998, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8490224486822571, 0.15097755131774288, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8685664156253453, 0.13143358437465474, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8858343578296817, 0.11416564217031833, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9010526715389762, 0.09894732846102389, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9144332365128198, 0.08556676348718023, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9261722145965264, 0.07382778540347357, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9364496329422705, 0.06355036705772948, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9454295266061546, 0.05457047339384541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9532604668007324, 0.04673953319926766, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9600763426393057, 0.039923657360694254, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9659972972699125, 0.03400270273008754, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.971130745291511, 0.028869254708488945, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.975572418558468, 0.024427581441531954, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9794074030288873, 0.020592596971112653, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9827111411428311, 0.017288858857168965, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9855503831123861, 0.014449616887613925, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9879840771076767, 0.012015922892323394, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9900641931482845, 0.009935806851715523, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9918364789707291, 0.008163521029270815, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9933411485659462, 0.006658851434053759, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9946135057219054, 0.005386494278094567, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9956845059646938, 0.004315494035306178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9965812609202838, 0.0034187390797163486, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.997327489436671, 0.002672510563328956, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9979439199017871, 0.002056080098212898, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9984486481342357, 0.0015513518657642722, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9988574550621354, 0.0011425449378646424, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9991840881776304, 0.0008159118223696749, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.999440510488429, 0.0005594895115710874, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9996371204027914, 0.00036287959720865404, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.999782945694725, 0.00021705430527496627, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9998858144113889, 0.00011418558861114869, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9999525053112863, 4.7494688713622946e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.99998888016377, 1.1119836230089053e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], "feedforward_input": [[[0.0, 1.0], [0.0, 1.0]], [[0.012578617838741058, 0.9999208860571255], [0.012578617838741058, 0.9999208860571255]], [[0.025155245389375847, 0.9996835567465339], [0.025155245389375847, 0.9996835567465339]], [[0.03772789267871718, 0.99928804962034], [0.03772789267871718, 0.99928804962034]], [[0.05029457036336618, 0.9987344272588006], [0.05029457036336618, 0.9987344272588006]], [[0.06285329004448194, 0.9980227772604111], [0.06285329004448194, 0.9980227772604111]], [[0.07540206458240159, 0.9971532122280464], [0.07540206458240159, 0.9971532122280464]], [[0.08793890841106125, 0.9961258697511429], [0.08793890841106125, 0.9961258697511429]], [[0.10046183785216795, 0.9949409123839288], [0.10046183785216795, 0.9949409123839288]], [[0.11296887142907283, 0.9935985276197029], [0.11296887142907283, 0.9935985276197029]], [[0.12545803018029603, 0.9920989278611685], [0.12545803018029603, 0.9920989278611685]], [[0.13792733797265358, 0.9904423503868246], [0.13792733797265358, 0.9904423503868246]], [[0.1503748218139367, 0.9886290573134227], [0.1503748218139367, 0.9886290573134227]], [[0.1627985121650943, 0.986659335554492], [0.1627985121650943, 0.986659335554492]], [[0.17519644325186898, 0.984533496774942], [0.17519644325186898, 0.984533496774942]], [[0.18756665337583714, 0.9822518773417481], [0.18756665337583714, 0.9822518773417481]], [[0.19990718522480458, 0.9798148382707295], [0.19990718522480458, 0.9798148382707295]], [[0.21221608618250787, 0.9772227651694256], [0.21221608618250787, 0.9772227651694256]], [[0.22449140863757258, 0.9744760681760832], [0.22449140863757258, 0.9744760681760832]], [[0.23673121029167973, 0.9715751818947602], [0.23673121029167973, 0.9715751818947602]], [[0.2489335544668916, 0.9685205653265598], [0.2489335544668916, 0.9685205653265598]], [[0.2610965104120882, 0.9653127017970033], [0.2610965104120882, 0.9653127017970033]], [[0.27321815360846585, 0.9619520988795548], [0.27321815360846585, 0.9619520988795548]], [[0.28529656607404974, 0.9584392883153087], [0.28529656607404974, 0.9584392883153087]], [[0.2973298366671723, 0.9547748259288535], [0.2973298366671723, 0.9547748259288535]], [[0.30931606138886886, 0.9509592915403253], [0.30931606138886886, 0.9509592915403253]], [[0.32125334368414366, 0.9469932888736633], [0.32125334368414366, 0.9469932888736633]], [[0.33313979474205757, 0.9428774454610842], [0.33313979474205757, 0.9428774454610842]], [[0.34497353379459045, 0.9386124125437894], [0.34497353379459045, 0.9386124125437894]], [[0.3567526884142317, 0.9341988649689198], [0.3567526884142317, 0.9341988649689198]], [[0.3684753948102499, 0.9296375010827771], [0.3684753948102499, 0.9296375010827771]], [[0.38013979812359666, 0.924929042620325], [0.38013979812359666, 0.924929042620325]], [[0.3917440527203973, 0.9200742345909914], [0.3917440527203973, 0.9200742345909914]], [[0.4032863224839812, 0.915073845160786], [0.4032863224839812, 0.915073845160786]], [[0.41476478110540693, 0.9099286655307568], [0.41476478110540693, 0.9099286655307568]], [[0.4261776123724353, 0.9046395098117981], [0.4261776123724353, 0.9046395098117981]], [[0.4375230104569043, 0.8992072148958368], [0.4375230104569043, 0.8992072148958368]], [[0.4487991802004621, 0.8936326403234123], [0.4487991802004621, 0.8936326403234123]], [[0.46000433739861096, 0.887916668147673], [0.46000433739861096, 0.887916668147673]], [[0.47113670908301786, 0.8820602027948115], [0.47113670908301786, 0.8820602027948115]], [[0.4821945338020477, 0.8760641709209582], [0.4821945338020477, 0.8760641709209582]], [[0.4931760618994744, 0.8699295212655597], [0.4931760618994744, 0.8699295212655597]], [[0.5040795557913246, 0.8636572245012607], [0.5040795557913246, 0.8636572245012607]], [[0.5149032902408126, 0.8572482730803168], [0.5149032902408126, 0.8572482730803168]], [[0.5256455526313207, 0.8507036810775614], [0.5256455526313207, 0.8507036810775614]], [[0.5363046432373825, 0.8440244840299503], [0.5363046432373825, 0.8440244840299503]], [[0.5468788754936273, 0.8372117387727107], [0.5468788754936273, 0.8372117387727107]], [[0.5573665762616421, 0.8302665232721208], [0.5573665762616421, 0.8302665232721208]], [[0.5677660860947078, 0.8231899364549453], [0.5677660860947078, 0.8231899364549453]], [[0.5780757595003707, 0.8159830980345546], [0.5780757595003707, 0.8159830980345546]], [[0.588293965200805, 0.8086471483337551], [0.588293965200805, 0.8086471483337551]], [[0.5984190863909268, 0.8011832481043575], [0.5984190863909268, 0.8011832481043575]], [[0.608449520994217, 0.7935925783435149], [0.608449520994217, 0.7935925783435149]], [[0.6183836819162153, 0.7858763401068549], [0.6183836819162153, 0.7858763401068549]], [[0.6282199972956423, 0.7780357543184395], [0.6282199972956423, 0.7780357543184395]], [[0.6379569107531118, 0.7700720615775812], [0.6379569107531118, 0.7700720615775812]], [[0.647592881637394, 0.7619865219625451], [0.647592881637394, 0.7619865219625451]], [[0.6571263852691885, 0.7537804148311695], [0.6571263852691885, 0.7537804148311695]], [[0.666555913182372, 0.7454550386184362], [0.666555913182372, 0.7454550386184362]], [[0.675879973362679, 0.7370117106310213], [0.675879973362679, 0.7370117106310213]], [[0.6850970904837809, 0.7284517668388609], [0.6850970904837809, 0.7284517668388609]], [[0.6942058061407225, 0.7197765616637636], [0.6942058061407225, 0.7197765616637636]], [[0.7032046790806838, 0.7109874677651024], [0.7032046790806838, 0.7109874677651024]], [[0.7120922854310254, 0.7020858758226226], [0.7120922854310254, 0.7020858758226226]], [[0.720867218924585, 0.6930731943163971], [0.720867218924585, 0.6930731943163971]], [[0.7295280911221884, 0.6839508493039657], [0.7295280911221884, 0.6839508493039657]], [[0.7380735316323389, 0.6747202841946927], [0.7380735316323389, 0.6747202841946927]], [[0.746502188328052, 0.6653829595213794], [0.746502188328052, 0.6653829595213794]], [[0.7548127275607989, 0.6559403527091677], [0.7548127275607989, 0.6559403527091677]], [[0.7630038343715272, 0.6463939578417693], [0.7630038343715272, 0.6463939578417693]], [[0.7710742126987247, 0.6367452854250606], [0.7710742126987247, 0.6367452854250606]], [[0.7790225855834911, 0.6269958621480786], [0.7790225855834911, 0.6269958621480786]], [[0.7868476953715899, 0.6171472306414553], [0.7868476953715899, 0.6171472306414553]], [[0.7945483039124437, 0.6072009492333317], [0.7945483039124437, 0.6072009492333317]], [[0.8021231927550437, 0.5971585917027863], [0.8021231927550437, 0.5971585917027863]], [[0.809571163340744, 0.5870217470308187], [0.809571163340744, 0.5870217470308187]], [[0.8168910371929053, 0.5767920191489297], [0.8168910371929053, 0.5767920191489297]], [[0.8240816561033644, 0.566471026685334], [0.8240816561033644, 0.566471026685334]], [[0.8311418823156935, 0.5560604027088476], [0.8311418823156935, 0.5560604027088476]], [[0.8380705987052264, 0.545561794470492], [0.8380705987052264, 0.545561794470492]], [[0.8448667089558177, 0.5349768631428518], [0.8448667089558177, 0.5349768631428518]], [[0.8515291377333112, 0.5243072835572319], [0.8515291377333112, 0.5243072835572319]], [[0.8580568308556875, 0.5135547439386516], [0.8580568308556875, 0.5135547439386516]], [[0.8644487554598649, 0.5027209456387218], [0.8644487554598649, 0.5027209456387218]], [[0.8707039001651274, 0.4918076028664418], [0.8707039001651274, 0.4918076028664418]], [[0.8768212752331536, 0.4808164424169648], [0.8768212752331536, 0.4808164424169648]], [[0.8827999127246196, 0.4697492033983709], [0.8827999127246196, 0.4697492033983709]], [[0.8886388666523558, 0.45860763695649104], [0.8886388666523558, 0.45860763695649104]], [[0.8943372131310272, 0.4473935059978269], [0.8943372131310272, 0.4473935059978269]], [[0.8998940505233182, 0.4361085849106111], [0.8998940505233182, 0.4361085849106111]], [[0.9053084995825966, 0.42475465928404793], [0.9053084995825966, 0.42475465928404793]], [[0.9105797035920355, 0.4133335256257842], [0.9105797035920355, 0.4133335256257842]], [[0.9157068285001692, 0.4018469910776512], [0.9157068285001692, 0.4018469910776512]], [[0.920689063052863, 0.3902968731297256], [0.920689063052863, 0.3902968731297256]], [[0.9255256189216778, 0.3786849993327503], [0.9255256189216778, 0.3786849993327503]], [[0.9302157308286042, 0.3670132070089654], [0.9302157308286042, 0.3670132070089654]], [[0.934758656667151, 0.35528334296139374], [0.934758656667151, 0.35528334296139374]], [[0.9391536776197676, 0.34349726318162344], [0.9391536776197676, 0.34349726318162344]], [[0.9434000982715812, 0.3316568325561391], [0.9434000982715812, 0.3316568325561391]], [[0.9474972467204298, 0.31976392457124536], [0.9474972467204298, 0.31976392457124536]], [[0.9514444746831766, 0.30782042101662793], [0.9514444746831766, 0.30782042101662793]], [[0.9552411575982869, 0.2958282116876025], [0.9552411575982869, 0.2958282116876025]], [[0.9588866947246497, 0.28378919408609693], [0.9588866947246497, 0.28378919408609693]], [[0.9623805092366334, 0.27170527312041276], [0.9623805092366334, 0.27170527312041276]], [[0.9657220483153546, 0.25957836080381586], [0.9657220483153546, 0.25957836080381586]], [[0.9689107832361495, 0.24741037595200252], [0.9689107832361495, 0.24741037595200252]], [[0.9719462094522335, 0.23520324387949015], [0.9719462094522335, 0.23520324387949015]], [[0.9748278466745341, 0.2229588960949774], [0.9748278466745341, 0.2229588960949774]], [[0.9775552389476861, 0.21067926999572642], [0.9775552389476861, 0.21067926999572642]], [[0.9801279547221765, 0.19836630856101303], [0.9801279547221765, 0.19836630856101303]], [[0.9825455869226277, 0.18602196004469224], [0.9825455869226277, 0.18602196004469224]], [[0.984807753012208, 0.17364817766693041], [0.984807753012208, 0.17364817766693041]], [[0.98691409505316, 0.16124691930515242], [0.98691409505316, 0.16124691930515242]], [[0.9888642797634357, 0.14882014718424924], [0.9888642797634357, 0.14882014718424924]], [[0.9906579985694317, 0.1363698275661], [0.9906579985694317, 0.1363698275661]], [[0.9922949676548136, 0.12389793043845522], [0.9922949676548136, 0.12389793043845522]], [[0.9937749280054242, 0.11140642920322849], [0.9937749280054242, 0.11140642920322849]], [[0.995097645450266, 0.09889730036424986], [0.995097645450266, 0.09889730036424986]], [[0.9962629106985543, 0.08637252321452853], [0.9962629106985543, 0.08637252321452853]], [[0.9972705393728327, 0.07383407952307214], [0.9972705393728327, 0.07383407952307214]], [[0.9981203720381463, 0.06128395322131638], [0.9981203720381463, 0.06128395322131638]], [[0.9988122742272691, 0.04872413008921228], [0.9988122742272691, 0.04872413008921228]], [[0.9993461364619809, 0.036156597441019206], [0.9993461364619809, 0.036156597441019206]], [[0.9997218742703887, 0.023583343810857166], [0.9997218742703887, 0.023583343810857166]], [[0.9999394282002937, 0.011006358638064812], [0.9999394282002937, 0.011006358638064812]], [[0.9999987638285974, -0.001572368047584414], [0.9999987638285974, -0.001572368047584414]], [[0.9998998717667489, -0.014150845940761853], [0.9998998717667489, -0.014150845940761853]], [[0.9996427676622299, -0.026727084775504745], [0.9996427676622299, -0.026727084775504745]], [[0.9992274921960794, -0.03929909464013115], [0.9992274921960794, -0.03929909464013115]], [[0.9986541110764565, -0.0518648862921008], [0.9986541110764565, -0.0518648862921008]], [[0.9979227150282433, -0.06442247147276806], [0.9979227150282433, -0.06442247147276806]], [[0.9970334197786902, -0.07696986322197923], [0.9970334197786902, -0.07696986322197923]], [[0.9959863660391044, -0.08950507619246638], [0.9959863660391044, -0.08950507619246638]], [[0.9947817194825853, -0.10202612696398403], [0.9947817194825853, -0.10202612696398403]], [[0.9934196707178107, -0.11453103435714077], [0.9934196707178107, -0.11453103435714077]], [[0.991900435258877, -0.12701781974687854], [0.991900435258877, -0.12701781974687854]], [[0.9902242534911986, -0.1394845073755453], [0.9902242534911986, -0.1394845073755453]], [[0.9883913906334728, -0.15192912466551547], [0.9883913906334728, -0.15192912466551547]], [[0.9864021366957146, -0.16434970253130593], [0.9864021366957146, -0.16434970253130593]], [[0.9842568064333687, -0.17674427569114137], [0.9842568064333687, -0.17674427569114137]], [[0.9819557392975067, -0.18911088297791617], [0.9819557392975067, -0.18911088297791617]], [[0.9794992993811165, -0.20144756764950503], [0.9794992993811165, -0.20144756764950503]], [[0.9768878753614926, -0.21375237769837538], [0.9768878753614926, -0.21375237769837538]], [[0.9741218804387363, -0.22602336616044894], [0.9741218804387363, -0.22602336616044894]], [[0.9712017522703763, -0.23825859142316483], [0.9712017522703763, -0.23825859142316483]], [[0.9681279529021188, -0.25045611753269825], [0.9681279529021188, -0.25045611753269825]], [[0.9649009686947391, -0.2626140145002818], [0.9649009686947391, -0.2626140145002818]], [[0.9615213102471255, -0.27473035860758266], [0.9615213102471255, -0.27473035860758266]], [[0.9579895123154889, -0.28680323271109], [0.9579895123154889, -0.28680323271109]], [[0.9543061337287488, -0.29883072654545967], [0.9543061337287488, -0.29883072654545967]], [[0.9504717573001116, -0.310810937025771], [0.9504717573001116, -0.310810937025771]], [[0.9464869897348526, -0.32274196854864906], [0.9464869897348526, -0.32274196854864906]], [[0.9423524615343186, -0.33462193329220136], [0.9423524615343186, -0.33462193329220136]], [[0.9380688268961659, -0.3464489515147234], [0.9380688268961659, -0.3464489515147234]], [[0.9336367636108462, -0.3582211518521272], [0.9336367636108462, -0.3582211518521272]], [[0.9290569729543628, -0.369936671614043], [0.9290569729543628, -0.369936671614043]], [[0.9243301795773085, -0.38159365707854837], [0.9243301795773085, -0.38159365707854837]], [[0.9194571313902055, -0.3931902637854787], [0.9194571313902055, -0.3931902637854787]], [[0.9144385994451658, -0.40472465682827324], [0.9144385994451658, -0.40472465682827324]], [[0.9092753778138886, -0.4161950111443075], [0.9092753778138886, -0.4161950111443075]], [[0.9039682834620162, -0.42759951180366895], [0.9039682834620162, -0.42759951180366895]], [[0.8985181561198674, -0.4389363542963303], [0.8985181561198674, -0.4389363542963303]], [[0.8929258581495686, -0.450203744817673], [0.8929258581495686, -0.450203744817673]], [[0.8871922744086043, -0.46139990055231683], [0.8871922744086043, -0.46139990055231683]], [[0.881318312109807, -0.47252304995621186], [0.881318312109807, -0.47252304995621186]], [[0.8753049006778131, -0.4835714330369443], [0.8753049006778131, -0.4835714330369443]], [[0.869152991601999, -0.4945433016322186], [0.869152991601999, -0.4945433016322186]], [[0.8628635582859312, -0.5054369196864643], [0.8628635582859312, -0.5054369196864643]], [[0.856437595893346, -0.5162505635255284], [0.856437595893346, -0.5162505635255284]], [[0.8498761211906867, -0.5269825221294092], [0.8498761211906867, -0.5269825221294092]], [[0.8431801723862224, -0.5376310974029872], [0.8431801723862224, -0.5376310974029872]], [[0.8363508089657762, -0.5481946044447097], [0.8363508089657762, -0.5481946044447097]], [[0.8293891115250829, -0.5586713718131919], [0.8293891115250829, -0.5586713718131919]], [[0.8222961815988096, -0.5690597417916836], [0.8222961815988096, -0.5690597417916836]], [[0.8150731414862624, -0.5793580706503667], [0.8150731414862624, -0.5793580706503667]], [[0.8077211340738071, -0.5895647289064391], [0.8077211340738071, -0.5895647289064391]], [[0.800241322654032, -0.5996781015819448], [0.800241322654032, -0.5996781015819448]], [[0.7926348907416848, -0.6096965884593069], [0.7926348907416848, -0.6096965884593069]], [[0.7849030418864046, -0.6196186043345285], [0.7849030418864046, -0.6196186043345285]], [[0.7770469994822886, -0.6294425792680156], [0.7770469994822886, -0.6294425792680156]], [[0.769068006574317, -0.6391669588329847], [0.769068006574317, -0.6391669588329847]], [[0.7609673256616678, -0.648790204361417], [0.7609673256616678, -0.648790204361417]], [[0.7527462384979551, -0.6583107931875185], [0.7527462384979551, -0.6583107931875185]], [[0.744406045888419, -0.6677272188886485], [0.744406045888419, -0.6677272188886485]], [[0.7359480674841035, -0.6770379915236763], [0.7359480674841035, -0.6770379915236763]], [[0.7273736415730488, -0.6862416378687335], [0.7273736415730488, -0.6862416378687335]], [[0.7186841248685385, -0.6953367016503177], [0.7186841248685385, -0.6953367016503177]], [[0.7098808922944289, -0.7043217437757161], [0.7098808922944289, -0.7043217437757161]], [[0.7009653367675978, -0.7131953425607098], [0.7009653367675978, -0.7131953425607098]], [[0.6919388689775463, -0.7219560939545244], [0.6919388689775463, -0.7219560939545244]], [[0.6828029171631891, -0.7306026117619886], [0.6828029171631891, -0.7306026117619886]], [[0.673558926886866, -0.739133527862871], [0.673558926886866, -0.739133527862871]], [[0.6642083608056142, -0.7475474924283534], [0.6642083608056142, -0.7475474924283534]], [[0.6547526984397353, -0.7558431741346118], [0.6547526984397353, -0.7558431741346118]], [[0.6451934359386937, -0.764019260373469], [0.6451934359386937, -0.764019260373469]], [[0.6355320858443845, -0.7720744574600859], [0.6355320858443845, -0.7720744574600859]], [[0.6257701768518059, -0.7800074908376582], [0.6257701768518059, -0.7800074908376582]], [[0.6159092535671797, -0.7878171052790867], [0.6159092535671797, -0.7878171052790867]], [[0.6059508762635484, -0.7955020650855897], [0.6059508762635484, -0.7955020650855897]], [[0.5958966206338979, -0.8030611542822255], [0.5958966206338979, -0.8030611542822255]], [[0.5857480775418397, -0.8104931768102919], [0.5857480775418397, -0.8104931768102919]], [[0.5755068527698903, -0.8177969567165775], [0.5755068527698903, -0.8177969567165775]], [[0.5651745667653929, -0.8249713383394301], [0.5651745667653929, -0.8249713383394301]], [[0.5547528543841173, -0.8320151864916135], [0.5547528543841173, -0.8320151864916135]], [[0.5442433646315792, -0.8389273866399272], [0.5442433646315792, -0.8389273866399272]], [[0.5336477604021226, -0.8457068450815559], [0.5336477604021226, -0.8457068450815559]], [[0.5229677182158028, -0.8523524891171238], [0.5229677182158028, -0.8523524891171238]], [[0.5122049279531147, -0.8588632672204258], [0.5122049279531147, -0.8588632672204258]], [[0.5013610925876063, -0.865238149204808], [0.5013610925876063, -0.865238149204808]], [[0.49043792791642066, -0.8714761263861723], [0.49043792791642066, -0.8714761263861723]], [[0.47943716228880995, -0.8775762117425775], [0.47943716228880995, -0.8775762117425775]], [[0.4683605363326608, -0.8835374400704151], [0.4683605363326608, -0.8835374400704151]], [[0.4572098026790794, -0.8893588681371302], [0.4572098026790794, -0.8893588681371302]], [[0.44598672568507636, -0.8950395748304677], [0.44598672568507636, -0.8950395748304677]], [[0.4346930811543961, -0.9005786613042182], [0.4346930811543961, -0.9005786613042182]], [[0.4233306560565345, -0.9059752511204399], [0.4233306560565345, -0.9059752511204399]], [[0.4119012482439928, -0.9112284903881356], [0.4119012482439928, -0.9112284903881356]], [[0.40040666616780407, -0.916337547898363], [0.40040666616780407, -0.916337547898363]], [[0.3888487285913878, -0.9213016152557539], [0.3888487285913878, -0.9213016152557539]], [[0.37722926430277026, -0.9261199070064258], [0.37722926430277026, -0.9261199070064258]], [[0.36555011182521946, -0.9307916607622618], [0.36555011182521946, -0.9307916607622618]], [[0.3538131191263388, -0.9353161373215428], [0.3538131191263388, -0.9353161373215428]], [[0.3420201433256689, -0.9396926207859083], [0.3420201433256689, -0.9396926207859083]], [[0.330173050400837, -0.9439204186736329], [0.330173050400837, -0.9439204186736329]], [[0.3182737148923088, -0.9479988620291954], [0.3182737148923088, -0.9479988620291954]], [[0.3063240196067838, -0.9519273055291264], [0.3063240196067838, -0.9519273055291264]], [[0.29432585531928224, -0.9557051275841167], [0.29432585531928224, -0.9557051275841167]], [[0.2822811204739722, -0.9593317304373701], [0.2822811204739722, -0.9593317304373701]], [[0.27019172088378224, -0.9628065402591843], [0.27019172088378224, -0.9628065402591843]], [[0.25805956942885044, -0.9661290072377479], [0.25805956942885044, -0.9661290072377479]], [[0.24588658575385056, -0.9692986056661355], [0.24588658575385056, -0.9692986056661355]], [[0.23367469596425278, -0.9723148340254889], [0.23367469596425278, -0.9723148340254889]], [[0.22142583232155955, -0.975177215064372], [0.22142583232155955, -0.975177215064372]], [[0.20914193293756786, -0.977885295874285], [0.20914193293756786, -0.977885295874285]], [[0.19682494146770554, -0.9804386479613267], [0.19682494146770554, -0.9804386479613267]], [[0.18447680680349254, -0.9828368673139948], [0.18447680680349254, -0.9828368673139948]], [[0.17209948276416928, -0.9850795744671115], [0.17209948276416928, -0.9850795744671115]], [[0.15969492778754976, -0.9871664145618657], [0.15969492778754976, -0.9871664145618657]], [[0.14726510462014156, -0.9890970574019613], [0.14726510462014156, -0.9890970574019613]], [[0.1348119800065847, -0.9908711975058636], [0.1348119800065847, -0.9908711975058636]], [[0.12233752437845731, -0.992488554155135], [0.12233752437845731, -0.992488554155135]], [[0.1098437115425002, -0.9939488714388522], [0.1098437115425002, -0.9939488714388522]], [[0.09733251836830287, -0.9952519182940991], [0.09733251836830287, -0.9952519182940991]], [[0.0848059244755095, -0.9963974885425265], [0.0848059244755095, -0.9963974885425265]], [[0.07226591192058739, -0.9973854009229761], [0.07226591192058739, -0.9973854009229761]], [[0.05971446488321034, -0.9982154991201608], [0.05971446488321034, -0.9982154991201608]], [[0.04715356935230619, -0.9988876517893978], [0.04715356935230619, -0.9988876517893978]], [[0.034585212811817465, -0.9994017525773913], [0.034585212811817465, -0.9994017525773913]], [[0.022011383926227784, -0.9997577201390606], [0.022011383926227784, -0.9997577201390606]], [[0.009434072225897046, -0.999955498150411], [0.009434072225897046, -0.999955498150411]], [[-0.0031447322077359985, -0.9999950553174459], [-0.0031447322077359985, -0.9999950553174459]], [[-0.015723039057040564, -0.9998763853811183], [-0.015723039057040564, -0.9998763853811183]], [[-0.02829885808311759, -0.9995995071183217], [-0.02829885808311759, -0.9995995071183217]], [[-0.04087019944071145, -0.9991644643389178], [-0.04087019944071145, -0.9991644643389178]], [[-0.053435073993057226, -0.9985713258788059], [-0.053435073993057226, -0.9985713258788059]], [[-0.06599149362662023, -0.9978201855890307], [-0.06599149362662023, -0.9978201855890307]], [[-0.07853747156566927, -0.996911162320932], [-0.07853747156566927, -0.996911162320932]], [[-0.09107102268664041, -0.9958443999073396], [-0.09107102268664041, -0.9958443999073396]], [[-0.10359016383223883, -0.9946200671398149], [-0.10359016383223883, -0.9946200671398149]], [[-0.11609291412522968, -0.993238357741943], [-0.11609291412522968, -0.993238357741943]], [[-0.12857729528186848, -0.9916994903386808], [-0.12857729528186848, -0.9916994903386808]], [[-0.14104133192491908, -0.9900037084217639], [-0.14104133192491908, -0.9900037084217639]], [[-0.15348305189621594, -0.9881512803111796], [-0.15348305189621594, -0.9881512803111796]], [[-0.16590048656871298, -0.9861424991127116], [-0.16590048656871298, -0.9861424991127116]], [[-0.1782916711579755, -0.9839776826715616], [-0.1782916711579755, -0.9839776826715616]], [[-0.19065464503306404, -0.9816571735220583], [-0.19065464503306404, -0.9816571735220583]], [[-0.20298745202676116, -0.979181338833458], [-0.20298745202676116, -0.979181338833458]], [[-0.2152881407450901, -0.9765505703518493], [-0.2152881407450901, -0.9765505703518493]], [[-0.2275547648760821, -0.9737652843381669], [-0.2275547648760821, -0.9737652843381669]], [[-0.23978538349773562, -0.9708259215023277], [-0.23978538349773562, -0.9708259215023277]], [[-0.25197806138512474, -0.967732946933499], [-0.25197806138512474, -0.967732946933499]], [[-0.2641308693166058, -0.9644868500265071], [-0.2641308693166058, -0.9644868500265071]], [[-0.2762418843790738, -0.9610881444044029], [-0.2762418843790738, -0.9610881444044029]], [[-0.2883091902722216, -0.9575373678371909], [-0.2883091902722216, -0.9575373678371909]], [[-0.3003308776117502, -0.9538350821567405], [-0.3003308776117502, -0.9538350821567405]], [[-0.31230504423148914, -0.9499818731678872], [-0.31230504423148914, -0.9499818731678872]], [[-0.32422979548437053, -0.9459783505557425], [-0.32422979548437053, -0.9459783505557425]], [[-0.33610324454221563, -0.9418251477892251], [-0.33610324454221563, -0.9418251477892251]], [[-0.34792351269428334, -0.9375229220208277], [-0.34792351269428334, -0.9375229220208277]], [[-0.3596887296445355, -0.9330723539826374], [-0.3596887296445355, -0.9330723539826374]], [[-0.3713970338075679, -0.9284741478786258], [-0.3713970338075679, -0.9284741478786258]], [[-0.3830465726031674, -0.9237290312732227], [-0.3830465726031674, -0.9237290312732227]], [[-0.3946355027494405, -0.9188377549761962], [-0.3946355027494405, -0.9188377549761962]], [[-0.406161990554472, -0.9138010929238535], [-0.406161990554472, -0.9138010929238535]], [[-0.41762421220646645, -0.9086198420565822], [-0.41762421220646645, -0.9086198420565822]], [[-0.4290203540623263, -0.9032948221927524], [-0.4290203540623263, -0.9032948221927524]], [[-0.44034861293461913, -0.8978268758989992], [-0.44034861293461913, -0.8978268758989992]], [[-0.4516071963768948, -0.892216868356904], [-0.4516071963768948, -0.892216868356904]], [[-0.46279432296729867, -0.8864656872260989], [-0.46279432296729867, -0.8864656872260989]], [[-0.47390822259044274, -0.8805742425038149], [-0.47390822259044274, -0.8805742425038149]], [[-0.4849471367174873, -0.8745434663808944], [-0.4849471367174873, -0.8745434663808944]], [[-0.495909318684389, -0.8683743130942929], [-0.495909318684389, -0.8683743130942929]], [[-0.5067930339682724, -0.8620677587760915], [-0.5067930339682724, -0.8620677587760915]], [[-0.5175965604618782, -0.8556248012990468], [-0.5175965604618782, -0.8556248012990468]], [[-0.5283181887460511, -0.849046460118698], [-0.5283181887460511, -0.849046460118698]], [[-0.538956222360216, -0.842333776112062], [-0.538956222360216, -0.842333776112062]], [[-0.5495089780708056, -0.8354878114129367], [-0.5495089780708056, -0.8354878114129367]], [[-0.5599747861375949, -0.8285096492438424], [-0.5599747861375949, -0.8285096492438424]], [[-0.5703519905779012, -0.8214003937446254], [-0.5703519905779012, -0.8214003937446254]], [[-0.5806389494286053, -0.814161169797753], [-0.5806389494286053, -0.814161169797753]], [[-0.5908340350059578, -0.8067931228503245], [-0.5908340350059578, -0.8067931228503245]], [[-0.6009356341631226, -0.7992974187328304], [-0.6009356341631226, -0.7992974187328304]], [[-0.6109421485454225, -0.7916752434746857], [-0.6109421485454225, -0.7916752434746857]], [[-0.6208519948432432, -0.7839278031165661], [-0.6208519948432432, -0.7839278031165661]], [[-0.630663605042557, -0.7760563235195791], [-0.630663605042557, -0.7760563235195791]], [[-0.6403754266730258, -0.7680620501712998], [-0.6403754266730258, -0.7680620501712998]], [[-0.6499859230536464, -0.7599462479886977], [-0.6499859230536464, -0.7599462479886977]], [[-0.6594935735358957, -0.7517102011179935], [-0.6594935735358957, -0.7517102011179935]], [[-0.6688968737443391, -0.7433552127314704], [-0.6688968737443391, -0.7433552127314704]], [[-0.6781943358146659, -0.7348826048212762], [-0.6781943358146659, -0.7348826048212762]], [[-0.6873844886291098, -0.7262937179902474], [-0.6873844886291098, -0.7262937179902474]], [[-0.6964658780492216, -0.717589911239788], [-0.6964658780492216, -0.717589911239788]], [[-0.7054370671459529, -0.7087725617548385], [-0.7054370671459529, -0.7087725617548385]], [[-0.7142966364270207, -0.6998430646859656], [-0.7142966364270207, -0.6998430646859656]], [[-0.723043184061509, -0.6908028329286112], [-0.723043184061509, -0.6908028329286112]], [[-0.731675326101678, -0.6816532968995332], [-0.731675326101678, -0.6816532968995332]], [[-0.7401916967019432, -0.6723959043104729], [-0.7401916967019432, -0.6723959043104729]], [[-0.7485909483349904, -0.6630321199390868], [-0.7485909483349904, -0.6630321199390868]], [[-0.7568717520049916, -0.6535634253971795], [-0.7568717520049916, -0.6535634253971795]], [[-0.7650327974578898, -0.6439913188962686], [-0.7650327974578898, -0.6439913188962686]], [[-0.7730727933887175, -0.634317315010528], [-0.7730727933887175, -0.634317315010528]], [[-0.7809904676459172, -0.6245429444371393], [-0.7809904676459172, -0.6245429444371393]], [[-0.788784567432631, -0.6146697537540928], [-0.788784567432631, -0.6146697537540928]], [[-0.7964538595049286, -0.6046993051754759], [-0.7964538595049286, -0.6046993051754759]], [[-0.8039971303669401, -0.5946331763042871], [-0.8039971303669401, -0.5946331763042871]], [[-0.8114131864628653, -0.5844729598828156], [-0.8114131864628653, -0.5844729598828156]], [[-0.8187008543658276, -0.5742202635406243], [-0.8187008543658276, -0.5742202635406243]], [[-0.825858980963543, -0.5638767095401779], [-0.825858980963543, -0.5638767095401779]], [[-0.8328864336407734, -0.5534439345201586], [-0.8328864336407734, -0.5534439345201586]], [[-0.8397821004585396, -0.5429235892364995], [-0.8397821004585396, -0.5429235892364995]], [[-0.8465448903300604, -0.5323173383011922], [-0.8465448903300604, -0.5323173383011922]], [[-0.8531737331933926, -0.521626859918898], [-0.8531737331933926, -0.521626859918898]], [[-0.8596675801807451, -0.5108538456214089], [-0.8596675801807451, -0.5108538456214089]], [[-0.8660254037844384, -0.5000000000000004], [-0.8660254037844384, -0.5000000000000004]], [[-0.872246198019486, -0.4890670404357173], [-0.872246198019486, -0.4890670404357173]], [[-0.8783289785827684, -0.4780566968276366], [-0.8783289785827684, -0.4780566968276366]], [[-0.8842727830087774, -0.46697071131914863], [-0.8842727830087774, -0.46697071131914863]], [[-0.8900766708219056, -0.4558108380223019], [-0.8900766708219056, -0.4558108380223019]], [[-0.895739723685255, -0.4445788427402534], [-0.895739723685255, -0.4445788427402534]], [[-0.9012610455459443, -0.4332765026878693], [-0.9012610455459443, -0.4332765026878693]], [[-0.9066397627768893, -0.4219056062105194], [-0.9066397627768893, -0.4219056062105194]], [[-0.9118750243150336, -0.410467952501114], [-0.9118750243150336, -0.410467952501114]], [[-0.9169660017960133, -0.39896535131541655], [-0.9169660017960133, -0.39896535131541655]], [[-0.921911889685225, -0.38739962268569333], [-0.921911889685225, -0.38739962268569333]], [[-0.9267119054052849, -0.37577259663273255], [-0.9267119054052849, -0.37577259663273255]], [[-0.931365289459854, -0.3640861128762842], [-0.931365289459854, -0.3640861128762842]], [[-0.9358713055538119, -0.3523420205439648], [-0.9358713055538119, -0.3523420205439648]], [[-0.9402292407097588, -0.3405421778786742], [-0.9402292407097588, -0.3405421778786742]], [[-0.9444384053808287, -0.32868845194456947], [-0.9444384053808287, -0.32868845194456947]], [[-0.948498133559795, -0.3167827183316434], [-0.948498133559795, -0.3167827183316434]], [[-0.9524077828844512, -0.30482686085895394], [-0.9524077828844512, -0.30482686085895394]], [[-0.9561667347392507, -0.2928227712765512], [-0.9561667347392507, -0.2928227712765512]], [[-0.959774394353189, -0.28077234896614933], [-0.959774394353189, -0.28077234896614933]], [[-0.9632301908939126, -0.26867750064059465], [-0.9632301908939126, -0.26867750064059465]], [[-0.9665335775580413, -0.25654014004216524], [-0.9665335775580413, -0.25654014004216524]], [[-0.9696840316576876, -0.2443621876397672], [-0.9696840316576876, -0.2443621876397672]], [[-0.97268105470316, -0.2321455703250619], [-0.97268105470316, -0.2321455703250619]], [[-0.9755241724818386, -0.21989222110757806], [-0.9755241724818386, -0.21989222110757806]], [[-0.9782129351332083, -0.2076040788088557], [-0.9782129351332083, -0.2076040788088557]], [[-0.9807469172200395, -0.19528308775567055], [-0.9807469172200395, -0.19528308775567055]], [[-0.9831257177957041, -0.18293119747238726], [-0.9831257177957041, -0.18293119747238726]], [[-0.9853489604676163, -0.17055036237249038], [-0.9853489604676163, -0.17055036237249038]], [[-0.9874162934567888, -0.15814254144934156], [-0.9874162934567888, -0.15814254144934156]], [[-0.9893273896534934, -0.14570969796621222], [-0.9893273896534934, -0.14570969796621222]], [[-0.9910819466690195, -0.1332537991456406], [-0.9910819466690195, -0.1332537991456406]], [[-0.9926796868835203, -0.1207768158581612], [-0.9926796868835203, -0.1207768158581612]], [[-0.9941203574899392, -0.10828072231046196], [-0.9941203574899392, -0.10828072231046196]], [[-0.9954037305340125, -0.09576749573300417], [-0.9954037305340125, -0.09576749573300417]], [[-0.9965296029503367, -0.08323911606717305], [-0.9965296029503367, -0.08323911606717305]], [[-0.9974977965944997, -0.070697565651995], [-0.9974977965944997, -0.070697565651995]], [[-0.9983081582712682, -0.05814482891047624], [-0.9983081582712682, -0.05814482891047624]], [[-0.9989605597588274, -0.04558289203561173], [-0.9989605597588274, -0.04558289203561173]], [[-0.9994548978290693, -0.0330137426761141], [-0.9994548978290693, -0.0330137426761141]], [[-0.9997910942639261, -0.020439369621912166], [-0.9997910942639261, -0.020439369621912166]], [[-0.9999690958677468, -0.007861762489468911], [-0.9999690958677468, -0.007861762489468911]], [[-0.999988874475714, 0.004717088593031313], [-0.999988874475714, 0.004717088593031313]], [[-0.9998504269583004, 0.01729519330057657], [-0.9998504269583004, 0.01729519330057657]], [[-0.9995537752217639, 0.029870561426252256], [-0.9995537752217639, 0.029870561426252256]], [[-0.9990989662046815, 0.04244120319614822], [-0.9990989662046815, 0.04244120319614822]], [[-0.9984860718705224, 0.055005129584192916], [-0.9984860718705224, 0.055005129584192916]], [[-0.9977151891962615, 0.06756035262687816], [-0.9977151891962615, 0.06756035262687816]], [[-0.9967864401570343, 0.08010488573780679], [-0.9967864401570343, 0.08010488573780679]], [[-0.9956999717068378, 0.09263674402202696], [-0.9956999717068378, 0.09263674402202696]], [[-0.9944559557552776, 0.10515394459009784], [-0.9944559557552776, 0.10515394459009784]], [[-0.9930545891403677, 0.11765450687183807], [-0.9930545891403677, 0.11765450687183807]], [[-0.9914960935973849, 0.1301364529297071], [-0.9914960935973849, 0.1301364529297071]], [[-0.9897807157237836, 0.1425978077717702], [-0.9897807157237836, 0.1425978077717702]], [[-0.9879087269401782, 0.1550365996641971], [-0.9879087269401782, 0.1550365996641971]], [[-0.9858804234473959, 0.16745086044324545], [-0.9858804234473959, 0.16745086044324545]], [[-0.9836961261796103, 0.17983862582667898], [-0.9836961261796103, 0.17983862582667898]], [[-0.9813561807535597, 0.19219793572457194], [-0.9813561807535597, 0.19219793572457194]], [[-0.9788609574138615, 0.20452683454945075], [-0.9788609574138615, 0.20452683454945075]], [[-0.9762108509744296, 0.21682337152571898], [-0.9762108509744296, 0.21682337152571898]], [[-0.9734062807560028, 0.22908560099832972], [-0.9734062807560028, 0.22908560099832972]], [[-0.9704476905197971, 0.24131158274063894], [-0.9704476905197971, 0.24131158274063894]], [[-0.9673355483972903, 0.25349938226140434], [-0.9673355483972903, 0.25349938226140434]], [[-0.9640703468161508, 0.2656470711108758], [-0.9640703468161508, 0.2656470711108758]], [[-0.9606526024223212, 0.27775272718593], [-0.9606526024223212, 0.27775272718593]], [[-0.957082855998271, 0.28981443503420057], [-0.957082855998271, 0.28981443503420057]], [[-0.9533616723774295, 0.30183028615715607], [-0.9533616723774295, 0.30183028615715607]], [[-0.9494896403548136, 0.31379837931207794], [-0.9494896403548136, 0.31379837931207794]], [[-0.9454673725938637, 0.3257168208128897], [-0.9454673725938637, 0.3257168208128897]], [[-0.9412955055295036, 0.33758372482979143], [-0.9412955055295036, 0.33758372482979143]], [[-0.9369746992674384, 0.34939721368765], [-0.9369746992674384, 0.34939721368765]], [[-0.9325056374797075, 0.361155418163101], [-0.9325056374797075, 0.361155418163101]], [[-0.9278890272965095, 0.3728564777803084], [-0.9278890272965095, 0.3728564777803084]], [[-0.9231255991943125, 0.3844985411053488], [-0.9231255991943125, 0.3844985411053488]], [[-0.9182161068802741, 0.3960797660391565], [-0.9182161068802741, 0.3960797660391565]], [[-0.9131613271729835, 0.4075983201089958], [-0.9131613271729835, 0.4075983201089958]], [[-0.9079620598795464, 0.41905238075840945], [-0.9079620598795464, 0.41905238075840945]], [[-0.9026191276690343, 0.4304401356355976], [-0.9026191276690343, 0.4304401356355976]], [[-0.8971333759423143, 0.4417597828801825], [-0.8971333759423143, 0.4417597828801825]], [[-0.8915056726982842, 0.4530095314083134], [-0.8915056726982842, 0.4530095314083134]], [[-0.8857369083965297, 0.4641876011960654], [-0.8857369083965297, 0.4641876011960654]], [[-0.8798279958164298, 0.4752922235610892], [-0.8798279958164298, 0.4752922235610892]], [[-0.873779869912729, 0.486321641442466], [-0.873779869912729, 0.486321641442466]], [[-0.8675934876676018, 0.49727410967872326], [-0.8675934876676018, 0.49727410967872326]], [[-0.8612698279392309, 0.5081478952839691], [-0.8612698279392309, 0.5081478952839691]], [[-0.8548098913069261, 0.5189412777220956], [-0.8548098913069261, 0.5189412777220956]], [[-0.8482146999128025, 0.5296525491790203], [-0.8482146999128025, 0.5296525491790203]], [[-0.8414852973000504, 0.5402800148329067], [-0.8414852973000504, 0.5402800148329067]], [[-0.8346227482478176, 0.5508219931223336], [-0.8346227482478176, 0.5508219931223336]], [[-0.8276281386027314, 0.5612768160123647], [-0.8276281386027314, 0.5612768160123647]], [[-0.8205025751070878, 0.5716428292584782], [-0.8205025751070878, 0.5716428292584782]], [[-0.8132471852237334, 0.5819183926683146], [-0.8132471852237334, 0.5819183926683146]], [[-0.8058631169576695, 0.5921018803612005], [-0.8058631169576695, 0.5921018803612005]], [[-0.7983515386744064, 0.6021916810254089], [-0.7983515386744064, 0.6021916810254089]], [[-0.7907136389150943, 0.6121861981731129], [-0.7907136389150943, 0.6121861981731129]], [[-0.7829506262084637, 0.6220838503929953], [-0.7829506262084637, 0.6220838503929953]], [[-0.7750637288796017, 0.6318830716004721], [-0.7750637288796017, 0.6318830716004721]], [[-0.7670541948555989, 0.6415823112854881], [-0.7670541948555989, 0.6415823112854881]], [[-0.7589232914680891, 0.6511800347578556], [-0.7589232914680891, 0.6511800347578556]], [[-0.7506723052527245, 0.6606747233900812], [-0.7506723052527245, 0.6606747233900812]], [[-0.7423025417456096, 0.670064874857657], [-0.7423025417456096, 0.670064874857657]], [[-0.7338153252767281, 0.6793490033767694], [-0.7338153252767281, 0.6793490033767694]], [[-0.7252119987603977, 0.6885256399393918], [-0.7252119987603977, 0.6885256399393918]], [[-0.7164939234827836, 0.6975933325457224], [-0.7164939234827836, 0.6975933325457224]], [[-0.7076624788865049, 0.706550646433932], [-0.7076624788865049, 0.706550646433932]], [[-0.698719062352368, 0.7153961643071813], [-0.698719062352368, 0.7153961643071813]], [[-0.6896650889782625, 0.7241284865578796], [-0.6896650889782625, 0.7241284865578796]], [[-0.6805019913552531, 0.7327462314891391], [-0.6805019913552531, 0.7327462314891391]], [[-0.6712312193409035, 0.7412480355333995], [-0.6712312193409035, 0.7412480355333995]], [[-0.6618542398298681, 0.7496325534681825], [-0.6618542398298681, 0.7496325534681825]], [[-0.6523725365217912, 0.7578984586289408], [-0.6523725365217912, 0.7578984586289408]], [[-0.6427876096865396, 0.7660444431189778], [-0.6427876096865396, 0.7660444431189778]], [[-0.6331009759268216, 0.7740692180163904], [-0.6331009759268216, 0.7740692180163904]], [[-0.623314167938217, 0.7819715135780128], [-0.623314167938217, 0.7819715135780128]], [[-0.6134287342666622, 0.7897500794403256], [-0.6134287342666622, 0.7897500794403256]], [[-0.6034462390634266, 0.7974036848172986], [-0.6034462390634266, 0.7974036848172986]], [[-0.5933682618376209, 0.8049311186951345], [-0.5933682618376209, 0.8049311186951345]], [[-0.5831963972062739, 0.8123311900238854], [-0.5831963972062739, 0.8123311900238854]], [[-0.5729322546420206, 0.819602727905911], [-0.5729322546420206, 0.819602727905911]], [[-0.5625774582184379, 0.826744581781146], [-0.5625774582184379, 0.826744581781146]], [[-0.552133646353071, 0.8337556216091511], [-0.552133646353071, 0.8337556216091511]], [[-0.541602471548191, 0.8406347380479176], [-0.541602471548191, 0.8406347380479176]], [[-0.5309856001293205, 0.8473808426293961], [-0.5309856001293205, 0.8473808426293961]], [[-0.5202847119815792, 0.8539928679317206], [-0.5202847119815792, 0.8539928679317206]], [[-0.5095015002838734, 0.8604697677481075], [-0.5095015002838734, 0.8604697677481075]], [[-0.4986376712409919, 0.8668105172523927], [-0.4986376712409919, 0.8668105172523927]], [[-0.487694943813635, 0.8730141131611879], [-0.487694943813635, 0.8730141131611879]], [[-0.47667504944642797, 0.8790795738926286], [-0.47667504944642797, 0.8790795738926286]], [[-0.4655797317939577, 0.8850059397216871], [-0.4655797317939577, 0.8850059397216871]], [[-0.45441074644487806, 0.890792272932028], [-0.45441074644487806, 0.890792272932028]], [[-0.4431698606441268, 0.8964376579643814], [-0.4431698606441268, 0.8964376579643814]], [[-0.4318588530132981, 0.9019412015614092], [-0.4318588530132981, 0.9019412015614092]], [[-0.4204795132692152, 0.907302032909044], [-0.4204795132692152, 0.907302032909044]], [[-0.4090336419407468, 0.9125193037742757], [-0.4090336419407468, 0.9125193037742757]], [[-0.3975230500839139, 0.9175921886393661], [-0.3975230500839139, 0.9175921886393661]], [[-0.38594955899532896, 0.9225198848324686], [-0.38594955899532896, 0.9225198848324686]], [[-0.3743149999240192, 0.9273016126546322], [-0.3743149999240192, 0.9273016126546322]], [[-0.3626212137816673, 0.9319366155031737], [-0.3626212137816673, 0.9319366155031737]], [[-0.35087005085133094, 0.9364241599913922], [-0.35087005085133094, 0.9364241599913922]], [[-0.3390633704946757, 0.9407635360646108], [-0.3390633704946757, 0.9407635360646108]], [[-0.3272030408577722, 0.9449540571125281], [-0.3272030408577722, 0.9449540571125281]], [[-0.3152909385755031, 0.9489950600778585], [-0.3152909385755031, 0.9489950600778585]], [[-0.3033289484746273, 0.9528859055612465], [-0.3033289484746273, 0.9528859055612465]], [[-0.29131896327554796, 0.9566259779224375], [-0.29131896327554796, 0.9566259779224375]], [[-0.2792628832928309, 0.9602146853776892], [-0.2792628832928309, 0.9602146853776892]], [[-0.26716261613452225, 0.9636514600934084], [-0.26716261613452225, 0.9636514600934084]], [[-0.25502007640031144, 0.9669357582759981], [-0.25502007640031144, 0.9669357582759981]], [[-0.24283718537858734, 0.9700670602579007], [-0.24283718537858734, 0.9700670602579007]], [[-0.23061587074244044, 0.9730448705798238], [-0.23061587074244044, 0.9730448705798238]], [[-0.21835806624464577, 0.975868718069136], [-0.21835806624464577, 0.975868718069136]], [[-0.20606571141169297, 0.9785381559144195], [-0.20606571141169297, 0.9785381559144195]], [[-0.19374075123689813, 0.981052761736168], [-0.19374075123689813, 0.981052761736168]], [[-0.18138513587265162, 0.9834121376536186], [-0.18138513587265162, 0.9834121376536186]], [[-0.16900082032184968, 0.9856159103477083], [-0.16900082032184968, 0.9856159103477083]], [[-0.15658976412855838, 0.9876637311201432], [-0.15658976412855838, 0.9876637311201432]], [[-0.14415393106795907, 0.9895552759485718], [-0.14415393106795907, 0.9895552759485718]], [[-0.13169528883562445, 0.9912902455378553], [-0.13169528883562445, 0.9912902455378553]], [[-0.11921580873617425, 0.9928683653674237], [-0.11921580873617425, 0.9928683653674237]], [[-0.10671746537135988, 0.9942893857347128], [-0.10671746537135988, 0.9942893857347128]], [[-0.0942022363276273, 0.9955530817946745], [-0.0942022363276273, 0.9955530817946745]], [[-0.08167210186320688, 0.9966592535953529], [-0.08167210186320688, 0.9966592535953529]], [[-0.06912904459478485, 0.9976077261095226], [-0.06912904459478485, 0.9976077261095226]], [[-0.056575049183792726, 0.998398349262383], [-0.056575049183792726, 0.998398349262383]], [[-0.04401210202238211, 0.9990309979553044], [-0.04401210202238211, 0.9990309979553044]], [[-0.031442190919121114, 0.9995055720856215], [-0.031442190919121114, 0.9995055720856215]], [[-0.018867304784467676, 0.9998219965624732], [-0.018867304784467676, 0.9998219965624732]], [[-0.006289433316068405, 0.9999802213186832], [-0.006289433316068405, 0.9999802213186832]], [[0.006289433316067026, 0.9999802213186832], [0.006289433316067026, 0.9999802213186832]], [[0.0188673047844663, 0.9998219965624732], [0.0188673047844663, 0.9998219965624732]], [[0.03144219091911974, 0.9995055720856215], [0.03144219091911974, 0.9995055720856215]], [[0.04401210202238073, 0.9990309979553045], [0.04401210202238073, 0.9990309979553045]], [[0.056575049183791346, 0.9983983492623831], [0.056575049183791346, 0.9983983492623831]], [[0.06912904459478347, 0.9976077261095226], [0.06912904459478347, 0.9976077261095226]], [[0.08167210186320639, 0.9966592535953529], [0.08167210186320639, 0.9966592535953529]], [[0.09420223632762592, 0.9955530817946746], [0.09420223632762592, 0.9955530817946746]], [[0.10671746537135851, 0.994289385734713], [0.10671746537135851, 0.994289385734713]], [[0.11921580873617288, 0.9928683653674238], [0.11921580873617288, 0.9928683653674238]], [[0.13169528883562306, 0.9912902455378555], [0.13169528883562306, 0.9912902455378555]], [[0.14415393106795768, 0.9895552759485721], [0.14415393106795768, 0.9895552759485721]], [[0.15658976412855702, 0.9876637311201434], [0.15658976412855702, 0.9876637311201434]], [[0.16900082032184832, 0.9856159103477086], [0.16900082032184832, 0.9856159103477086]], [[0.18138513587265026, 0.9834121376536189], [0.18138513587265026, 0.9834121376536189]], [[0.19374075123689677, 0.9810527617361683], [0.19374075123689677, 0.9810527617361683]], [[0.2060657114116916, 0.9785381559144198], [0.2060657114116916, 0.9785381559144198]], [[0.21835806624464443, 0.9758687180691363], [0.21835806624464443, 0.9758687180691363]], [[0.2306158707424391, 0.9730448705798241], [0.2306158707424391, 0.9730448705798241]], [[0.24283718537858687, 0.9700670602579009], [0.24283718537858687, 0.9700670602579009]], [[0.2550200764003101, 0.9669357582759984], [0.2550200764003101, 0.9669357582759984]], [[0.2671626161345209, 0.9636514600934087], [0.2671626161345209, 0.9636514600934087]], [[0.2792628832928296, 0.9602146853776896], [0.2792628832928296, 0.9602146853776896]], [[0.2913189632755466, 0.956625977922438], [0.2913189632755466, 0.956625977922438]], [[0.30332894847462605, 0.952885905561247], [0.30332894847462605, 0.952885905561247]], [[0.3152909385755018, 0.9489950600778589], [0.3152909385755018, 0.9489950600778589]], [[0.3272030408577709, 0.9449540571125286], [0.3272030408577709, 0.9449540571125286]], [[0.33906337049467444, 0.9407635360646113], [0.33906337049467444, 0.9407635360646113]], [[0.3508700508513296, 0.9364241599913926], [0.3508700508513296, 0.9364241599913926]], [[0.36262121378166595, 0.9319366155031743], [0.36262121378166595, 0.9319366155031743]], [[0.3743149999240179, 0.9273016126546327], [0.3743149999240179, 0.9273016126546327]], [[0.3859495589953277, 0.9225198848324692], [0.3859495589953277, 0.9225198848324692]], [[0.39752305008391264, 0.9175921886393666], [0.39752305008391264, 0.9175921886393666]], [[0.40903364194074554, 0.9125193037742763], [0.40903364194074554, 0.9125193037742763]], [[0.4204795132692139, 0.9073020329090445], [0.4204795132692139, 0.9073020329090445]], [[0.4318588530132969, 0.9019412015614098], [0.4318588530132969, 0.9019412015614098]], [[0.44316986064412556, 0.896437657964382], [0.44316986064412556, 0.896437657964382]], [[0.45441074644487683, 0.8907922729320287], [0.45441074644487683, 0.8907922729320287]], [[0.46557973179395645, 0.8850059397216877], [0.46557973179395645, 0.8850059397216877]], [[0.47667504944642675, 0.8790795738926293], [0.47667504944642675, 0.8790795738926293]], [[0.48769494381363376, 0.8730141131611886], [0.48769494381363376, 0.8730141131611886]], [[0.4986376712409907, 0.8668105172523933], [0.4986376712409907, 0.8668105172523933]], [[0.5095015002838723, 0.8604697677481082], [0.5095015002838723, 0.8604697677481082]], [[0.520284711981578, 0.8539928679317214], [0.520284711981578, 0.8539928679317214]], [[0.5309856001293194, 0.8473808426293968], [0.5309856001293194, 0.8473808426293968]], [[0.5416024715481897, 0.8406347380479183], [0.5416024715481897, 0.8406347380479183]], [[0.5521336463530699, 0.8337556216091518], [0.5521336463530699, 0.8337556216091518]], [[0.5625774582184366, 0.8267445817811466], [0.5625774582184366, 0.8267445817811466]], [[0.5729322546420195, 0.8196027279059118], [0.5729322546420195, 0.8196027279059118]], [[0.5831963972062728, 0.8123311900238863], [0.5831963972062728, 0.8123311900238863]], [[0.5933682618376198, 0.8049311186951352], [0.5933682618376198, 0.8049311186951352]], [[0.6034462390634255, 0.7974036848172994], [0.6034462390634255, 0.7974036848172994]], [[0.6134287342666611, 0.7897500794403265], [0.6134287342666611, 0.7897500794403265]], [[0.6233141679382159, 0.7819715135780135], [0.6233141679382159, 0.7819715135780135]], [[0.6331009759268206, 0.7740692180163913], [0.6331009759268206, 0.7740692180163913]], [[0.6427876096865385, 0.7660444431189787], [0.6427876096865385, 0.7660444431189787]], [[0.6523725365217901, 0.7578984586289417], [0.6523725365217901, 0.7578984586289417]], [[0.6618542398298678, 0.7496325534681827], [0.6618542398298678, 0.7496325534681827]], [[0.6712312193409025, 0.7412480355334005], [0.6712312193409025, 0.7412480355334005]], [[0.6805019913552521, 0.7327462314891401], [0.6805019913552521, 0.7327462314891401]], [[0.6896650889782615, 0.7241284865578805], [0.6896650889782615, 0.7241284865578805]], [[0.698719062352367, 0.7153961643071823], [0.698719062352367, 0.7153961643071823]], [[0.7076624788865039, 0.7065506464339328], [0.7076624788865039, 0.7065506464339328]], [[0.7164939234827827, 0.6975933325457234], [0.7164939234827827, 0.6975933325457234]], [[0.7252119987603968, 0.6885256399393928], [0.7252119987603968, 0.6885256399393928]], [[0.7338153252767271, 0.6793490033767704], [0.7338153252767271, 0.6793490033767704]], [[0.7423025417456087, 0.670064874857658], [0.7423025417456087, 0.670064874857658]], [[0.7506723052527237, 0.6606747233900823], [0.7506723052527237, 0.6606747233900823]], [[0.7589232914680881, 0.6511800347578566], [0.7589232914680881, 0.6511800347578566]], [[0.767054194855598, 0.6415823112854891], [0.767054194855598, 0.6415823112854891]], [[0.7750637288796014, 0.6318830716004724], [0.7750637288796014, 0.6318830716004724]], [[0.7829506262084629, 0.6220838503929964], [0.7829506262084629, 0.6220838503929964]], [[0.7907136389150935, 0.612186198173114], [0.7907136389150935, 0.612186198173114]], [[0.7983515386744056, 0.60219168102541], [0.7983515386744056, 0.60219168102541]], [[0.8058631169576688, 0.5921018803612016], [0.8058631169576688, 0.5921018803612016]], [[0.8132471852237325, 0.5819183926683157], [0.8132471852237325, 0.5819183926683157]], [[0.820502575107087, 0.5716428292584793], [0.820502575107087, 0.5716428292584793]], [[0.8276281386027308, 0.5612768160123658], [0.8276281386027308, 0.5612768160123658]], [[0.8346227482478168, 0.5508219931223347], [0.8346227482478168, 0.5508219931223347]], [[0.8414852973000496, 0.5402800148329078], [0.8414852973000496, 0.5402800148329078]], [[0.8482146999128017, 0.5296525491790214], [0.8482146999128017, 0.5296525491790214]], [[0.8548098913069254, 0.5189412777220967], [0.8548098913069254, 0.5189412777220967]], [[0.8612698279392301, 0.5081478952839703], [0.8612698279392301, 0.5081478952839703]], [[0.8675934876676011, 0.49727410967872443], [0.8675934876676011, 0.49727410967872443]], [[0.8737798699127283, 0.48632164144246715], [0.8737798699127283, 0.48632164144246715]], [[0.8798279958164291, 0.4752922235610904], [0.8798279958164291, 0.4752922235610904]], [[0.8857369083965291, 0.4641876011960666], [0.8857369083965291, 0.4641876011960666]], [[0.8915056726982836, 0.4530095314083147], [0.8915056726982836, 0.4530095314083147]], [[0.8971333759423138, 0.4417597828801838], [0.8971333759423138, 0.4417597828801838]], [[0.9026191276690336, 0.43044013563559885], [0.9026191276690336, 0.43044013563559885]], [[0.9079620598795458, 0.4190523807584107], [0.9079620598795458, 0.4190523807584107]], [[0.9131613271729829, 0.4075983201089971], [0.9131613271729829, 0.4075983201089971]], [[0.9182161068802737, 0.39607976603915773], [0.9182161068802737, 0.39607976603915773]], [[0.9231255991943119, 0.3844985411053501], [0.9231255991943119, 0.3844985411053501]], [[0.9278890272965089, 0.37285647778030967], [0.9278890272965089, 0.37285647778030967]], [[0.932505637479707, 0.36115541816310226], [0.932505637479707, 0.36115541816310226]], [[0.9369746992674379, 0.3493972136876513], [0.9369746992674379, 0.3493972136876513]], [[0.9412955055295031, 0.3375837248297927], [0.9412955055295031, 0.3375837248297927]], [[0.9454673725938633, 0.32571682081289105], [0.9454673725938633, 0.32571682081289105]], [[0.9494896403548132, 0.3137983793120792], [0.9494896403548132, 0.3137983793120792]], [[0.9533616723774291, 0.3018302861571574], [0.9533616723774291, 0.3018302861571574]], [[0.9570828559982706, 0.2898144350342019], [0.9570828559982706, 0.2898144350342019]], [[0.9606526024223209, 0.27775272718593136], [0.9606526024223209, 0.27775272718593136]], [[0.9640703468161504, 0.26564707111087715], [0.9640703468161504, 0.26564707111087715]], [[0.96733554839729, 0.25349938226140567], [0.96733554839729, 0.25349938226140567]], [[0.9704476905197967, 0.24131158274064027], [0.9704476905197967, 0.24131158274064027]], [[0.9734062807560024, 0.22908560099833106], [0.9734062807560024, 0.22908560099833106]], [[0.9762108509744293, 0.21682337152572034], [0.9762108509744293, 0.21682337152572034]], [[0.9788609574138614, 0.20452683454945125], [0.9788609574138614, 0.20452683454945125]], [[0.9813561807535595, 0.1921979357245733], [0.9813561807535595, 0.1921979357245733]], [[0.98369612617961, 0.17983862582668034], [0.98369612617961, 0.17983862582668034]], [[0.9858804234473957, 0.1674508604432468], [0.9858804234473957, 0.1674508604432468]], [[0.987908726940178, 0.15503659966419847], [0.987908726940178, 0.15503659966419847]], [[0.9897807157237833, 0.14259780777177156], [0.9897807157237833, 0.14259780777177156]], [[0.9914960935973847, 0.13013645292970846], [0.9914960935973847, 0.13013645292970846]], [[0.9930545891403676, 0.11765450687183943], [0.9930545891403676, 0.11765450687183943]], [[0.9944559557552775, 0.1051539445900992], [0.9944559557552775, 0.1051539445900992]], [[0.9956999717068375, 0.09263674402202833], [0.9956999717068375, 0.09263674402202833]], [[0.9967864401570342, 0.08010488573780816], [0.9967864401570342, 0.08010488573780816]], [[0.9977151891962615, 0.06756035262687954], [0.9977151891962615, 0.06756035262687954]], [[0.9984860718705224, 0.05500512958419429], [0.9984860718705224, 0.05500512958419429]], [[0.9990989662046814, 0.042441203196148705], [0.9990989662046814, 0.042441203196148705]], [[0.9995537752217638, 0.029870561426253633], [0.9995537752217638, 0.029870561426253633]], [[0.9998504269583004, 0.01729519330057795], [0.9998504269583004, 0.01729519330057795]], [[0.999988874475714, 0.004717088593032691], [0.999988874475714, 0.004717088593032691]], [[0.999969095867747, -0.007861762489467534], [0.999969095867747, -0.007861762489467534]], [[0.9997910942639262, -0.020439369621910786], [0.9997910942639262, -0.020439369621910786]], [[0.9994548978290694, -0.03301374267611272], [0.9994548978290694, -0.03301374267611272]], [[0.9989605597588275, -0.045582892035610355], [0.9989605597588275, -0.045582892035610355]], [[0.9983081582712683, -0.058144828910474865], [0.9983081582712683, -0.058144828910474865]], [[0.9974977965944998, -0.07069756565199363], [0.9974977965944998, -0.07069756565199363]], [[0.9965296029503368, -0.08323911606717167], [0.9965296029503368, -0.08323911606717167]], [[0.9954037305340127, -0.09576749573300279], [0.9954037305340127, -0.09576749573300279]], [[0.9941203574899394, -0.1082807223104606], [0.9941203574899394, -0.1082807223104606]], [[0.9926796868835203, -0.12077681585816072], [0.9926796868835203, -0.12077681585816072]], [[0.9910819466690197, -0.1332537991456392], [0.9910819466690197, -0.1332537991456392]], [[0.9893273896534936, -0.14570969796621086], [0.9893273896534936, -0.14570969796621086]], [[0.9874162934567892, -0.1581425414493393], [0.9874162934567892, -0.1581425414493393]], [[0.9853489604676167, -0.17055036237248902], [0.9853489604676167, -0.17055036237248902]], [[0.9831257177957046, -0.18293119747238504], [0.9831257177957046, -0.18293119747238504]], [[0.9807469172200398, -0.1952830877556692], [0.9807469172200398, -0.1952830877556692]], [[0.9782129351332084, -0.2076040788088552], [0.9782129351332084, -0.2076040788088552]], [[0.9755241724818389, -0.2198922211075767], [0.9755241724818389, -0.2198922211075767]], [[0.9726810547031601, -0.23214557032506142], [0.9726810547031601, -0.23214557032506142]], [[0.9696840316576879, -0.24436218763976586], [0.9696840316576879, -0.24436218763976586]], [[0.9665335775580415, -0.25654014004216474], [0.9665335775580415, -0.25654014004216474]], [[0.9632301908939129, -0.2686775006405933], [0.9632301908939129, -0.2686775006405933]], [[0.9597743943531892, -0.2807723489661489], [0.9597743943531892, -0.2807723489661489]], [[0.9561667347392514, -0.29282277127654904], [0.9561667347392514, -0.29282277127654904]], [[0.9524077828844516, -0.3048268608589526], [0.9524077828844516, -0.3048268608589526]], [[0.9484981335597957, -0.3167827183316413], [0.9484981335597957, -0.3167827183316413]], [[0.9444384053808291, -0.32868845194456814], [0.9444384053808291, -0.32868845194456814]], [[0.9402292407097596, -0.340542177878672], [0.9402292407097596, -0.340542177878672]], [[0.9358713055538124, -0.3523420205439635], [0.9358713055538124, -0.3523420205439635]], [[0.9313652894598542, -0.36408611287628373], [0.9313652894598542, -0.36408611287628373]], [[0.9267119054052854, -0.37577259663273127], [0.9267119054052854, -0.37577259663273127]], [[0.9219118896852252, -0.38739962268569283], [0.9219118896852252, -0.38739962268569283]], [[0.9169660017960138, -0.3989653513154153], [0.9169660017960138, -0.3989653513154153]], [[0.9118750243150339, -0.4104679525011135], [0.9118750243150339, -0.4104679525011135]], [[0.9066397627768898, -0.4219056062105182], [0.9066397627768898, -0.4219056062105182]], [[0.901261045545945, -0.4332765026878681], [0.901261045545945, -0.4332765026878681]], [[0.895739723685256, -0.44457884274025133], [0.895739723685256, -0.44457884274025133]], [[0.8900766708219062, -0.45581083802230066], [0.8900766708219062, -0.45581083802230066]], [[0.8842727830087785, -0.46697071131914664], [0.8842727830087785, -0.46697071131914664]], [[0.878328978582769, -0.47805669682763535], [0.878328978582769, -0.47805669682763535]], [[0.8722461980194871, -0.48906704043571536], [0.8722461980194871, -0.48906704043571536]], [[0.8660254037844392, -0.4999999999999992], [0.8660254037844392, -0.4999999999999992]], [[0.8596675801807453, -0.5108538456214086], [0.8596675801807453, -0.5108538456214086]], [[0.8531737331933934, -0.5216268599188969], [0.8531737331933934, -0.5216268599188969]], [[0.8465448903300608, -0.5323173383011919], [0.8465448903300608, -0.5323173383011919]], [[0.8397821004585404, -0.5429235892364983], [0.8397821004585404, -0.5429235892364983]], [[0.8328864336407736, -0.5534439345201582], [0.8328864336407736, -0.5534439345201582]], [[0.8258589809635439, -0.5638767095401768], [0.8258589809635439, -0.5638767095401768]], [[0.8187008543658284, -0.5742202635406232], [0.8187008543658284, -0.5742202635406232]], [[0.8114131864628666, -0.5844729598828138], [0.8114131864628666, -0.5844729598828138]], [[0.803997130366941, -0.5946331763042861], [0.803997130366941, -0.5946331763042861]], [[0.7964538595049301, -0.6046993051754741], [0.7964538595049301, -0.6046993051754741]], [[0.7887845674326319, -0.6146697537540917], [0.7887845674326319, -0.6146697537540917]], [[0.7809904676459185, -0.6245429444371375], [0.7809904676459185, -0.6245429444371375]], [[0.7730727933887184, -0.6343173150105269], [0.7730727933887184, -0.6343173150105269]], [[0.76503279745789, -0.6439913188962683], [0.76503279745789, -0.6439913188962683]], [[0.7568717520049925, -0.6535634253971785], [0.7568717520049925, -0.6535634253971785]], [[0.7485909483349908, -0.6630321199390865], [0.7485909483349908, -0.6630321199390865]], [[0.7401916967019444, -0.6723959043104716], [0.7401916967019444, -0.6723959043104716]], [[0.7316753261016786, -0.6816532968995326], [0.7316753261016786, -0.6816532968995326]], [[0.7230431840615102, -0.69080283292861], [0.7230431840615102, -0.69080283292861]], [[0.7142966364270213, -0.6998430646859649], [0.7142966364270213, -0.6998430646859649]], [[0.7054370671459542, -0.7087725617548373], [0.7054370671459542, -0.7087725617548373]], [[0.6964658780492222, -0.7175899112397874], [0.6964658780492222, -0.7175899112397874]], [[0.6873844886291115, -0.7262937179902459], [0.6873844886291115, -0.7262937179902459]], [[0.678194335814667, -0.7348826048212753], [0.678194335814667, -0.7348826048212753]], [[0.6688968737443408, -0.7433552127314689], [0.6688968737443408, -0.7433552127314689]], [[0.6594935735358967, -0.7517102011179926], [0.6594935735358967, -0.7517102011179926]], [[0.6499859230536468, -0.7599462479886974], [0.6499859230536468, -0.7599462479886974]], [[0.6403754266730268, -0.7680620501712988], [0.6403754266730268, -0.7680620501712988]], [[0.6306636050425575, -0.7760563235195788], [0.6306636050425575, -0.7760563235195788]], [[0.6208519948432446, -0.7839278031165648], [0.6208519948432446, -0.7839278031165648]], [[0.6109421485454233, -0.7916752434746851], [0.6109421485454233, -0.7916752434746851]], [[0.600935634163124, -0.7992974187328293], [0.600935634163124, -0.7992974187328293]], [[0.5908340350059585, -0.8067931228503239], [0.5908340350059585, -0.8067931228503239]], [[0.5806389494286068, -0.8141611697977519], [0.5806389494286068, -0.8141611697977519]], [[0.570351990577902, -0.8214003937446248], [0.570351990577902, -0.8214003937446248]], [[0.5599747861375968, -0.8285096492438412], [0.5599747861375968, -0.8285096492438412]], [[0.5495089780708068, -0.8354878114129359], [0.5495089780708068, -0.8354878114129359]], [[0.5389562223602165, -0.8423337761120617], [0.5389562223602165, -0.8423337761120617]], [[0.5283181887460523, -0.8490464601186973], [0.5283181887460523, -0.8490464601186973]], [[0.5175965604618786, -0.8556248012990465], [0.5175965604618786, -0.8556248012990465]], [[0.5067930339682736, -0.8620677587760909], [0.5067930339682736, -0.8620677587760909]], [[0.49590931868438975, -0.8683743130942925], [0.49590931868438975, -0.8683743130942925]], [[0.4849471367174889, -0.8745434663808935], [0.4849471367174889, -0.8745434663808935]], [[0.4739082225904436, -0.8805742425038144], [0.4739082225904436, -0.8805742425038144]], [[0.4627943229673003, -0.886465687226098], [0.4627943229673003, -0.886465687226098]], [[0.4516071963768956, -0.8922168683569035], [0.4516071963768956, -0.8922168683569035]], [[0.44034861293462074, -0.8978268758989985], [0.44034861293462074, -0.8978268758989985]], [[0.42902035406232714, -0.903294822192752], [0.42902035406232714, -0.903294822192752]], [[0.4176242122064685, -0.9086198420565812], [0.4176242122064685, -0.9086198420565812]], [[0.4061619905544733, -0.9138010929238529], [0.4061619905544733, -0.9138010929238529]], [[0.3946355027494409, -0.918837754976196], [0.3946355027494409, -0.918837754976196]], [[0.38304657260316866, -0.9237290312732221], [0.38304657260316866, -0.9237290312732221]], [[0.37139703380756833, -0.9284741478786256], [0.37139703380756833, -0.9284741478786256]], [[0.3596887296445368, -0.9330723539826369], [0.3596887296445368, -0.9330723539826369]], [[0.34792351269428423, -0.9375229220208273], [0.34792351269428423, -0.9375229220208273]], [[0.3361032445422173, -0.9418251477892244], [0.3361032445422173, -0.9418251477892244]], [[0.3242297954843714, -0.9459783505557422], [0.3242297954843714, -0.9459783505557422]], [[0.31230504423149086, -0.9499818731678866], [0.31230504423149086, -0.9499818731678866]], [[0.3003308776117511, -0.9538350821567402], [0.3003308776117511, -0.9538350821567402]], [[0.28830919027222335, -0.9575373678371905], [0.28830919027222335, -0.9575373678371905]], [[0.27624188437907515, -0.9610881444044025], [0.27624188437907515, -0.9610881444044025]], [[0.264130869316608, -0.9644868500265066], [0.264130869316608, -0.9644868500265066]], [[0.2519780613851261, -0.9677329469334987], [0.2519780613851261, -0.9677329469334987]], [[0.2397853834977361, -0.9708259215023276], [0.2397853834977361, -0.9708259215023276]], [[0.22755476487608342, -0.9737652843381666], [0.22755476487608342, -0.9737652843381666]], [[0.2152881407450906, -0.9765505703518492], [0.2152881407450906, -0.9765505703518492]], [[0.20298745202676252, -0.9791813388334577], [0.20298745202676252, -0.9791813388334577]], [[0.19065464503306495, -0.9816571735220581], [0.19065464503306495, -0.9816571735220581]], [[0.17829167115797728, -0.9839776826715613], [0.17829167115797728, -0.9839776826715613]], [[0.1659004865687139, -0.9861424991127113], [0.1659004865687139, -0.9861424991127113]], [[0.15348305189621775, -0.9881512803111794], [0.15348305189621775, -0.9881512803111794]], [[0.14104133192492, -0.9900037084217637], [0.14104133192492, -0.9900037084217637]], [[0.12857729528187029, -0.9916994903386805], [0.12857729528187029, -0.9916994903386805]], [[0.11609291412523105, -0.9932383577419429], [0.11609291412523105, -0.9932383577419429]], [[0.10359016383224108, -0.9946200671398147], [0.10359016383224108, -0.9946200671398147]], [[0.09107102268664179, -0.9958443999073395], [0.09107102268664179, -0.9958443999073395]], [[0.07853747156566976, -0.996911162320932], [0.07853747156566976, -0.996911162320932]], [[0.0659914936266216, -0.9978201855890306], [0.0659914936266216, -0.9978201855890306]], [[0.05343507399305771, -0.9985713258788059], [0.05343507399305771, -0.9985713258788059]], [[0.04087019944071283, -0.9991644643389177], [0.04087019944071283, -0.9991644643389177]], [[0.028298858083118522, -0.9995995071183216], [0.028298858083118522, -0.9995995071183216]], [[0.01572303905704239, -0.9998763853811183], [0.01572303905704239, -0.9998763853811183]], [[0.003144732207736932, -0.9999950553174458], [0.003144732207736932, -0.9999950553174458]], [[-0.009434072225895224, -0.999955498150411], [-0.009434072225895224, -0.999955498150411]], [[-0.02201138392622685, -0.9997577201390606], [-0.02201138392622685, -0.9997577201390606]], [[-0.03458521281181564, -0.9994017525773914], [-0.03458521281181564, -0.9994017525773914]], [[-0.04715356935230482, -0.9988876517893979], [-0.04715356935230482, -0.9988876517893979]], [[-0.05971446488320808, -0.9982154991201609], [-0.05971446488320808, -0.9982154991201609]], [[-0.07226591192058601, -0.9973854009229762], [-0.07226591192058601, -0.9973854009229762]], [[-0.08480592447550901, -0.9963974885425265], [-0.08480592447550901, -0.9963974885425265]], [[-0.0973325183683015, -0.9952519182940992], [-0.0973325183683015, -0.9952519182940992]], [[-0.1098437115424997, -0.9939488714388522], [-0.1098437115424997, -0.9939488714388522]], [[-0.12233752437845594, -0.9924885541551351], [-0.12233752437845594, -0.9924885541551351]], [[-0.13481198000658376, -0.9908711975058637], [-0.13481198000658376, -0.9908711975058637]], [[-0.14726510462013975, -0.9890970574019616], [-0.14726510462013975, -0.9890970574019616]], [[-0.15969492778754882, -0.9871664145618658], [-0.15969492778754882, -0.9871664145618658]], [[-0.17209948276416748, -0.9850795744671118], [-0.17209948276416748, -0.9850795744671118]], [[-0.18447680680349163, -0.9828368673139949], [-0.18447680680349163, -0.9828368673139949]], [[-0.19682494146770374, -0.9804386479613271], [-0.19682494146770374, -0.9804386479613271]], [[-0.2091419329375665, -0.9778852958742853], [-0.2091419329375665, -0.9778852958742853]], [[-0.22142583232155733, -0.9751772150643726], [-0.22142583232155733, -0.9751772150643726]], [[-0.23367469596425144, -0.9723148340254892], [-0.23367469596425144, -0.9723148340254892]], [[-0.24588658575385006, -0.9692986056661356], [-0.24588658575385006, -0.9692986056661356]], [[-0.2580595694288491, -0.9661290072377483], [-0.2580595694288491, -0.9661290072377483]], [[-0.2701917208837818, -0.9628065402591844], [-0.2701917208837818, -0.9628065402591844]], [[-0.2822811204739704, -0.9593317304373705], [-0.2822811204739704, -0.9593317304373705]], [[-0.29432585531928135, -0.9557051275841171], [-0.29432585531928135, -0.9557051275841171]], [[-0.30632401960678207, -0.951927305529127], [-0.30632401960678207, -0.951927305529127]], [[-0.31827371489230794, -0.9479988620291956], [-0.31827371489230794, -0.9479988620291956]], [[-0.3301730504008353, -0.9439204186736335], [-0.3301730504008353, -0.9439204186736335]], [[-0.342020143325668, -0.9396926207859086], [-0.342020143325668, -0.9396926207859086]], [[-0.35381311912633706, -0.9353161373215435], [-0.35381311912633706, -0.9353161373215435]], [[-0.3655501118252182, -0.9307916607622624], [-0.3655501118252182, -0.9307916607622624]], [[-0.37722926430276815, -0.9261199070064267], [-0.37722926430276815, -0.9261199070064267]], [[-0.3888487285913865, -0.9213016152557545], [-0.3888487285913865, -0.9213016152557545]], [[-0.4004066661678036, -0.9163375478983632], [-0.4004066661678036, -0.9163375478983632]], [[-0.4119012482439916, -0.9112284903881362], [-0.4119012482439916, -0.9112284903881362]], [[-0.4233306560565341, -0.9059752511204401], [-0.4233306560565341, -0.9059752511204401]], [[-0.4346930811543944, -0.9005786613042189], [-0.4346930811543944, -0.9005786613042189]], [[-0.4459867256850755, -0.8950395748304681], [-0.4459867256850755, -0.8950395748304681]], [[-0.4572098026790778, -0.8893588681371309], [-0.4572098026790778, -0.8893588681371309]], [[-0.46836053633265995, -0.8835374400704156], [-0.46836053633265995, -0.8835374400704156]], [[-0.47943716228880834, -0.8775762117425784], [-0.47943716228880834, -0.8775762117425784]], [[-0.4904379279164198, -0.8714761263861728], [-0.4904379279164198, -0.8714761263861728]], [[-0.5013610925876044, -0.8652381492048091], [-0.5013610925876044, -0.8652381492048091]], [[-0.5122049279531135, -0.8588632672204265], [-0.5122049279531135, -0.8588632672204265]], [[-0.5229677182158008, -0.852352489117125], [-0.5229677182158008, -0.852352489117125]], [[-0.5336477604021214, -0.8457068450815567], [-0.5336477604021214, -0.8457068450815567]], [[-0.5442433646315787, -0.8389273866399275], [-0.5442433646315787, -0.8389273866399275]], [[-0.5547528543841161, -0.8320151864916143], [-0.5547528543841161, -0.8320151864916143]], [[-0.5651745667653925, -0.8249713383394304], [-0.5651745667653925, -0.8249713383394304]], [[-0.5755068527698889, -0.8177969567165786], [-0.5755068527698889, -0.8177969567165786]], [[-0.5857480775418389, -0.8104931768102923], [-0.5857480775418389, -0.8104931768102923]], [[-0.5958966206338965, -0.8030611542822266], [-0.5958966206338965, -0.8030611542822266]], [[-0.6059508762635476, -0.7955020650855904], [-0.6059508762635476, -0.7955020650855904]], [[-0.6159092535671783, -0.7878171052790878], [-0.6159092535671783, -0.7878171052790878]], [[-0.6257701768518052, -0.7800074908376589], [-0.6257701768518052, -0.7800074908376589]], [[-0.6355320858443827, -0.7720744574600873], [-0.6355320858443827, -0.7720744574600873]], [[-0.6451934359386927, -0.76401926037347], [-0.6451934359386927, -0.76401926037347]], [[-0.6547526984397336, -0.7558431741346133], [-0.6547526984397336, -0.7558431741346133]], [[-0.6642083608056132, -0.7475474924283543], [-0.6642083608056132, -0.7475474924283543]], [[-0.6735589268868657, -0.7391335278628713], [-0.6735589268868657, -0.7391335278628713]], [[-0.6828029171631881, -0.7306026117619896], [-0.6828029171631881, -0.7306026117619896]], [[-0.6919388689775459, -0.7219560939545248], [-0.6919388689775459, -0.7219560939545248]], [[-0.7009653367675964, -0.7131953425607112], [-0.7009653367675964, -0.7131953425607112]], [[-0.7098808922944282, -0.7043217437757168], [-0.7098808922944282, -0.7043217437757168]], [[-0.7186841248685372, -0.695336701650319], [-0.7186841248685372, -0.695336701650319]], [[-0.7273736415730482, -0.6862416378687342], [-0.7273736415730482, -0.6862416378687342]], [[-0.7359480674841022, -0.6770379915236775], [-0.7359480674841022, -0.6770379915236775]], [[-0.7444060458884184, -0.6677272188886492], [-0.7444060458884184, -0.6677272188886492]], [[-0.7527462384979536, -0.6583107931875202], [-0.7527462384979536, -0.6583107931875202]], [[-0.7609673256616669, -0.648790204361418], [-0.7609673256616669, -0.648790204361418]], [[-0.7690680065743155, -0.6391669588329865], [-0.7690680065743155, -0.6391669588329865]], [[-0.7770469994822877, -0.6294425792680167], [-0.7770469994822877, -0.6294425792680167]], [[-0.7849030418864043, -0.619618604334529], [-0.7849030418864043, -0.619618604334529]], [[-0.7926348907416839, -0.609696588459308], [-0.7926348907416839, -0.609696588459308]], [[-0.8002413226540318, -0.5996781015819452], [-0.8002413226540318, -0.5996781015819452]], [[-0.807721134073806, -0.5895647289064406], [-0.807721134073806, -0.5895647289064406]], [[-0.8150731414862619, -0.5793580706503675], [-0.8150731414862619, -0.5793580706503675]], [[-0.8222961815988086, -0.5690597417916851], [-0.8222961815988086, -0.5690597417916851]], [[-0.8293891115250823, -0.5586713718131927], [-0.8293891115250823, -0.5586713718131927]], [[-0.8363508089657752, -0.5481946044447112], [-0.8363508089657752, -0.5481946044447112]], [[-0.8431801723862219, -0.537631097402988], [-0.8431801723862219, -0.537631097402988]], [[-0.8498761211906855, -0.5269825221294112], [-0.8498761211906855, -0.5269825221294112]], [[-0.8564375958933453, -0.5162505635255297], [-0.8564375958933453, -0.5162505635255297]], [[-0.8628635582859301, -0.5054369196864662], [-0.8628635582859301, -0.5054369196864662]], [[-0.8691529916019983, -0.49454330163221977], [-0.8691529916019983, -0.49454330163221977]], [[-0.8753049006778127, -0.4835714330369447], [-0.8753049006778127, -0.4835714330369447]], [[-0.8813183121098064, -0.4725230499562131], [-0.8813183121098064, -0.4725230499562131]], [[-0.8871922744086038, -0.46139990055231767], [-0.8871922744086038, -0.46139990055231767]], [[-0.8929258581495678, -0.4502037448176746], [-0.8929258581495678, -0.4502037448176746]], [[-0.898518156119867, -0.43893635429633115], [-0.898518156119867, -0.43893635429633115]], [[-0.9039682834620154, -0.42759951180367056], [-0.9039682834620154, -0.42759951180367056]], [[-0.9092753778138881, -0.4161950111443084], [-0.9092753778138881, -0.4161950111443084]], [[-0.914438599445165, -0.40472465682827513], [-0.914438599445165, -0.40472465682827513]], [[-0.919457131390205, -0.39319026378547983], [-0.919457131390205, -0.39319026378547983]], [[-0.9243301795773077, -0.38159365707855025], [-0.9243301795773077, -0.38159365707855025]], [[-0.9290569729543624, -0.36993667161404425], [-0.9290569729543624, -0.36993667161404425]], [[-0.9336367636108461, -0.3582211518521277], [-0.9336367636108461, -0.3582211518521277]], [[-0.9380688268961654, -0.34644895151472466], [-0.9380688268961654, -0.34644895151472466]], [[-0.9423524615343185, -0.3346219332922018], [-0.9423524615343185, -0.3346219332922018]], [[-0.946486989734852, -0.32274196854865056], [-0.946486989734852, -0.32274196854865056]], [[-0.9504717573001114, -0.31081093702577167], [-0.9504717573001114, -0.31081093702577167]], [[-0.9543061337287484, -0.2988307265454612], [-0.9543061337287484, -0.2988307265454612]], [[-0.9579895123154887, -0.2868032327110909], [-0.9579895123154887, -0.2868032327110909]], [[-0.9615213102471251, -0.27473035860758444], [-0.9615213102471251, -0.27473035860758444]], [[-0.9649009686947388, -0.2626140145002827], [-0.9649009686947388, -0.2626140145002827]], [[-0.9681279529021183, -0.25045611753270025], [-0.9681279529021183, -0.25045611753270025]], [[-0.9712017522703761, -0.23825859142316594], [-0.9712017522703761, -0.23825859142316594]], [[-0.9741218804387358, -0.22602336616045093], [-0.9741218804387358, -0.22602336616045093]], [[-0.9768878753614922, -0.21375237769837674], [-0.9768878753614922, -0.21375237769837674]], [[-0.9794992993811164, -0.2014475676495055], [-0.9794992993811164, -0.2014475676495055]], [[-0.9819557392975065, -0.18911088297791753], [-0.9819557392975065, -0.18911088297791753]], [[-0.9842568064333685, -0.17674427569114207], [-0.9842568064333685, -0.17674427569114207]], [[-0.9864021366957143, -0.1643497025313075], [-0.9864021366957143, -0.1643497025313075]], [[-0.9883913906334727, -0.1519291246655162], [-0.9883913906334727, -0.1519291246655162]], [[-0.9902242534911982, -0.1394845073755471], [-0.9902242534911982, -0.1394845073755471]], [[-0.9919004352588768, -0.12701781974687945], [-0.9919004352588768, -0.12701781974687945]], [[-0.9934196707178105, -0.11453103435714257], [-0.9934196707178105, -0.11453103435714257]], [[-0.9947817194825852, -0.10202612696398496], [-0.9947817194825852, -0.10202612696398496]], [[-0.9959863660391042, -0.08950507619246842], [-0.9959863660391042, -0.08950507619246842]], [[-0.9970334197786901, -0.07696986322198038], [-0.9970334197786901, -0.07696986322198038]], [[-0.9979227150282431, -0.0644224714727701], [-0.9979227150282431, -0.0644224714727701]], [[-0.9986541110764564, -0.051864886292102175], [-0.9986541110764564, -0.051864886292102175]], [[-0.9992274921960794, -0.03929909464013164], [-0.9992274921960794, -0.03929909464013164]], [[-0.9996427676622299, -0.026727084775506123], [-0.9996427676622299, -0.026727084775506123]], [[-0.9998998717667489, -0.014150845940762564], [-0.9998998717667489, -0.014150845940762564]], [[-0.9999987638285974, -0.001572368047586014], [-0.9999987638285974, -0.001572368047586014]], [[-0.9999394282002937, 0.0110063586380641], [-0.9999394282002937, 0.0110063586380641]], [[-0.9997218742703887, 0.02358334381085534], [-0.9997218742703887, 0.02358334381085534]], [[-0.9993461364619809, 0.036156597441018276], [-0.9993461364619809, 0.036156597441018276]], [[-0.9988122742272693, 0.04872413008921046], [-0.9988122742272693, 0.04872413008921046]], [[-0.9981203720381463, 0.06128395322131545], [-0.9981203720381463, 0.06128395322131545]], [[-0.9972705393728328, 0.0738340795230701], [-0.9972705393728328, 0.0738340795230701]], [[-0.9962629106985544, 0.08637252321452737], [-0.9962629106985544, 0.08637252321452737]], [[-0.9950976454502662, 0.09889730036424782], [-0.9950976454502662, 0.09889730036424782]], [[-0.9937749280054243, 0.11140642920322712], [-0.9937749280054243, 0.11140642920322712]], [[-0.9922949676548137, 0.12389793043845473], [-0.9922949676548137, 0.12389793043845473]], [[-0.9906579985694319, 0.1363698275660986], [-0.9906579985694319, 0.1363698275660986]], [[-0.9888642797634358, 0.14882014718424852], [-0.9888642797634358, 0.14882014718424852]], [[-0.9869140950531602, 0.16124691930515087], [-0.9869140950531602, 0.16124691930515087]], [[-0.9848077530122081, 0.17364817766692972], [-0.9848077530122081, 0.17364817766692972]], [[-0.9825455869226281, 0.18602196004469043], [-0.9825455869226281, 0.18602196004469043]], [[-0.9801279547221767, 0.19836630856101212], [-0.9801279547221767, 0.19836630856101212]], [[-0.9775552389476866, 0.21067926999572462], [-0.9775552389476866, 0.21067926999572462]], [[-0.9748278466745344, 0.2229588960949763], [-0.9748278466745344, 0.2229588960949763]], [[-0.9719462094522341, 0.23520324387948816], [-0.9719462094522341, 0.23520324387948816]], [[-0.9689107832361499, 0.24741037595200138], [-0.9689107832361499, 0.24741037595200138]], [[-0.9657220483153551, 0.25957836080381363], [-0.9657220483153551, 0.25957836080381363]], [[-0.9623805092366339, 0.27170527312041143], [-0.9623805092366339, 0.27170527312041143]], [[-0.9588866947246498, 0.2837891940860965], [-0.9588866947246498, 0.2837891940860965]], [[-0.9552411575982872, 0.29582821168760115], [-0.9552411575982872, 0.29582821168760115]], [[-0.9514444746831768, 0.30782042101662727], [-0.9514444746831768, 0.30782042101662727]], [[-0.9474972467204302, 0.31976392457124386], [-0.9474972467204302, 0.31976392457124386]], [[-0.9434000982715814, 0.3316568325561384], [-0.9434000982715814, 0.3316568325561384]], [[-0.9391536776197683, 0.3434972631816217], [-0.9391536776197683, 0.3434972631816217]], [[-0.9347586566671513, 0.35528334296139286], [-0.9347586566671513, 0.35528334296139286]], [[-0.9302157308286049, 0.3670132070089637], [-0.9302157308286049, 0.3670132070089637]], [[-0.9255256189216783, 0.3786849993327492], [-0.9255256189216783, 0.3786849993327492]], [[-0.9206890630528639, 0.3902968731297237], [-0.9206890630528639, 0.3902968731297237]], [[-0.9157068285001696, 0.40184699107765015], [-0.9157068285001696, 0.40184699107765015]], [[-0.9105797035920364, 0.41333352562578207], [-0.9105797035920364, 0.41333352562578207]], [[-0.9053084995825972, 0.4247546592840467], [-0.9053084995825972, 0.4247546592840467]], [[-0.8998940505233184, 0.4361085849106107], [-0.8998940505233184, 0.4361085849106107]], [[-0.8943372131310279, 0.4473935059978257], [-0.8943372131310279, 0.4473935059978257]], [[-0.8886388666523561, 0.45860763695649037], [-0.8886388666523561, 0.45860763695649037]], [[-0.8827999127246203, 0.4697492033983695], [-0.8827999127246203, 0.4697492033983695]], [[-0.8768212752331539, 0.48081644241696414], [-0.8768212752331539, 0.48081644241696414]], [[-0.8707039001651283, 0.49180760286644026], [-0.8707039001651283, 0.49180760286644026]], [[-0.8644487554598653, 0.502720945638721], [-0.8644487554598653, 0.502720945638721]], [[-0.8580568308556884, 0.5135547439386501], [-0.8580568308556884, 0.5135547439386501]], [[-0.8515291377333118, 0.5243072835572309], [-0.8515291377333118, 0.5243072835572309]], [[-0.8448667089558188, 0.53497686314285], [-0.8448667089558188, 0.53497686314285]], [[-0.838070598705227, 0.5455617944704909], [-0.838070598705227, 0.5455617944704909]], [[-0.8311418823156947, 0.5560604027088458], [-0.8311418823156947, 0.5560604027088458]], [[-0.8240816561033651, 0.5664710266853329], [-0.8240816561033651, 0.5664710266853329]], [[-0.8168910371929057, 0.5767920191489293], [-0.8168910371929057, 0.5767920191489293]], [[-0.8095711633407447, 0.5870217470308176], [-0.8095711633407447, 0.5870217470308176]], [[-0.8021231927550442, 0.5971585917027857], [-0.8021231927550442, 0.5971585917027857]], [[-0.7945483039124446, 0.6072009492333305], [-0.7945483039124446, 0.6072009492333305]], [[-0.7868476953715905, 0.6171472306414546], [-0.7868476953715905, 0.6171472306414546]], [[-0.7790225855834922, 0.6269958621480771], [-0.7790225855834922, 0.6269958621480771]], [[-0.7710742126987252, 0.6367452854250599], [-0.7710742126987252, 0.6367452854250599]], [[-0.7630038343715285, 0.6463939578417678], [-0.7630038343715285, 0.6463939578417678]], [[-0.7548127275607995, 0.6559403527091668], [-0.7548127275607995, 0.6559403527091668]], [[-0.7465021883280534, 0.6653829595213779], [-0.7465021883280534, 0.6653829595213779]], [[-0.7380735316323398, 0.6747202841946918], [-0.7380735316323398, 0.6747202841946918]], [[-0.7295280911221899, 0.6839508493039641], [-0.7295280911221899, 0.6839508493039641]], [[-0.7208672189245859, 0.6930731943163961], [-0.7208672189245859, 0.6930731943163961]], [[-0.7120922854310258, 0.7020858758226223], [-0.7120922854310258, 0.7020858758226223]], [[-0.703204679080685, 0.7109874677651012], [-0.703204679080685, 0.7109874677651012]], [[-0.694205806140723, 0.719776561663763], [-0.694205806140723, 0.719776561663763]], [[-0.685097090483782, 0.7284517668388598], [-0.685097090483782, 0.7284517668388598]], [[-0.6758799733626797, 0.7370117106310208], [-0.6758799733626797, 0.7370117106310208]], [[-0.6665559131823733, 0.745455038618435], [-0.6665559131823733, 0.745455038618435]], [[-0.6571263852691893, 0.7537804148311689], [-0.6571263852691893, 0.7537804148311689]], [[-0.6475928816373955, 0.7619865219625438], [-0.6475928816373955, 0.7619865219625438]], [[-0.6379569107531127, 0.7700720615775806], [-0.6379569107531127, 0.7700720615775806]], [[-0.6282199972956439, 0.7780357543184383], [-0.6282199972956439, 0.7780357543184383]], [[-0.6183836819162163, 0.7858763401068541], [-0.6183836819162163, 0.7858763401068541]], [[-0.6084495209942188, 0.7935925783435136], [-0.6084495209942188, 0.7935925783435136]], [[-0.5984190863909279, 0.8011832481043567], [-0.5984190863909279, 0.8011832481043567]], [[-0.5882939652008056, 0.8086471483337546], [-0.5882939652008056, 0.8086471483337546]], [[-0.5780757595003719, 0.8159830980345537], [-0.5780757595003719, 0.8159830980345537]], [[-0.5677660860947084, 0.8231899364549449], [-0.5677660860947084, 0.8231899364549449]], [[-0.5573665762616435, 0.8302665232721198], [-0.5573665762616435, 0.8302665232721198]], [[-0.546878875493628, 0.8372117387727103], [-0.546878875493628, 0.8372117387727103]], [[-0.5363046432373839, 0.8440244840299495], [-0.5363046432373839, 0.8440244840299495]], [[-0.5256455526313215, 0.850703681077561], [-0.5256455526313215, 0.850703681077561]], [[-0.5149032902408143, 0.8572482730803158], [-0.5149032902408143, 0.8572482730803158]], [[-0.5040795557913256, 0.86365722450126], [-0.5040795557913256, 0.86365722450126]], [[-0.49317606189947616, 0.8699295212655587], [-0.49317606189947616, 0.8699295212655587]], [[-0.4821945338020488, 0.8760641709209576], [-0.4821945338020488, 0.8760641709209576]], [[-0.4711367090830182, 0.8820602027948112], [-0.4711367090830182, 0.8820602027948112]], [[-0.46000433739861224, 0.8879166681476723], [-0.46000433739861224, 0.8879166681476723]], [[-0.44879918020046267, 0.893632640323412], [-0.44879918020046267, 0.893632640323412]], [[-0.43752301045690567, 0.8992072148958361], [-0.43752301045690567, 0.8992072148958361]], [[-0.4261776123724359, 0.9046395098117977], [-0.4261776123724359, 0.9046395098117977]], [[-0.4147647811054085, 0.909928665530756], [-0.4147647811054085, 0.909928665530756]], [[-0.403286322483982, 0.9150738451607857], [-0.403286322483982, 0.9150738451607857]], [[-0.39174405272039897, 0.9200742345909907], [-0.39174405272039897, 0.9200742345909907]], [[-0.3801397981235976, 0.9249290426203247], [-0.3801397981235976, 0.9249290426203247]], [[-0.3684753948102517, 0.9296375010827764], [-0.3684753948102517, 0.9296375010827764]], [[-0.3567526884142328, 0.9341988649689195], [-0.3567526884142328, 0.9341988649689195]], [[-0.34497353379459245, 0.9386124125437886], [-0.34497353379459245, 0.9386124125437886]], [[-0.33313979474205874, 0.9428774454610838], [-0.33313979474205874, 0.9428774454610838]], [[-0.3212533436841441, 0.9469932888736632], [-0.3212533436841441, 0.9469932888736632]], [[-0.30931606138887024, 0.9509592915403249], [-0.30931606138887024, 0.9509592915403249]], [[-0.2973298366671729, 0.9547748259288534], [-0.2973298366671729, 0.9547748259288534]], [[-0.28529656607405124, 0.9584392883153082], [-0.28529656607405124, 0.9584392883153082]], [[-0.2732181536084666, 0.9619520988795546], [-0.2732181536084666, 0.9619520988795546]], [[-0.26109651041208987, 0.9653127017970029], [-0.26109651041208987, 0.9653127017970029]], [[-0.24893355446689247, 0.9685205653265596], [-0.24893355446689247, 0.9685205653265596]], [[-0.2367312102916815, 0.9715751818947599], [-0.2367312102916815, 0.9715751818947599]], [[-0.22449140863757358, 0.974476068176083], [-0.22449140863757358, 0.974476068176083]], [[-0.2122160861825098, 0.9772227651694252], [-0.2122160861825098, 0.9772227651694252]], [[-0.19990718522480572, 0.9798148382707292], [-0.19990718522480572, 0.9798148382707292]], [[-0.1875666533758392, 0.9822518773417477], [-0.1875666533758392, 0.9822518773417477]], [[-0.17519644325187023, 0.9845334967749417], [-0.17519644325187023, 0.9845334967749417]], [[-0.16279851216509478, 0.9866593355544919], [-0.16279851216509478, 0.9866593355544919]], [[-0.1503748218139381, 0.9886290573134224], [-0.1503748218139381, 0.9886290573134224]], [[-0.1379273379726542, 0.9904423503868245], [-0.1379273379726542, 0.9904423503868245]], [[-0.12545803018029758, 0.9920989278611683], [-0.12545803018029758, 0.9920989278611683]], [[-0.11296887142907358, 0.9935985276197029], [-0.11296887142907358, 0.9935985276197029]], [[-0.10046183785216964, 0.9949409123839287], [-0.10046183785216964, 0.9949409123839287]], [[-0.08793890841106214, 0.9961258697511428], [-0.08793890841106214, 0.9961258697511428]], [[-0.07540206458240344, 0.9971532122280462], [-0.07540206458240344, 0.9971532122280462]], [[-0.06285329004448297, 0.9980227772604111], [-0.06285329004448297, 0.9980227772604111]], [[-0.05029457036336817, 0.9987344272588005], [-0.05029457036336817, 0.9987344272588005]], [[-0.037727892678718344, 0.99928804962034], [-0.037727892678718344, 0.99928804962034]], [[-0.025155245389377974, 0.9996835567465338], [-0.025155245389377974, 0.9996835567465338]], [[-0.012578617838742366, 0.9999208860571255], [-0.012578617838742366, 0.9999208860571255]], [[-4.898587196589413e-16, 1.0], [-4.898587196589413e-16, 1.0]]], "init_spikes": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], "n_neurons": 2} \ No newline at end of file +{"coef_": [[-0.004372, -0.02786, -0.04582, -0.0588, -0.06539, -0.06396, -0.05328, -0.03192, 0.0002296, 0.04143, 0.08794, 0.1483, 0.2053, 0.2483, 0.2892, 0.3093, 0.2917, 0.2225, 0.07357, -0.2711, -0.006235, -0.01047, 0.02189, 0.058, 0.09002, 0.1118, 0.1209, 0.1167, 0.09909, 0.07044, 0.03448, -0.01565, -0.06823, -0.1128, -0.1655, -0.2176, -0.2621, -0.2982, -0.3255, -0.3449, 0.5, 0.5], [-0.004637, 0.02223, 0.07071, 0.09572, 0.1012, 0.08923, 0.06464, 0.03076, -0.007911, -0.04737, -0.08429, -0.1249, -0.1582, -0.1827, -0.2081, -0.23, -0.2473, -0.2616, -0.2741, -0.287, 0.01127, 0.04864, 0.0544, 0.05082, 0.03975, 0.02393, 0.004725, -0.01763, -0.04202, -0.06744, -0.09269, -0.1231, -0.1522, -0.1763, -0.2051, -0.2348, -0.2629, -0.2896, -0.3149, -0.3389, 0.5, 0.5]], "coupling_basis": [[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0024979173609873673, 0.9975020826390129], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.11451325277931029, 0.8854867472206909, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.25013898844998006, 0.7498610115500185, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3122501403134024, 0.687749859686596, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.28176761370807446, 0.7182323862919272, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17383844924397923, 0.8261615507560222, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.04364762794083282, 0.9563523720591665, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9912618171282106, 0.008738182871789013, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7892946476427273, 0.21070535235727128, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.3531647741677867, 0.6468352258322151, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.011883820048045501, 0.9881161799519544, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7841665801263835, 0.21583341987361648, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17688067665784446, 0.8231193233421555, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9253003862638604, 0.0746996137361397, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2549435480705588, 0.7450564519294413, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.9205258993369989, 0.07947410066300109, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.16827351931758228, 0.8317264806824178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7835282009408713, 0.21647179905912872, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.019118847416525586, 0.9808811525834744, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.4372031242218587, 0.5627968757781414, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.0, 0.9120243919870162, 0.08797560801298382, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.044222034278324274, 0.9557779657216758, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.40793669708774605, 0.5920633029122541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.8283923698925478, 0.17160763010745222, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.0, 0.9999802058373224, 1.9794162677666538e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.1458111022283093, 0.8541888977716907, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.4778824971400245, 0.5221175028599756, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.803486827077907, 0.19651317292209308, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.0, 0.9824675828481839, 0.017532417151816082, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.029720664099906924, 0.9702793359000932, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.19724020774947038, 0.8027597922505296, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.44389603578613035, 0.5561039642138698, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.6909694421867117, 0.30903055781328825, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.8804498633788072, 0.1195501366211929, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.0, 0.9828262050955638, 0.017173794904436157, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.005816278861877466, 0.9941837211381226, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.07171948190677246, 0.9282805180932275, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.19211081158089233, 0.8078891884191077, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.3422365913893123, 0.6577634086106878, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.49997219806462273, 0.5000278019353773, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.6481581380891199, 0.3518418619108801, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.775227808426499, 0.22477219157350103, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.8747644272334134, 0.12523557276658664, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9445228823471115, 0.05547711765288865, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9852942394771702, 0.014705760522829736, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.0, 0.9998405276097415, 0.00015947239025848603, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.00798856965539202, 0.9920114303446079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.03392307742054024, 0.9660769225794598, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.07373523476821137, 0.9262647652317886, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.12352988337197751, 0.8764701166280225, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.17990211564285485, 0.8200978843571451, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.2399997347398921, 0.7600002652601079, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.3015222924967669, 0.6984777075032332, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.36268149196393995, 0.63731850803606, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.42214108290743424, 0.5778589170925659, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.47894873221112266, 0.5210512677888774, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5324679173051469, 0.46753208269485313, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.5823146093533313, 0.4176853906466687, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.6283012081735033, 0.3716987918264968, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.6703886551778314, 0.32961134482216864, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7086466881407022, 0.2913533118592979, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7432216468423799, 0.25677835315762026, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.7743109612271127, 0.22568903877288732, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.802143356101582, 0.197856643898418, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.82696381862707, 0.17303618137292998, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8490224486822571, 0.15097755131774288, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8685664156253453, 0.13143358437465474, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.8858343578296817, 0.11416564217031833, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9010526715389762, 0.09894732846102389, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9144332365128198, 0.08556676348718023, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9261722145965264, 0.07382778540347357, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9364496329422705, 0.06355036705772948, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9454295266061546, 0.05457047339384541, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9532604668007324, 0.04673953319926766, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9600763426393057, 0.039923657360694254, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9659972972699125, 0.03400270273008754, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.971130745291511, 0.028869254708488945, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.975572418558468, 0.024427581441531954, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9794074030288873, 0.020592596971112653, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9827111411428311, 0.017288858857168965, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9855503831123861, 0.014449616887613925, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9879840771076767, 0.012015922892323394, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9900641931482845, 0.009935806851715523, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9918364789707291, 0.008163521029270815, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9933411485659462, 0.006658851434053759, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9946135057219054, 0.005386494278094567, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9956845059646938, 0.004315494035306178, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9965812609202838, 0.0034187390797163486, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.997327489436671, 0.002672510563328956, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9979439199017871, 0.002056080098212898, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9984486481342357, 0.0015513518657642722, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9988574550621354, 0.0011425449378646424, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9991840881776304, 0.0008159118223696749, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.999440510488429, 0.0005594895115710874, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9996371204027914, 0.00036287959720865404, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.999782945694725, 0.00021705430527496627, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9998858144113889, 0.00011418558861114869, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.9999525053112863, 4.7494688713622946e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [0.99998888016377, 1.1119836230089053e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]], "feedforward_input": [[[0.0, 1.0], [0.0, 1.0]], [[0.012578617838741058, 0.9999208860571255], [0.012578617838741058, 0.9999208860571255]], [[0.025155245389375847, 0.9996835567465339], [0.025155245389375847, 0.9996835567465339]], [[0.03772789267871718, 0.99928804962034], [0.03772789267871718, 0.99928804962034]], [[0.05029457036336618, 0.9987344272588006], [0.05029457036336618, 0.9987344272588006]], [[0.06285329004448194, 0.9980227772604111], [0.06285329004448194, 0.9980227772604111]], [[0.07540206458240159, 0.9971532122280464], [0.07540206458240159, 0.9971532122280464]], [[0.08793890841106125, 0.9961258697511429], [0.08793890841106125, 0.9961258697511429]], [[0.10046183785216795, 0.9949409123839288], [0.10046183785216795, 0.9949409123839288]], [[0.11296887142907283, 0.9935985276197029], [0.11296887142907283, 0.9935985276197029]], [[0.12545803018029603, 0.9920989278611685], [0.12545803018029603, 0.9920989278611685]], [[0.13792733797265358, 0.9904423503868246], [0.13792733797265358, 0.9904423503868246]], [[0.1503748218139367, 0.9886290573134227], [0.1503748218139367, 0.9886290573134227]], [[0.1627985121650943, 0.986659335554492], [0.1627985121650943, 0.986659335554492]], [[0.17519644325186898, 0.984533496774942], [0.17519644325186898, 0.984533496774942]], [[0.18756665337583714, 0.9822518773417481], [0.18756665337583714, 0.9822518773417481]], [[0.19990718522480458, 0.9798148382707295], [0.19990718522480458, 0.9798148382707295]], [[0.21221608618250787, 0.9772227651694256], [0.21221608618250787, 0.9772227651694256]], [[0.22449140863757258, 0.9744760681760832], [0.22449140863757258, 0.9744760681760832]], [[0.23673121029167973, 0.9715751818947602], [0.23673121029167973, 0.9715751818947602]], [[0.2489335544668916, 0.9685205653265598], [0.2489335544668916, 0.9685205653265598]], [[0.2610965104120882, 0.9653127017970033], [0.2610965104120882, 0.9653127017970033]], [[0.27321815360846585, 0.9619520988795548], [0.27321815360846585, 0.9619520988795548]], [[0.28529656607404974, 0.9584392883153087], [0.28529656607404974, 0.9584392883153087]], [[0.2973298366671723, 0.9547748259288535], [0.2973298366671723, 0.9547748259288535]], [[0.30931606138886886, 0.9509592915403253], [0.30931606138886886, 0.9509592915403253]], [[0.32125334368414366, 0.9469932888736633], [0.32125334368414366, 0.9469932888736633]], [[0.33313979474205757, 0.9428774454610842], [0.33313979474205757, 0.9428774454610842]], [[0.34497353379459045, 0.9386124125437894], [0.34497353379459045, 0.9386124125437894]], [[0.3567526884142317, 0.9341988649689198], [0.3567526884142317, 0.9341988649689198]], [[0.3684753948102499, 0.9296375010827771], [0.3684753948102499, 0.9296375010827771]], [[0.38013979812359666, 0.924929042620325], [0.38013979812359666, 0.924929042620325]], [[0.3917440527203973, 0.9200742345909914], [0.3917440527203973, 0.9200742345909914]], [[0.4032863224839812, 0.915073845160786], [0.4032863224839812, 0.915073845160786]], [[0.41476478110540693, 0.9099286655307568], [0.41476478110540693, 0.9099286655307568]], [[0.4261776123724353, 0.9046395098117981], [0.4261776123724353, 0.9046395098117981]], [[0.4375230104569043, 0.8992072148958368], [0.4375230104569043, 0.8992072148958368]], [[0.4487991802004621, 0.8936326403234123], [0.4487991802004621, 0.8936326403234123]], [[0.46000433739861096, 0.887916668147673], [0.46000433739861096, 0.887916668147673]], [[0.47113670908301786, 0.8820602027948115], [0.47113670908301786, 0.8820602027948115]], [[0.4821945338020477, 0.8760641709209582], [0.4821945338020477, 0.8760641709209582]], [[0.4931760618994744, 0.8699295212655597], [0.4931760618994744, 0.8699295212655597]], [[0.5040795557913246, 0.8636572245012607], [0.5040795557913246, 0.8636572245012607]], [[0.5149032902408126, 0.8572482730803168], [0.5149032902408126, 0.8572482730803168]], [[0.5256455526313207, 0.8507036810775614], [0.5256455526313207, 0.8507036810775614]], [[0.5363046432373825, 0.8440244840299503], [0.5363046432373825, 0.8440244840299503]], [[0.5468788754936273, 0.8372117387727107], [0.5468788754936273, 0.8372117387727107]], [[0.5573665762616421, 0.8302665232721208], [0.5573665762616421, 0.8302665232721208]], [[0.5677660860947078, 0.8231899364549453], [0.5677660860947078, 0.8231899364549453]], [[0.5780757595003707, 0.8159830980345546], [0.5780757595003707, 0.8159830980345546]], [[0.588293965200805, 0.8086471483337551], [0.588293965200805, 0.8086471483337551]], [[0.5984190863909268, 0.8011832481043575], [0.5984190863909268, 0.8011832481043575]], [[0.608449520994217, 0.7935925783435149], [0.608449520994217, 0.7935925783435149]], [[0.6183836819162153, 0.7858763401068549], [0.6183836819162153, 0.7858763401068549]], [[0.6282199972956423, 0.7780357543184395], [0.6282199972956423, 0.7780357543184395]], [[0.6379569107531118, 0.7700720615775812], [0.6379569107531118, 0.7700720615775812]], [[0.647592881637394, 0.7619865219625451], [0.647592881637394, 0.7619865219625451]], [[0.6571263852691885, 0.7537804148311695], [0.6571263852691885, 0.7537804148311695]], [[0.666555913182372, 0.7454550386184362], [0.666555913182372, 0.7454550386184362]], [[0.675879973362679, 0.7370117106310213], [0.675879973362679, 0.7370117106310213]], [[0.6850970904837809, 0.7284517668388609], [0.6850970904837809, 0.7284517668388609]], [[0.6942058061407225, 0.7197765616637636], [0.6942058061407225, 0.7197765616637636]], [[0.7032046790806838, 0.7109874677651024], [0.7032046790806838, 0.7109874677651024]], [[0.7120922854310254, 0.7020858758226226], [0.7120922854310254, 0.7020858758226226]], [[0.720867218924585, 0.6930731943163971], [0.720867218924585, 0.6930731943163971]], [[0.7295280911221884, 0.6839508493039657], [0.7295280911221884, 0.6839508493039657]], [[0.7380735316323389, 0.6747202841946927], [0.7380735316323389, 0.6747202841946927]], [[0.746502188328052, 0.6653829595213794], [0.746502188328052, 0.6653829595213794]], [[0.7548127275607989, 0.6559403527091677], [0.7548127275607989, 0.6559403527091677]], [[0.7630038343715272, 0.6463939578417693], [0.7630038343715272, 0.6463939578417693]], [[0.7710742126987247, 0.6367452854250606], [0.7710742126987247, 0.6367452854250606]], [[0.7790225855834911, 0.6269958621480786], [0.7790225855834911, 0.6269958621480786]], [[0.7868476953715899, 0.6171472306414553], [0.7868476953715899, 0.6171472306414553]], [[0.7945483039124437, 0.6072009492333317], [0.7945483039124437, 0.6072009492333317]], [[0.8021231927550437, 0.5971585917027863], [0.8021231927550437, 0.5971585917027863]], [[0.809571163340744, 0.5870217470308187], [0.809571163340744, 0.5870217470308187]], [[0.8168910371929053, 0.5767920191489297], [0.8168910371929053, 0.5767920191489297]], [[0.8240816561033644, 0.566471026685334], [0.8240816561033644, 0.566471026685334]], [[0.8311418823156935, 0.5560604027088476], [0.8311418823156935, 0.5560604027088476]], [[0.8380705987052264, 0.545561794470492], [0.8380705987052264, 0.545561794470492]], [[0.8448667089558177, 0.5349768631428518], [0.8448667089558177, 0.5349768631428518]], [[0.8515291377333112, 0.5243072835572319], [0.8515291377333112, 0.5243072835572319]], [[0.8580568308556875, 0.5135547439386516], [0.8580568308556875, 0.5135547439386516]], [[0.8644487554598649, 0.5027209456387218], [0.8644487554598649, 0.5027209456387218]], [[0.8707039001651274, 0.4918076028664418], [0.8707039001651274, 0.4918076028664418]], [[0.8768212752331536, 0.4808164424169648], [0.8768212752331536, 0.4808164424169648]], [[0.8827999127246196, 0.4697492033983709], [0.8827999127246196, 0.4697492033983709]], [[0.8886388666523558, 0.45860763695649104], [0.8886388666523558, 0.45860763695649104]], [[0.8943372131310272, 0.4473935059978269], [0.8943372131310272, 0.4473935059978269]], [[0.8998940505233182, 0.4361085849106111], [0.8998940505233182, 0.4361085849106111]], [[0.9053084995825966, 0.42475465928404793], [0.9053084995825966, 0.42475465928404793]], [[0.9105797035920355, 0.4133335256257842], [0.9105797035920355, 0.4133335256257842]], [[0.9157068285001692, 0.4018469910776512], [0.9157068285001692, 0.4018469910776512]], [[0.920689063052863, 0.3902968731297256], [0.920689063052863, 0.3902968731297256]], [[0.9255256189216778, 0.3786849993327503], [0.9255256189216778, 0.3786849993327503]], [[0.9302157308286042, 0.3670132070089654], [0.9302157308286042, 0.3670132070089654]], [[0.934758656667151, 0.35528334296139374], [0.934758656667151, 0.35528334296139374]], [[0.9391536776197676, 0.34349726318162344], [0.9391536776197676, 0.34349726318162344]], [[0.9434000982715812, 0.3316568325561391], [0.9434000982715812, 0.3316568325561391]], [[0.9474972467204298, 0.31976392457124536], [0.9474972467204298, 0.31976392457124536]], [[0.9514444746831766, 0.30782042101662793], [0.9514444746831766, 0.30782042101662793]], [[0.9552411575982869, 0.2958282116876025], [0.9552411575982869, 0.2958282116876025]], [[0.9588866947246497, 0.28378919408609693], [0.9588866947246497, 0.28378919408609693]], [[0.9623805092366334, 0.27170527312041276], [0.9623805092366334, 0.27170527312041276]], [[0.9657220483153546, 0.25957836080381586], [0.9657220483153546, 0.25957836080381586]], [[0.9689107832361495, 0.24741037595200252], [0.9689107832361495, 0.24741037595200252]], [[0.9719462094522335, 0.23520324387949015], [0.9719462094522335, 0.23520324387949015]], [[0.9748278466745341, 0.2229588960949774], [0.9748278466745341, 0.2229588960949774]], [[0.9775552389476861, 0.21067926999572642], [0.9775552389476861, 0.21067926999572642]], [[0.9801279547221765, 0.19836630856101303], [0.9801279547221765, 0.19836630856101303]], [[0.9825455869226277, 0.18602196004469224], [0.9825455869226277, 0.18602196004469224]], [[0.984807753012208, 0.17364817766693041], [0.984807753012208, 0.17364817766693041]], [[0.98691409505316, 0.16124691930515242], [0.98691409505316, 0.16124691930515242]], [[0.9888642797634357, 0.14882014718424924], [0.9888642797634357, 0.14882014718424924]], [[0.9906579985694317, 0.1363698275661], [0.9906579985694317, 0.1363698275661]], [[0.9922949676548136, 0.12389793043845522], [0.9922949676548136, 0.12389793043845522]], [[0.9937749280054242, 0.11140642920322849], [0.9937749280054242, 0.11140642920322849]], [[0.995097645450266, 0.09889730036424986], [0.995097645450266, 0.09889730036424986]], [[0.9962629106985543, 0.08637252321452853], [0.9962629106985543, 0.08637252321452853]], [[0.9972705393728327, 0.07383407952307214], [0.9972705393728327, 0.07383407952307214]], [[0.9981203720381463, 0.06128395322131638], [0.9981203720381463, 0.06128395322131638]], [[0.9988122742272691, 0.04872413008921228], [0.9988122742272691, 0.04872413008921228]], [[0.9993461364619809, 0.036156597441019206], [0.9993461364619809, 0.036156597441019206]], [[0.9997218742703887, 0.023583343810857166], [0.9997218742703887, 0.023583343810857166]], [[0.9999394282002937, 0.011006358638064812], [0.9999394282002937, 0.011006358638064812]], [[0.9999987638285974, -0.001572368047584414], [0.9999987638285974, -0.001572368047584414]], [[0.9998998717667489, -0.014150845940761853], [0.9998998717667489, -0.014150845940761853]], [[0.9996427676622299, -0.026727084775504745], [0.9996427676622299, -0.026727084775504745]], [[0.9992274921960794, -0.03929909464013115], [0.9992274921960794, -0.03929909464013115]], [[0.9986541110764565, -0.0518648862921008], [0.9986541110764565, -0.0518648862921008]], [[0.9979227150282433, -0.06442247147276806], [0.9979227150282433, -0.06442247147276806]], [[0.9970334197786902, -0.07696986322197923], [0.9970334197786902, -0.07696986322197923]], [[0.9959863660391044, -0.08950507619246638], [0.9959863660391044, -0.08950507619246638]], [[0.9947817194825853, -0.10202612696398403], [0.9947817194825853, -0.10202612696398403]], [[0.9934196707178107, -0.11453103435714077], [0.9934196707178107, -0.11453103435714077]], [[0.991900435258877, -0.12701781974687854], [0.991900435258877, -0.12701781974687854]], [[0.9902242534911986, -0.1394845073755453], [0.9902242534911986, -0.1394845073755453]], [[0.9883913906334728, -0.15192912466551547], [0.9883913906334728, -0.15192912466551547]], [[0.9864021366957146, -0.16434970253130593], [0.9864021366957146, -0.16434970253130593]], [[0.9842568064333687, -0.17674427569114137], [0.9842568064333687, -0.17674427569114137]], [[0.9819557392975067, -0.18911088297791617], [0.9819557392975067, -0.18911088297791617]], [[0.9794992993811165, -0.20144756764950503], [0.9794992993811165, -0.20144756764950503]], [[0.9768878753614926, -0.21375237769837538], [0.9768878753614926, -0.21375237769837538]], [[0.9741218804387363, -0.22602336616044894], [0.9741218804387363, -0.22602336616044894]], [[0.9712017522703763, -0.23825859142316483], [0.9712017522703763, -0.23825859142316483]], [[0.9681279529021188, -0.25045611753269825], [0.9681279529021188, -0.25045611753269825]], [[0.9649009686947391, -0.2626140145002818], [0.9649009686947391, -0.2626140145002818]], [[0.9615213102471255, -0.27473035860758266], [0.9615213102471255, -0.27473035860758266]], [[0.9579895123154889, -0.28680323271109], [0.9579895123154889, -0.28680323271109]], [[0.9543061337287488, -0.29883072654545967], [0.9543061337287488, -0.29883072654545967]], [[0.9504717573001116, -0.310810937025771], [0.9504717573001116, -0.310810937025771]], [[0.9464869897348526, -0.32274196854864906], [0.9464869897348526, -0.32274196854864906]], [[0.9423524615343186, -0.33462193329220136], [0.9423524615343186, -0.33462193329220136]], [[0.9380688268961659, -0.3464489515147234], [0.9380688268961659, -0.3464489515147234]], [[0.9336367636108462, -0.3582211518521272], [0.9336367636108462, -0.3582211518521272]], [[0.9290569729543628, -0.369936671614043], [0.9290569729543628, -0.369936671614043]], [[0.9243301795773085, -0.38159365707854837], [0.9243301795773085, -0.38159365707854837]], [[0.9194571313902055, -0.3931902637854787], [0.9194571313902055, -0.3931902637854787]], [[0.9144385994451658, -0.40472465682827324], [0.9144385994451658, -0.40472465682827324]], [[0.9092753778138886, -0.4161950111443075], [0.9092753778138886, -0.4161950111443075]], [[0.9039682834620162, -0.42759951180366895], [0.9039682834620162, -0.42759951180366895]], [[0.8985181561198674, -0.4389363542963303], [0.8985181561198674, -0.4389363542963303]], [[0.8929258581495686, -0.450203744817673], [0.8929258581495686, -0.450203744817673]], [[0.8871922744086043, -0.46139990055231683], [0.8871922744086043, -0.46139990055231683]], [[0.881318312109807, -0.47252304995621186], [0.881318312109807, -0.47252304995621186]], [[0.8753049006778131, -0.4835714330369443], [0.8753049006778131, -0.4835714330369443]], [[0.869152991601999, -0.4945433016322186], [0.869152991601999, -0.4945433016322186]], [[0.8628635582859312, -0.5054369196864643], [0.8628635582859312, -0.5054369196864643]], [[0.856437595893346, -0.5162505635255284], [0.856437595893346, -0.5162505635255284]], [[0.8498761211906867, -0.5269825221294092], [0.8498761211906867, -0.5269825221294092]], [[0.8431801723862224, -0.5376310974029872], [0.8431801723862224, -0.5376310974029872]], [[0.8363508089657762, -0.5481946044447097], [0.8363508089657762, -0.5481946044447097]], [[0.8293891115250829, -0.5586713718131919], [0.8293891115250829, -0.5586713718131919]], [[0.8222961815988096, -0.5690597417916836], [0.8222961815988096, -0.5690597417916836]], [[0.8150731414862624, -0.5793580706503667], [0.8150731414862624, -0.5793580706503667]], [[0.8077211340738071, -0.5895647289064391], [0.8077211340738071, -0.5895647289064391]], [[0.800241322654032, -0.5996781015819448], [0.800241322654032, -0.5996781015819448]], [[0.7926348907416848, -0.6096965884593069], [0.7926348907416848, -0.6096965884593069]], [[0.7849030418864046, -0.6196186043345285], [0.7849030418864046, -0.6196186043345285]], [[0.7770469994822886, -0.6294425792680156], [0.7770469994822886, -0.6294425792680156]], [[0.769068006574317, -0.6391669588329847], [0.769068006574317, -0.6391669588329847]], [[0.7609673256616678, -0.648790204361417], [0.7609673256616678, -0.648790204361417]], [[0.7527462384979551, -0.6583107931875185], [0.7527462384979551, -0.6583107931875185]], [[0.744406045888419, -0.6677272188886485], [0.744406045888419, -0.6677272188886485]], [[0.7359480674841035, -0.6770379915236763], [0.7359480674841035, -0.6770379915236763]], [[0.7273736415730488, -0.6862416378687335], [0.7273736415730488, -0.6862416378687335]], [[0.7186841248685385, -0.6953367016503177], [0.7186841248685385, -0.6953367016503177]], [[0.7098808922944289, -0.7043217437757161], [0.7098808922944289, -0.7043217437757161]], [[0.7009653367675978, -0.7131953425607098], [0.7009653367675978, -0.7131953425607098]], [[0.6919388689775463, -0.7219560939545244], [0.6919388689775463, -0.7219560939545244]], [[0.6828029171631891, -0.7306026117619886], [0.6828029171631891, -0.7306026117619886]], [[0.673558926886866, -0.739133527862871], [0.673558926886866, -0.739133527862871]], [[0.6642083608056142, -0.7475474924283534], [0.6642083608056142, -0.7475474924283534]], [[0.6547526984397353, -0.7558431741346118], [0.6547526984397353, -0.7558431741346118]], [[0.6451934359386937, -0.764019260373469], [0.6451934359386937, -0.764019260373469]], [[0.6355320858443845, -0.7720744574600859], [0.6355320858443845, -0.7720744574600859]], [[0.6257701768518059, -0.7800074908376582], [0.6257701768518059, -0.7800074908376582]], [[0.6159092535671797, -0.7878171052790867], [0.6159092535671797, -0.7878171052790867]], [[0.6059508762635484, -0.7955020650855897], [0.6059508762635484, -0.7955020650855897]], [[0.5958966206338979, -0.8030611542822255], [0.5958966206338979, -0.8030611542822255]], [[0.5857480775418397, -0.8104931768102919], [0.5857480775418397, -0.8104931768102919]], [[0.5755068527698903, -0.8177969567165775], [0.5755068527698903, -0.8177969567165775]], [[0.5651745667653929, -0.8249713383394301], [0.5651745667653929, -0.8249713383394301]], [[0.5547528543841173, -0.8320151864916135], [0.5547528543841173, -0.8320151864916135]], [[0.5442433646315792, -0.8389273866399272], [0.5442433646315792, -0.8389273866399272]], [[0.5336477604021226, -0.8457068450815559], [0.5336477604021226, -0.8457068450815559]], [[0.5229677182158028, -0.8523524891171238], [0.5229677182158028, -0.8523524891171238]], [[0.5122049279531147, -0.8588632672204258], [0.5122049279531147, -0.8588632672204258]], [[0.5013610925876063, -0.865238149204808], [0.5013610925876063, -0.865238149204808]], [[0.49043792791642066, -0.8714761263861723], [0.49043792791642066, -0.8714761263861723]], [[0.47943716228880995, -0.8775762117425775], [0.47943716228880995, -0.8775762117425775]], [[0.4683605363326608, -0.8835374400704151], [0.4683605363326608, -0.8835374400704151]], [[0.4572098026790794, -0.8893588681371302], [0.4572098026790794, -0.8893588681371302]], [[0.44598672568507636, -0.8950395748304677], [0.44598672568507636, -0.8950395748304677]], [[0.4346930811543961, -0.9005786613042182], [0.4346930811543961, -0.9005786613042182]], [[0.4233306560565345, -0.9059752511204399], [0.4233306560565345, -0.9059752511204399]], [[0.4119012482439928, -0.9112284903881356], [0.4119012482439928, -0.9112284903881356]], [[0.40040666616780407, -0.916337547898363], [0.40040666616780407, -0.916337547898363]], [[0.3888487285913878, -0.9213016152557539], [0.3888487285913878, -0.9213016152557539]], [[0.37722926430277026, -0.9261199070064258], [0.37722926430277026, -0.9261199070064258]], [[0.36555011182521946, -0.9307916607622618], [0.36555011182521946, -0.9307916607622618]], [[0.3538131191263388, -0.9353161373215428], [0.3538131191263388, -0.9353161373215428]], [[0.3420201433256689, -0.9396926207859083], [0.3420201433256689, -0.9396926207859083]], [[0.330173050400837, -0.9439204186736329], [0.330173050400837, -0.9439204186736329]], [[0.3182737148923088, -0.9479988620291954], [0.3182737148923088, -0.9479988620291954]], [[0.3063240196067838, -0.9519273055291264], [0.3063240196067838, -0.9519273055291264]], [[0.29432585531928224, -0.9557051275841167], [0.29432585531928224, -0.9557051275841167]], [[0.2822811204739722, -0.9593317304373701], [0.2822811204739722, -0.9593317304373701]], [[0.27019172088378224, -0.9628065402591843], [0.27019172088378224, -0.9628065402591843]], [[0.25805956942885044, -0.9661290072377479], [0.25805956942885044, -0.9661290072377479]], [[0.24588658575385056, -0.9692986056661355], [0.24588658575385056, -0.9692986056661355]], [[0.23367469596425278, -0.9723148340254889], [0.23367469596425278, -0.9723148340254889]], [[0.22142583232155955, -0.975177215064372], [0.22142583232155955, -0.975177215064372]], [[0.20914193293756786, -0.977885295874285], [0.20914193293756786, -0.977885295874285]], [[0.19682494146770554, -0.9804386479613267], [0.19682494146770554, -0.9804386479613267]], [[0.18447680680349254, -0.9828368673139948], [0.18447680680349254, -0.9828368673139948]], [[0.17209948276416928, -0.9850795744671115], [0.17209948276416928, -0.9850795744671115]], [[0.15969492778754976, -0.9871664145618657], [0.15969492778754976, -0.9871664145618657]], [[0.14726510462014156, -0.9890970574019613], [0.14726510462014156, -0.9890970574019613]], [[0.1348119800065847, -0.9908711975058636], [0.1348119800065847, -0.9908711975058636]], [[0.12233752437845731, -0.992488554155135], [0.12233752437845731, -0.992488554155135]], [[0.1098437115425002, -0.9939488714388522], [0.1098437115425002, -0.9939488714388522]], [[0.09733251836830287, -0.9952519182940991], [0.09733251836830287, -0.9952519182940991]], [[0.0848059244755095, -0.9963974885425265], [0.0848059244755095, -0.9963974885425265]], [[0.07226591192058739, -0.9973854009229761], [0.07226591192058739, -0.9973854009229761]], [[0.05971446488321034, -0.9982154991201608], [0.05971446488321034, -0.9982154991201608]], [[0.04715356935230619, -0.9988876517893978], [0.04715356935230619, -0.9988876517893978]], [[0.034585212811817465, -0.9994017525773913], [0.034585212811817465, -0.9994017525773913]], [[0.022011383926227784, -0.9997577201390606], [0.022011383926227784, -0.9997577201390606]], [[0.009434072225897046, -0.999955498150411], [0.009434072225897046, -0.999955498150411]], [[-0.0031447322077359985, -0.9999950553174459], [-0.0031447322077359985, -0.9999950553174459]], [[-0.015723039057040564, -0.9998763853811183], [-0.015723039057040564, -0.9998763853811183]], [[-0.02829885808311759, -0.9995995071183217], [-0.02829885808311759, -0.9995995071183217]], [[-0.04087019944071145, -0.9991644643389178], [-0.04087019944071145, -0.9991644643389178]], [[-0.053435073993057226, -0.9985713258788059], [-0.053435073993057226, -0.9985713258788059]], [[-0.06599149362662023, -0.9978201855890307], [-0.06599149362662023, -0.9978201855890307]], [[-0.07853747156566927, -0.996911162320932], [-0.07853747156566927, -0.996911162320932]], [[-0.09107102268664041, -0.9958443999073396], [-0.09107102268664041, -0.9958443999073396]], [[-0.10359016383223883, -0.9946200671398149], [-0.10359016383223883, -0.9946200671398149]], [[-0.11609291412522968, -0.993238357741943], [-0.11609291412522968, -0.993238357741943]], [[-0.12857729528186848, -0.9916994903386808], [-0.12857729528186848, -0.9916994903386808]], [[-0.14104133192491908, -0.9900037084217639], [-0.14104133192491908, -0.9900037084217639]], [[-0.15348305189621594, -0.9881512803111796], [-0.15348305189621594, -0.9881512803111796]], [[-0.16590048656871298, -0.9861424991127116], [-0.16590048656871298, -0.9861424991127116]], [[-0.1782916711579755, -0.9839776826715616], [-0.1782916711579755, -0.9839776826715616]], [[-0.19065464503306404, -0.9816571735220583], [-0.19065464503306404, -0.9816571735220583]], [[-0.20298745202676116, -0.979181338833458], [-0.20298745202676116, -0.979181338833458]], [[-0.2152881407450901, -0.9765505703518493], [-0.2152881407450901, -0.9765505703518493]], [[-0.2275547648760821, -0.9737652843381669], [-0.2275547648760821, -0.9737652843381669]], [[-0.23978538349773562, -0.9708259215023277], [-0.23978538349773562, -0.9708259215023277]], [[-0.25197806138512474, -0.967732946933499], [-0.25197806138512474, -0.967732946933499]], [[-0.2641308693166058, -0.9644868500265071], [-0.2641308693166058, -0.9644868500265071]], [[-0.2762418843790738, -0.9610881444044029], [-0.2762418843790738, -0.9610881444044029]], [[-0.2883091902722216, -0.9575373678371909], [-0.2883091902722216, -0.9575373678371909]], [[-0.3003308776117502, -0.9538350821567405], [-0.3003308776117502, -0.9538350821567405]], [[-0.31230504423148914, -0.9499818731678872], [-0.31230504423148914, -0.9499818731678872]], [[-0.32422979548437053, -0.9459783505557425], [-0.32422979548437053, -0.9459783505557425]], [[-0.33610324454221563, -0.9418251477892251], [-0.33610324454221563, -0.9418251477892251]], [[-0.34792351269428334, -0.9375229220208277], [-0.34792351269428334, -0.9375229220208277]], [[-0.3596887296445355, -0.9330723539826374], [-0.3596887296445355, -0.9330723539826374]], [[-0.3713970338075679, -0.9284741478786258], [-0.3713970338075679, -0.9284741478786258]], [[-0.3830465726031674, -0.9237290312732227], [-0.3830465726031674, -0.9237290312732227]], [[-0.3946355027494405, -0.9188377549761962], [-0.3946355027494405, -0.9188377549761962]], [[-0.406161990554472, -0.9138010929238535], [-0.406161990554472, -0.9138010929238535]], [[-0.41762421220646645, -0.9086198420565822], [-0.41762421220646645, -0.9086198420565822]], [[-0.4290203540623263, -0.9032948221927524], [-0.4290203540623263, -0.9032948221927524]], [[-0.44034861293461913, -0.8978268758989992], [-0.44034861293461913, -0.8978268758989992]], [[-0.4516071963768948, -0.892216868356904], [-0.4516071963768948, -0.892216868356904]], [[-0.46279432296729867, -0.8864656872260989], [-0.46279432296729867, -0.8864656872260989]], [[-0.47390822259044274, -0.8805742425038149], [-0.47390822259044274, -0.8805742425038149]], [[-0.4849471367174873, -0.8745434663808944], [-0.4849471367174873, -0.8745434663808944]], [[-0.495909318684389, -0.8683743130942929], [-0.495909318684389, -0.8683743130942929]], [[-0.5067930339682724, -0.8620677587760915], [-0.5067930339682724, -0.8620677587760915]], [[-0.5175965604618782, -0.8556248012990468], [-0.5175965604618782, -0.8556248012990468]], [[-0.5283181887460511, -0.849046460118698], [-0.5283181887460511, -0.849046460118698]], [[-0.538956222360216, -0.842333776112062], [-0.538956222360216, -0.842333776112062]], [[-0.5495089780708056, -0.8354878114129367], [-0.5495089780708056, -0.8354878114129367]], [[-0.5599747861375949, -0.8285096492438424], [-0.5599747861375949, -0.8285096492438424]], [[-0.5703519905779012, -0.8214003937446254], [-0.5703519905779012, -0.8214003937446254]], [[-0.5806389494286053, -0.814161169797753], [-0.5806389494286053, -0.814161169797753]], [[-0.5908340350059578, -0.8067931228503245], [-0.5908340350059578, -0.8067931228503245]], [[-0.6009356341631226, -0.7992974187328304], [-0.6009356341631226, -0.7992974187328304]], [[-0.6109421485454225, -0.7916752434746857], [-0.6109421485454225, -0.7916752434746857]], [[-0.6208519948432432, -0.7839278031165661], [-0.6208519948432432, -0.7839278031165661]], [[-0.630663605042557, -0.7760563235195791], [-0.630663605042557, -0.7760563235195791]], [[-0.6403754266730258, -0.7680620501712998], [-0.6403754266730258, -0.7680620501712998]], [[-0.6499859230536464, -0.7599462479886977], [-0.6499859230536464, -0.7599462479886977]], [[-0.6594935735358957, -0.7517102011179935], [-0.6594935735358957, -0.7517102011179935]], [[-0.6688968737443391, -0.7433552127314704], [-0.6688968737443391, -0.7433552127314704]], [[-0.6781943358146659, -0.7348826048212762], [-0.6781943358146659, -0.7348826048212762]], [[-0.6873844886291098, -0.7262937179902474], [-0.6873844886291098, -0.7262937179902474]], [[-0.6964658780492216, -0.717589911239788], [-0.6964658780492216, -0.717589911239788]], [[-0.7054370671459529, -0.7087725617548385], [-0.7054370671459529, -0.7087725617548385]], [[-0.7142966364270207, -0.6998430646859656], [-0.7142966364270207, -0.6998430646859656]], [[-0.723043184061509, -0.6908028329286112], [-0.723043184061509, -0.6908028329286112]], [[-0.731675326101678, -0.6816532968995332], [-0.731675326101678, -0.6816532968995332]], [[-0.7401916967019432, -0.6723959043104729], [-0.7401916967019432, -0.6723959043104729]], [[-0.7485909483349904, -0.6630321199390868], [-0.7485909483349904, -0.6630321199390868]], [[-0.7568717520049916, -0.6535634253971795], [-0.7568717520049916, -0.6535634253971795]], [[-0.7650327974578898, -0.6439913188962686], [-0.7650327974578898, -0.6439913188962686]], [[-0.7730727933887175, -0.634317315010528], [-0.7730727933887175, -0.634317315010528]], [[-0.7809904676459172, -0.6245429444371393], [-0.7809904676459172, -0.6245429444371393]], [[-0.788784567432631, -0.6146697537540928], [-0.788784567432631, -0.6146697537540928]], [[-0.7964538595049286, -0.6046993051754759], [-0.7964538595049286, -0.6046993051754759]], [[-0.8039971303669401, -0.5946331763042871], [-0.8039971303669401, -0.5946331763042871]], [[-0.8114131864628653, -0.5844729598828156], [-0.8114131864628653, -0.5844729598828156]], [[-0.8187008543658276, -0.5742202635406243], [-0.8187008543658276, -0.5742202635406243]], [[-0.825858980963543, -0.5638767095401779], [-0.825858980963543, -0.5638767095401779]], [[-0.8328864336407734, -0.5534439345201586], [-0.8328864336407734, -0.5534439345201586]], [[-0.8397821004585396, -0.5429235892364995], [-0.8397821004585396, -0.5429235892364995]], [[-0.8465448903300604, -0.5323173383011922], [-0.8465448903300604, -0.5323173383011922]], [[-0.8531737331933926, -0.521626859918898], [-0.8531737331933926, -0.521626859918898]], [[-0.8596675801807451, -0.5108538456214089], [-0.8596675801807451, -0.5108538456214089]], [[-0.8660254037844384, -0.5000000000000004], [-0.8660254037844384, -0.5000000000000004]], [[-0.872246198019486, -0.4890670404357173], [-0.872246198019486, -0.4890670404357173]], [[-0.8783289785827684, -0.4780566968276366], [-0.8783289785827684, -0.4780566968276366]], [[-0.8842727830087774, -0.46697071131914863], [-0.8842727830087774, -0.46697071131914863]], [[-0.8900766708219056, -0.4558108380223019], [-0.8900766708219056, -0.4558108380223019]], [[-0.895739723685255, -0.4445788427402534], [-0.895739723685255, -0.4445788427402534]], [[-0.9012610455459443, -0.4332765026878693], [-0.9012610455459443, -0.4332765026878693]], [[-0.9066397627768893, -0.4219056062105194], [-0.9066397627768893, -0.4219056062105194]], [[-0.9118750243150336, -0.410467952501114], [-0.9118750243150336, -0.410467952501114]], [[-0.9169660017960133, -0.39896535131541655], [-0.9169660017960133, -0.39896535131541655]], [[-0.921911889685225, -0.38739962268569333], [-0.921911889685225, -0.38739962268569333]], [[-0.9267119054052849, -0.37577259663273255], [-0.9267119054052849, -0.37577259663273255]], [[-0.931365289459854, -0.3640861128762842], [-0.931365289459854, -0.3640861128762842]], [[-0.9358713055538119, -0.3523420205439648], [-0.9358713055538119, -0.3523420205439648]], [[-0.9402292407097588, -0.3405421778786742], [-0.9402292407097588, -0.3405421778786742]], [[-0.9444384053808287, -0.32868845194456947], [-0.9444384053808287, -0.32868845194456947]], [[-0.948498133559795, -0.3167827183316434], [-0.948498133559795, -0.3167827183316434]], [[-0.9524077828844512, -0.30482686085895394], [-0.9524077828844512, -0.30482686085895394]], [[-0.9561667347392507, -0.2928227712765512], [-0.9561667347392507, -0.2928227712765512]], [[-0.959774394353189, -0.28077234896614933], [-0.959774394353189, -0.28077234896614933]], [[-0.9632301908939126, -0.26867750064059465], [-0.9632301908939126, -0.26867750064059465]], [[-0.9665335775580413, -0.25654014004216524], [-0.9665335775580413, -0.25654014004216524]], [[-0.9696840316576876, -0.2443621876397672], [-0.9696840316576876, -0.2443621876397672]], [[-0.97268105470316, -0.2321455703250619], [-0.97268105470316, -0.2321455703250619]], [[-0.9755241724818386, -0.21989222110757806], [-0.9755241724818386, -0.21989222110757806]], [[-0.9782129351332083, -0.2076040788088557], [-0.9782129351332083, -0.2076040788088557]], [[-0.9807469172200395, -0.19528308775567055], [-0.9807469172200395, -0.19528308775567055]], [[-0.9831257177957041, -0.18293119747238726], [-0.9831257177957041, -0.18293119747238726]], [[-0.9853489604676163, -0.17055036237249038], [-0.9853489604676163, -0.17055036237249038]], [[-0.9874162934567888, -0.15814254144934156], [-0.9874162934567888, -0.15814254144934156]], [[-0.9893273896534934, -0.14570969796621222], [-0.9893273896534934, -0.14570969796621222]], [[-0.9910819466690195, -0.1332537991456406], [-0.9910819466690195, -0.1332537991456406]], [[-0.9926796868835203, -0.1207768158581612], [-0.9926796868835203, -0.1207768158581612]], [[-0.9941203574899392, -0.10828072231046196], [-0.9941203574899392, -0.10828072231046196]], [[-0.9954037305340125, -0.09576749573300417], [-0.9954037305340125, -0.09576749573300417]], [[-0.9965296029503367, -0.08323911606717305], [-0.9965296029503367, -0.08323911606717305]], [[-0.9974977965944997, -0.070697565651995], [-0.9974977965944997, -0.070697565651995]], [[-0.9983081582712682, -0.05814482891047624], [-0.9983081582712682, -0.05814482891047624]], [[-0.9989605597588274, -0.04558289203561173], [-0.9989605597588274, -0.04558289203561173]], [[-0.9994548978290693, -0.0330137426761141], [-0.9994548978290693, -0.0330137426761141]], [[-0.9997910942639261, -0.020439369621912166], [-0.9997910942639261, -0.020439369621912166]], [[-0.9999690958677468, -0.007861762489468911], [-0.9999690958677468, -0.007861762489468911]], [[-0.999988874475714, 0.004717088593031313], [-0.999988874475714, 0.004717088593031313]], [[-0.9998504269583004, 0.01729519330057657], [-0.9998504269583004, 0.01729519330057657]], [[-0.9995537752217639, 0.029870561426252256], [-0.9995537752217639, 0.029870561426252256]], [[-0.9990989662046815, 0.04244120319614822], [-0.9990989662046815, 0.04244120319614822]], [[-0.9984860718705224, 0.055005129584192916], [-0.9984860718705224, 0.055005129584192916]], [[-0.9977151891962615, 0.06756035262687816], [-0.9977151891962615, 0.06756035262687816]], [[-0.9967864401570343, 0.08010488573780679], [-0.9967864401570343, 0.08010488573780679]], [[-0.9956999717068378, 0.09263674402202696], [-0.9956999717068378, 0.09263674402202696]], [[-0.9944559557552776, 0.10515394459009784], [-0.9944559557552776, 0.10515394459009784]], [[-0.9930545891403677, 0.11765450687183807], [-0.9930545891403677, 0.11765450687183807]], [[-0.9914960935973849, 0.1301364529297071], [-0.9914960935973849, 0.1301364529297071]], [[-0.9897807157237836, 0.1425978077717702], [-0.9897807157237836, 0.1425978077717702]], [[-0.9879087269401782, 0.1550365996641971], [-0.9879087269401782, 0.1550365996641971]], [[-0.9858804234473959, 0.16745086044324545], [-0.9858804234473959, 0.16745086044324545]], [[-0.9836961261796103, 0.17983862582667898], [-0.9836961261796103, 0.17983862582667898]], [[-0.9813561807535597, 0.19219793572457194], [-0.9813561807535597, 0.19219793572457194]], [[-0.9788609574138615, 0.20452683454945075], [-0.9788609574138615, 0.20452683454945075]], [[-0.9762108509744296, 0.21682337152571898], [-0.9762108509744296, 0.21682337152571898]], [[-0.9734062807560028, 0.22908560099832972], [-0.9734062807560028, 0.22908560099832972]], [[-0.9704476905197971, 0.24131158274063894], [-0.9704476905197971, 0.24131158274063894]], [[-0.9673355483972903, 0.25349938226140434], [-0.9673355483972903, 0.25349938226140434]], [[-0.9640703468161508, 0.2656470711108758], [-0.9640703468161508, 0.2656470711108758]], [[-0.9606526024223212, 0.27775272718593], [-0.9606526024223212, 0.27775272718593]], [[-0.957082855998271, 0.28981443503420057], [-0.957082855998271, 0.28981443503420057]], [[-0.9533616723774295, 0.30183028615715607], [-0.9533616723774295, 0.30183028615715607]], [[-0.9494896403548136, 0.31379837931207794], [-0.9494896403548136, 0.31379837931207794]], [[-0.9454673725938637, 0.3257168208128897], [-0.9454673725938637, 0.3257168208128897]], [[-0.9412955055295036, 0.33758372482979143], [-0.9412955055295036, 0.33758372482979143]], [[-0.9369746992674384, 0.34939721368765], [-0.9369746992674384, 0.34939721368765]], [[-0.9325056374797075, 0.361155418163101], [-0.9325056374797075, 0.361155418163101]], [[-0.9278890272965095, 0.3728564777803084], [-0.9278890272965095, 0.3728564777803084]], [[-0.9231255991943125, 0.3844985411053488], [-0.9231255991943125, 0.3844985411053488]], [[-0.9182161068802741, 0.3960797660391565], [-0.9182161068802741, 0.3960797660391565]], [[-0.9131613271729835, 0.4075983201089958], [-0.9131613271729835, 0.4075983201089958]], [[-0.9079620598795464, 0.41905238075840945], [-0.9079620598795464, 0.41905238075840945]], [[-0.9026191276690343, 0.4304401356355976], [-0.9026191276690343, 0.4304401356355976]], [[-0.8971333759423143, 0.4417597828801825], [-0.8971333759423143, 0.4417597828801825]], [[-0.8915056726982842, 0.4530095314083134], [-0.8915056726982842, 0.4530095314083134]], [[-0.8857369083965297, 0.4641876011960654], [-0.8857369083965297, 0.4641876011960654]], [[-0.8798279958164298, 0.4752922235610892], [-0.8798279958164298, 0.4752922235610892]], [[-0.873779869912729, 0.486321641442466], [-0.873779869912729, 0.486321641442466]], [[-0.8675934876676018, 0.49727410967872326], [-0.8675934876676018, 0.49727410967872326]], [[-0.8612698279392309, 0.5081478952839691], [-0.8612698279392309, 0.5081478952839691]], [[-0.8548098913069261, 0.5189412777220956], [-0.8548098913069261, 0.5189412777220956]], [[-0.8482146999128025, 0.5296525491790203], [-0.8482146999128025, 0.5296525491790203]], [[-0.8414852973000504, 0.5402800148329067], [-0.8414852973000504, 0.5402800148329067]], [[-0.8346227482478176, 0.5508219931223336], [-0.8346227482478176, 0.5508219931223336]], [[-0.8276281386027314, 0.5612768160123647], [-0.8276281386027314, 0.5612768160123647]], [[-0.8205025751070878, 0.5716428292584782], [-0.8205025751070878, 0.5716428292584782]], [[-0.8132471852237334, 0.5819183926683146], [-0.8132471852237334, 0.5819183926683146]], [[-0.8058631169576695, 0.5921018803612005], [-0.8058631169576695, 0.5921018803612005]], [[-0.7983515386744064, 0.6021916810254089], [-0.7983515386744064, 0.6021916810254089]], [[-0.7907136389150943, 0.6121861981731129], [-0.7907136389150943, 0.6121861981731129]], [[-0.7829506262084637, 0.6220838503929953], [-0.7829506262084637, 0.6220838503929953]], [[-0.7750637288796017, 0.6318830716004721], [-0.7750637288796017, 0.6318830716004721]], [[-0.7670541948555989, 0.6415823112854881], [-0.7670541948555989, 0.6415823112854881]], [[-0.7589232914680891, 0.6511800347578556], [-0.7589232914680891, 0.6511800347578556]], [[-0.7506723052527245, 0.6606747233900812], [-0.7506723052527245, 0.6606747233900812]], [[-0.7423025417456096, 0.670064874857657], [-0.7423025417456096, 0.670064874857657]], [[-0.7338153252767281, 0.6793490033767694], [-0.7338153252767281, 0.6793490033767694]], [[-0.7252119987603977, 0.6885256399393918], [-0.7252119987603977, 0.6885256399393918]], [[-0.7164939234827836, 0.6975933325457224], [-0.7164939234827836, 0.6975933325457224]], [[-0.7076624788865049, 0.706550646433932], [-0.7076624788865049, 0.706550646433932]], [[-0.698719062352368, 0.7153961643071813], [-0.698719062352368, 0.7153961643071813]], [[-0.6896650889782625, 0.7241284865578796], [-0.6896650889782625, 0.7241284865578796]], [[-0.6805019913552531, 0.7327462314891391], [-0.6805019913552531, 0.7327462314891391]], [[-0.6712312193409035, 0.7412480355333995], [-0.6712312193409035, 0.7412480355333995]], [[-0.6618542398298681, 0.7496325534681825], [-0.6618542398298681, 0.7496325534681825]], [[-0.6523725365217912, 0.7578984586289408], [-0.6523725365217912, 0.7578984586289408]], [[-0.6427876096865396, 0.7660444431189778], [-0.6427876096865396, 0.7660444431189778]], [[-0.6331009759268216, 0.7740692180163904], [-0.6331009759268216, 0.7740692180163904]], [[-0.623314167938217, 0.7819715135780128], [-0.623314167938217, 0.7819715135780128]], [[-0.6134287342666622, 0.7897500794403256], [-0.6134287342666622, 0.7897500794403256]], [[-0.6034462390634266, 0.7974036848172986], [-0.6034462390634266, 0.7974036848172986]], [[-0.5933682618376209, 0.8049311186951345], [-0.5933682618376209, 0.8049311186951345]], [[-0.5831963972062739, 0.8123311900238854], [-0.5831963972062739, 0.8123311900238854]], [[-0.5729322546420206, 0.819602727905911], [-0.5729322546420206, 0.819602727905911]], [[-0.5625774582184379, 0.826744581781146], [-0.5625774582184379, 0.826744581781146]], [[-0.552133646353071, 0.8337556216091511], [-0.552133646353071, 0.8337556216091511]], [[-0.541602471548191, 0.8406347380479176], [-0.541602471548191, 0.8406347380479176]], [[-0.5309856001293205, 0.8473808426293961], [-0.5309856001293205, 0.8473808426293961]], [[-0.5202847119815792, 0.8539928679317206], [-0.5202847119815792, 0.8539928679317206]], [[-0.5095015002838734, 0.8604697677481075], [-0.5095015002838734, 0.8604697677481075]], [[-0.4986376712409919, 0.8668105172523927], [-0.4986376712409919, 0.8668105172523927]], [[-0.487694943813635, 0.8730141131611879], [-0.487694943813635, 0.8730141131611879]], [[-0.47667504944642797, 0.8790795738926286], [-0.47667504944642797, 0.8790795738926286]], [[-0.4655797317939577, 0.8850059397216871], [-0.4655797317939577, 0.8850059397216871]], [[-0.45441074644487806, 0.890792272932028], [-0.45441074644487806, 0.890792272932028]], [[-0.4431698606441268, 0.8964376579643814], [-0.4431698606441268, 0.8964376579643814]], [[-0.4318588530132981, 0.9019412015614092], [-0.4318588530132981, 0.9019412015614092]], [[-0.4204795132692152, 0.907302032909044], [-0.4204795132692152, 0.907302032909044]], [[-0.4090336419407468, 0.9125193037742757], [-0.4090336419407468, 0.9125193037742757]], [[-0.3975230500839139, 0.9175921886393661], [-0.3975230500839139, 0.9175921886393661]], [[-0.38594955899532896, 0.9225198848324686], [-0.38594955899532896, 0.9225198848324686]], [[-0.3743149999240192, 0.9273016126546322], [-0.3743149999240192, 0.9273016126546322]], [[-0.3626212137816673, 0.9319366155031737], [-0.3626212137816673, 0.9319366155031737]], [[-0.35087005085133094, 0.9364241599913922], [-0.35087005085133094, 0.9364241599913922]], [[-0.3390633704946757, 0.9407635360646108], [-0.3390633704946757, 0.9407635360646108]], [[-0.3272030408577722, 0.9449540571125281], [-0.3272030408577722, 0.9449540571125281]], [[-0.3152909385755031, 0.9489950600778585], [-0.3152909385755031, 0.9489950600778585]], [[-0.3033289484746273, 0.9528859055612465], [-0.3033289484746273, 0.9528859055612465]], [[-0.29131896327554796, 0.9566259779224375], [-0.29131896327554796, 0.9566259779224375]], [[-0.2792628832928309, 0.9602146853776892], [-0.2792628832928309, 0.9602146853776892]], [[-0.26716261613452225, 0.9636514600934084], [-0.26716261613452225, 0.9636514600934084]], [[-0.25502007640031144, 0.9669357582759981], [-0.25502007640031144, 0.9669357582759981]], [[-0.24283718537858734, 0.9700670602579007], [-0.24283718537858734, 0.9700670602579007]], [[-0.23061587074244044, 0.9730448705798238], [-0.23061587074244044, 0.9730448705798238]], [[-0.21835806624464577, 0.975868718069136], [-0.21835806624464577, 0.975868718069136]], [[-0.20606571141169297, 0.9785381559144195], [-0.20606571141169297, 0.9785381559144195]], [[-0.19374075123689813, 0.981052761736168], [-0.19374075123689813, 0.981052761736168]], [[-0.18138513587265162, 0.9834121376536186], [-0.18138513587265162, 0.9834121376536186]], [[-0.16900082032184968, 0.9856159103477083], [-0.16900082032184968, 0.9856159103477083]], [[-0.15658976412855838, 0.9876637311201432], [-0.15658976412855838, 0.9876637311201432]], [[-0.14415393106795907, 0.9895552759485718], [-0.14415393106795907, 0.9895552759485718]], [[-0.13169528883562445, 0.9912902455378553], [-0.13169528883562445, 0.9912902455378553]], [[-0.11921580873617425, 0.9928683653674237], [-0.11921580873617425, 0.9928683653674237]], [[-0.10671746537135988, 0.9942893857347128], [-0.10671746537135988, 0.9942893857347128]], [[-0.0942022363276273, 0.9955530817946745], [-0.0942022363276273, 0.9955530817946745]], [[-0.08167210186320688, 0.9966592535953529], [-0.08167210186320688, 0.9966592535953529]], [[-0.06912904459478485, 0.9976077261095226], [-0.06912904459478485, 0.9976077261095226]], [[-0.056575049183792726, 0.998398349262383], [-0.056575049183792726, 0.998398349262383]], [[-0.04401210202238211, 0.9990309979553044], [-0.04401210202238211, 0.9990309979553044]], [[-0.031442190919121114, 0.9995055720856215], [-0.031442190919121114, 0.9995055720856215]], [[-0.018867304784467676, 0.9998219965624732], [-0.018867304784467676, 0.9998219965624732]], [[-0.006289433316068405, 0.9999802213186832], [-0.006289433316068405, 0.9999802213186832]], [[0.006289433316067026, 0.9999802213186832], [0.006289433316067026, 0.9999802213186832]], [[0.0188673047844663, 0.9998219965624732], [0.0188673047844663, 0.9998219965624732]], [[0.03144219091911974, 0.9995055720856215], [0.03144219091911974, 0.9995055720856215]], [[0.04401210202238073, 0.9990309979553045], [0.04401210202238073, 0.9990309979553045]], [[0.056575049183791346, 0.9983983492623831], [0.056575049183791346, 0.9983983492623831]], [[0.06912904459478347, 0.9976077261095226], [0.06912904459478347, 0.9976077261095226]], [[0.08167210186320639, 0.9966592535953529], [0.08167210186320639, 0.9966592535953529]], [[0.09420223632762592, 0.9955530817946746], [0.09420223632762592, 0.9955530817946746]], [[0.10671746537135851, 0.994289385734713], [0.10671746537135851, 0.994289385734713]], [[0.11921580873617288, 0.9928683653674238], [0.11921580873617288, 0.9928683653674238]], [[0.13169528883562306, 0.9912902455378555], [0.13169528883562306, 0.9912902455378555]], [[0.14415393106795768, 0.9895552759485721], [0.14415393106795768, 0.9895552759485721]], [[0.15658976412855702, 0.9876637311201434], [0.15658976412855702, 0.9876637311201434]], [[0.16900082032184832, 0.9856159103477086], [0.16900082032184832, 0.9856159103477086]], [[0.18138513587265026, 0.9834121376536189], [0.18138513587265026, 0.9834121376536189]], [[0.19374075123689677, 0.9810527617361683], [0.19374075123689677, 0.9810527617361683]], [[0.2060657114116916, 0.9785381559144198], [0.2060657114116916, 0.9785381559144198]], [[0.21835806624464443, 0.9758687180691363], [0.21835806624464443, 0.9758687180691363]], [[0.2306158707424391, 0.9730448705798241], [0.2306158707424391, 0.9730448705798241]], [[0.24283718537858687, 0.9700670602579009], [0.24283718537858687, 0.9700670602579009]], [[0.2550200764003101, 0.9669357582759984], [0.2550200764003101, 0.9669357582759984]], [[0.2671626161345209, 0.9636514600934087], [0.2671626161345209, 0.9636514600934087]], [[0.2792628832928296, 0.9602146853776896], [0.2792628832928296, 0.9602146853776896]], [[0.2913189632755466, 0.956625977922438], [0.2913189632755466, 0.956625977922438]], [[0.30332894847462605, 0.952885905561247], [0.30332894847462605, 0.952885905561247]], [[0.3152909385755018, 0.9489950600778589], [0.3152909385755018, 0.9489950600778589]], [[0.3272030408577709, 0.9449540571125286], [0.3272030408577709, 0.9449540571125286]], [[0.33906337049467444, 0.9407635360646113], [0.33906337049467444, 0.9407635360646113]], [[0.3508700508513296, 0.9364241599913926], [0.3508700508513296, 0.9364241599913926]], [[0.36262121378166595, 0.9319366155031743], [0.36262121378166595, 0.9319366155031743]], [[0.3743149999240179, 0.9273016126546327], [0.3743149999240179, 0.9273016126546327]], [[0.3859495589953277, 0.9225198848324692], [0.3859495589953277, 0.9225198848324692]], [[0.39752305008391264, 0.9175921886393666], [0.39752305008391264, 0.9175921886393666]], [[0.40903364194074554, 0.9125193037742763], [0.40903364194074554, 0.9125193037742763]], [[0.4204795132692139, 0.9073020329090445], [0.4204795132692139, 0.9073020329090445]], [[0.4318588530132969, 0.9019412015614098], [0.4318588530132969, 0.9019412015614098]], [[0.44316986064412556, 0.896437657964382], [0.44316986064412556, 0.896437657964382]], [[0.45441074644487683, 0.8907922729320287], [0.45441074644487683, 0.8907922729320287]], [[0.46557973179395645, 0.8850059397216877], [0.46557973179395645, 0.8850059397216877]], [[0.47667504944642675, 0.8790795738926293], [0.47667504944642675, 0.8790795738926293]], [[0.48769494381363376, 0.8730141131611886], [0.48769494381363376, 0.8730141131611886]], [[0.4986376712409907, 0.8668105172523933], [0.4986376712409907, 0.8668105172523933]], [[0.5095015002838723, 0.8604697677481082], [0.5095015002838723, 0.8604697677481082]], [[0.520284711981578, 0.8539928679317214], [0.520284711981578, 0.8539928679317214]], [[0.5309856001293194, 0.8473808426293968], [0.5309856001293194, 0.8473808426293968]], [[0.5416024715481897, 0.8406347380479183], [0.5416024715481897, 0.8406347380479183]], [[0.5521336463530699, 0.8337556216091518], [0.5521336463530699, 0.8337556216091518]], [[0.5625774582184366, 0.8267445817811466], [0.5625774582184366, 0.8267445817811466]], [[0.5729322546420195, 0.8196027279059118], [0.5729322546420195, 0.8196027279059118]], [[0.5831963972062728, 0.8123311900238863], [0.5831963972062728, 0.8123311900238863]], [[0.5933682618376198, 0.8049311186951352], [0.5933682618376198, 0.8049311186951352]], [[0.6034462390634255, 0.7974036848172994], [0.6034462390634255, 0.7974036848172994]], [[0.6134287342666611, 0.7897500794403265], [0.6134287342666611, 0.7897500794403265]], [[0.6233141679382159, 0.7819715135780135], [0.6233141679382159, 0.7819715135780135]], [[0.6331009759268206, 0.7740692180163913], [0.6331009759268206, 0.7740692180163913]], [[0.6427876096865385, 0.7660444431189787], [0.6427876096865385, 0.7660444431189787]], [[0.6523725365217901, 0.7578984586289417], [0.6523725365217901, 0.7578984586289417]], [[0.6618542398298678, 0.7496325534681827], [0.6618542398298678, 0.7496325534681827]], [[0.6712312193409025, 0.7412480355334005], [0.6712312193409025, 0.7412480355334005]], [[0.6805019913552521, 0.7327462314891401], [0.6805019913552521, 0.7327462314891401]], [[0.6896650889782615, 0.7241284865578805], [0.6896650889782615, 0.7241284865578805]], [[0.698719062352367, 0.7153961643071823], [0.698719062352367, 0.7153961643071823]], [[0.7076624788865039, 0.7065506464339328], [0.7076624788865039, 0.7065506464339328]], [[0.7164939234827827, 0.6975933325457234], [0.7164939234827827, 0.6975933325457234]], [[0.7252119987603968, 0.6885256399393928], [0.7252119987603968, 0.6885256399393928]], [[0.7338153252767271, 0.6793490033767704], [0.7338153252767271, 0.6793490033767704]], [[0.7423025417456087, 0.670064874857658], [0.7423025417456087, 0.670064874857658]], [[0.7506723052527237, 0.6606747233900823], [0.7506723052527237, 0.6606747233900823]], [[0.7589232914680881, 0.6511800347578566], [0.7589232914680881, 0.6511800347578566]], [[0.767054194855598, 0.6415823112854891], [0.767054194855598, 0.6415823112854891]], [[0.7750637288796014, 0.6318830716004724], [0.7750637288796014, 0.6318830716004724]], [[0.7829506262084629, 0.6220838503929964], [0.7829506262084629, 0.6220838503929964]], [[0.7907136389150935, 0.612186198173114], [0.7907136389150935, 0.612186198173114]], [[0.7983515386744056, 0.60219168102541], [0.7983515386744056, 0.60219168102541]], [[0.8058631169576688, 0.5921018803612016], [0.8058631169576688, 0.5921018803612016]], [[0.8132471852237325, 0.5819183926683157], [0.8132471852237325, 0.5819183926683157]], [[0.820502575107087, 0.5716428292584793], [0.820502575107087, 0.5716428292584793]], [[0.8276281386027308, 0.5612768160123658], [0.8276281386027308, 0.5612768160123658]], [[0.8346227482478168, 0.5508219931223347], [0.8346227482478168, 0.5508219931223347]], [[0.8414852973000496, 0.5402800148329078], [0.8414852973000496, 0.5402800148329078]], [[0.8482146999128017, 0.5296525491790214], [0.8482146999128017, 0.5296525491790214]], [[0.8548098913069254, 0.5189412777220967], [0.8548098913069254, 0.5189412777220967]], [[0.8612698279392301, 0.5081478952839703], [0.8612698279392301, 0.5081478952839703]], [[0.8675934876676011, 0.49727410967872443], [0.8675934876676011, 0.49727410967872443]], [[0.8737798699127283, 0.48632164144246715], [0.8737798699127283, 0.48632164144246715]], [[0.8798279958164291, 0.4752922235610904], [0.8798279958164291, 0.4752922235610904]], [[0.8857369083965291, 0.4641876011960666], [0.8857369083965291, 0.4641876011960666]], [[0.8915056726982836, 0.4530095314083147], [0.8915056726982836, 0.4530095314083147]], [[0.8971333759423138, 0.4417597828801838], [0.8971333759423138, 0.4417597828801838]], [[0.9026191276690336, 0.43044013563559885], [0.9026191276690336, 0.43044013563559885]], [[0.9079620598795458, 0.4190523807584107], [0.9079620598795458, 0.4190523807584107]], [[0.9131613271729829, 0.4075983201089971], [0.9131613271729829, 0.4075983201089971]], [[0.9182161068802737, 0.39607976603915773], [0.9182161068802737, 0.39607976603915773]], [[0.9231255991943119, 0.3844985411053501], [0.9231255991943119, 0.3844985411053501]], [[0.9278890272965089, 0.37285647778030967], [0.9278890272965089, 0.37285647778030967]], [[0.932505637479707, 0.36115541816310226], [0.932505637479707, 0.36115541816310226]], [[0.9369746992674379, 0.3493972136876513], [0.9369746992674379, 0.3493972136876513]], [[0.9412955055295031, 0.3375837248297927], [0.9412955055295031, 0.3375837248297927]], [[0.9454673725938633, 0.32571682081289105], [0.9454673725938633, 0.32571682081289105]], [[0.9494896403548132, 0.3137983793120792], [0.9494896403548132, 0.3137983793120792]], [[0.9533616723774291, 0.3018302861571574], [0.9533616723774291, 0.3018302861571574]], [[0.9570828559982706, 0.2898144350342019], [0.9570828559982706, 0.2898144350342019]], [[0.9606526024223209, 0.27775272718593136], [0.9606526024223209, 0.27775272718593136]], [[0.9640703468161504, 0.26564707111087715], [0.9640703468161504, 0.26564707111087715]], [[0.96733554839729, 0.25349938226140567], [0.96733554839729, 0.25349938226140567]], [[0.9704476905197967, 0.24131158274064027], [0.9704476905197967, 0.24131158274064027]], [[0.9734062807560024, 0.22908560099833106], [0.9734062807560024, 0.22908560099833106]], [[0.9762108509744293, 0.21682337152572034], [0.9762108509744293, 0.21682337152572034]], [[0.9788609574138614, 0.20452683454945125], [0.9788609574138614, 0.20452683454945125]], [[0.9813561807535595, 0.1921979357245733], [0.9813561807535595, 0.1921979357245733]], [[0.98369612617961, 0.17983862582668034], [0.98369612617961, 0.17983862582668034]], [[0.9858804234473957, 0.1674508604432468], [0.9858804234473957, 0.1674508604432468]], [[0.987908726940178, 0.15503659966419847], [0.987908726940178, 0.15503659966419847]], [[0.9897807157237833, 0.14259780777177156], [0.9897807157237833, 0.14259780777177156]], [[0.9914960935973847, 0.13013645292970846], [0.9914960935973847, 0.13013645292970846]], [[0.9930545891403676, 0.11765450687183943], [0.9930545891403676, 0.11765450687183943]], [[0.9944559557552775, 0.1051539445900992], [0.9944559557552775, 0.1051539445900992]], [[0.9956999717068375, 0.09263674402202833], [0.9956999717068375, 0.09263674402202833]], [[0.9967864401570342, 0.08010488573780816], [0.9967864401570342, 0.08010488573780816]], [[0.9977151891962615, 0.06756035262687954], [0.9977151891962615, 0.06756035262687954]], [[0.9984860718705224, 0.05500512958419429], [0.9984860718705224, 0.05500512958419429]], [[0.9990989662046814, 0.042441203196148705], [0.9990989662046814, 0.042441203196148705]], [[0.9995537752217638, 0.029870561426253633], [0.9995537752217638, 0.029870561426253633]], [[0.9998504269583004, 0.01729519330057795], [0.9998504269583004, 0.01729519330057795]], [[0.999988874475714, 0.004717088593032691], [0.999988874475714, 0.004717088593032691]], [[0.999969095867747, -0.007861762489467534], [0.999969095867747, -0.007861762489467534]], [[0.9997910942639262, -0.020439369621910786], [0.9997910942639262, -0.020439369621910786]], [[0.9994548978290694, -0.03301374267611272], [0.9994548978290694, -0.03301374267611272]], [[0.9989605597588275, -0.045582892035610355], [0.9989605597588275, -0.045582892035610355]], [[0.9983081582712683, -0.058144828910474865], [0.9983081582712683, -0.058144828910474865]], [[0.9974977965944998, -0.07069756565199363], [0.9974977965944998, -0.07069756565199363]], [[0.9965296029503368, -0.08323911606717167], [0.9965296029503368, -0.08323911606717167]], [[0.9954037305340127, -0.09576749573300279], [0.9954037305340127, -0.09576749573300279]], [[0.9941203574899394, -0.1082807223104606], [0.9941203574899394, -0.1082807223104606]], [[0.9926796868835203, -0.12077681585816072], [0.9926796868835203, -0.12077681585816072]], [[0.9910819466690197, -0.1332537991456392], [0.9910819466690197, -0.1332537991456392]], [[0.9893273896534936, -0.14570969796621086], [0.9893273896534936, -0.14570969796621086]], [[0.9874162934567892, -0.1581425414493393], [0.9874162934567892, -0.1581425414493393]], [[0.9853489604676167, -0.17055036237248902], [0.9853489604676167, -0.17055036237248902]], [[0.9831257177957046, -0.18293119747238504], [0.9831257177957046, -0.18293119747238504]], [[0.9807469172200398, -0.1952830877556692], [0.9807469172200398, -0.1952830877556692]], [[0.9782129351332084, -0.2076040788088552], [0.9782129351332084, -0.2076040788088552]], [[0.9755241724818389, -0.2198922211075767], [0.9755241724818389, -0.2198922211075767]], [[0.9726810547031601, -0.23214557032506142], [0.9726810547031601, -0.23214557032506142]], [[0.9696840316576879, -0.24436218763976586], [0.9696840316576879, -0.24436218763976586]], [[0.9665335775580415, -0.25654014004216474], [0.9665335775580415, -0.25654014004216474]], [[0.9632301908939129, -0.2686775006405933], [0.9632301908939129, -0.2686775006405933]], [[0.9597743943531892, -0.2807723489661489], [0.9597743943531892, -0.2807723489661489]], [[0.9561667347392514, -0.29282277127654904], [0.9561667347392514, -0.29282277127654904]], [[0.9524077828844516, -0.3048268608589526], [0.9524077828844516, -0.3048268608589526]], [[0.9484981335597957, -0.3167827183316413], [0.9484981335597957, -0.3167827183316413]], [[0.9444384053808291, -0.32868845194456814], [0.9444384053808291, -0.32868845194456814]], [[0.9402292407097596, -0.340542177878672], [0.9402292407097596, -0.340542177878672]], [[0.9358713055538124, -0.3523420205439635], [0.9358713055538124, -0.3523420205439635]], [[0.9313652894598542, -0.36408611287628373], [0.9313652894598542, -0.36408611287628373]], [[0.9267119054052854, -0.37577259663273127], [0.9267119054052854, -0.37577259663273127]], [[0.9219118896852252, -0.38739962268569283], [0.9219118896852252, -0.38739962268569283]], [[0.9169660017960138, -0.3989653513154153], [0.9169660017960138, -0.3989653513154153]], [[0.9118750243150339, -0.4104679525011135], [0.9118750243150339, -0.4104679525011135]], [[0.9066397627768898, -0.4219056062105182], [0.9066397627768898, -0.4219056062105182]], [[0.901261045545945, -0.4332765026878681], [0.901261045545945, -0.4332765026878681]], [[0.895739723685256, -0.44457884274025133], [0.895739723685256, -0.44457884274025133]], [[0.8900766708219062, -0.45581083802230066], [0.8900766708219062, -0.45581083802230066]], [[0.8842727830087785, -0.46697071131914664], [0.8842727830087785, -0.46697071131914664]], [[0.878328978582769, -0.47805669682763535], [0.878328978582769, -0.47805669682763535]], [[0.8722461980194871, -0.48906704043571536], [0.8722461980194871, -0.48906704043571536]], [[0.8660254037844392, -0.4999999999999992], [0.8660254037844392, -0.4999999999999992]], [[0.8596675801807453, -0.5108538456214086], [0.8596675801807453, -0.5108538456214086]], [[0.8531737331933934, -0.5216268599188969], [0.8531737331933934, -0.5216268599188969]], [[0.8465448903300608, -0.5323173383011919], [0.8465448903300608, -0.5323173383011919]], [[0.8397821004585404, -0.5429235892364983], [0.8397821004585404, -0.5429235892364983]], [[0.8328864336407736, -0.5534439345201582], [0.8328864336407736, -0.5534439345201582]], [[0.8258589809635439, -0.5638767095401768], [0.8258589809635439, -0.5638767095401768]], [[0.8187008543658284, -0.5742202635406232], [0.8187008543658284, -0.5742202635406232]], [[0.8114131864628666, -0.5844729598828138], [0.8114131864628666, -0.5844729598828138]], [[0.803997130366941, -0.5946331763042861], [0.803997130366941, -0.5946331763042861]], [[0.7964538595049301, -0.6046993051754741], [0.7964538595049301, -0.6046993051754741]], [[0.7887845674326319, -0.6146697537540917], [0.7887845674326319, -0.6146697537540917]], [[0.7809904676459185, -0.6245429444371375], [0.7809904676459185, -0.6245429444371375]], [[0.7730727933887184, -0.6343173150105269], [0.7730727933887184, -0.6343173150105269]], [[0.76503279745789, -0.6439913188962683], [0.76503279745789, -0.6439913188962683]], [[0.7568717520049925, -0.6535634253971785], [0.7568717520049925, -0.6535634253971785]], [[0.7485909483349908, -0.6630321199390865], [0.7485909483349908, -0.6630321199390865]], [[0.7401916967019444, -0.6723959043104716], [0.7401916967019444, -0.6723959043104716]], [[0.7316753261016786, -0.6816532968995326], [0.7316753261016786, -0.6816532968995326]], [[0.7230431840615102, -0.69080283292861], [0.7230431840615102, -0.69080283292861]], [[0.7142966364270213, -0.6998430646859649], [0.7142966364270213, -0.6998430646859649]], [[0.7054370671459542, -0.7087725617548373], [0.7054370671459542, -0.7087725617548373]], [[0.6964658780492222, -0.7175899112397874], [0.6964658780492222, -0.7175899112397874]], [[0.6873844886291115, -0.7262937179902459], [0.6873844886291115, -0.7262937179902459]], [[0.678194335814667, -0.7348826048212753], [0.678194335814667, -0.7348826048212753]], [[0.6688968737443408, -0.7433552127314689], [0.6688968737443408, -0.7433552127314689]], [[0.6594935735358967, -0.7517102011179926], [0.6594935735358967, -0.7517102011179926]], [[0.6499859230536468, -0.7599462479886974], [0.6499859230536468, -0.7599462479886974]], [[0.6403754266730268, -0.7680620501712988], [0.6403754266730268, -0.7680620501712988]], [[0.6306636050425575, -0.7760563235195788], [0.6306636050425575, -0.7760563235195788]], [[0.6208519948432446, -0.7839278031165648], [0.6208519948432446, -0.7839278031165648]], [[0.6109421485454233, -0.7916752434746851], [0.6109421485454233, -0.7916752434746851]], [[0.600935634163124, -0.7992974187328293], [0.600935634163124, -0.7992974187328293]], [[0.5908340350059585, -0.8067931228503239], [0.5908340350059585, -0.8067931228503239]], [[0.5806389494286068, -0.8141611697977519], [0.5806389494286068, -0.8141611697977519]], [[0.570351990577902, -0.8214003937446248], [0.570351990577902, -0.8214003937446248]], [[0.5599747861375968, -0.8285096492438412], [0.5599747861375968, -0.8285096492438412]], [[0.5495089780708068, -0.8354878114129359], [0.5495089780708068, -0.8354878114129359]], [[0.5389562223602165, -0.8423337761120617], [0.5389562223602165, -0.8423337761120617]], [[0.5283181887460523, -0.8490464601186973], [0.5283181887460523, -0.8490464601186973]], [[0.5175965604618786, -0.8556248012990465], [0.5175965604618786, -0.8556248012990465]], [[0.5067930339682736, -0.8620677587760909], [0.5067930339682736, -0.8620677587760909]], [[0.49590931868438975, -0.8683743130942925], [0.49590931868438975, -0.8683743130942925]], [[0.4849471367174889, -0.8745434663808935], [0.4849471367174889, -0.8745434663808935]], [[0.4739082225904436, -0.8805742425038144], [0.4739082225904436, -0.8805742425038144]], [[0.4627943229673003, -0.886465687226098], [0.4627943229673003, -0.886465687226098]], [[0.4516071963768956, -0.8922168683569035], [0.4516071963768956, -0.8922168683569035]], [[0.44034861293462074, -0.8978268758989985], [0.44034861293462074, -0.8978268758989985]], [[0.42902035406232714, -0.903294822192752], [0.42902035406232714, -0.903294822192752]], [[0.4176242122064685, -0.9086198420565812], [0.4176242122064685, -0.9086198420565812]], [[0.4061619905544733, -0.9138010929238529], [0.4061619905544733, -0.9138010929238529]], [[0.3946355027494409, -0.918837754976196], [0.3946355027494409, -0.918837754976196]], [[0.38304657260316866, -0.9237290312732221], [0.38304657260316866, -0.9237290312732221]], [[0.37139703380756833, -0.9284741478786256], [0.37139703380756833, -0.9284741478786256]], [[0.3596887296445368, -0.9330723539826369], [0.3596887296445368, -0.9330723539826369]], [[0.34792351269428423, -0.9375229220208273], [0.34792351269428423, -0.9375229220208273]], [[0.3361032445422173, -0.9418251477892244], [0.3361032445422173, -0.9418251477892244]], [[0.3242297954843714, -0.9459783505557422], [0.3242297954843714, -0.9459783505557422]], [[0.31230504423149086, -0.9499818731678866], [0.31230504423149086, -0.9499818731678866]], [[0.3003308776117511, -0.9538350821567402], [0.3003308776117511, -0.9538350821567402]], [[0.28830919027222335, -0.9575373678371905], [0.28830919027222335, -0.9575373678371905]], [[0.27624188437907515, -0.9610881444044025], [0.27624188437907515, -0.9610881444044025]], [[0.264130869316608, -0.9644868500265066], [0.264130869316608, -0.9644868500265066]], [[0.2519780613851261, -0.9677329469334987], [0.2519780613851261, -0.9677329469334987]], [[0.2397853834977361, -0.9708259215023276], [0.2397853834977361, -0.9708259215023276]], [[0.22755476487608342, -0.9737652843381666], [0.22755476487608342, -0.9737652843381666]], [[0.2152881407450906, -0.9765505703518492], [0.2152881407450906, -0.9765505703518492]], [[0.20298745202676252, -0.9791813388334577], [0.20298745202676252, -0.9791813388334577]], [[0.19065464503306495, -0.9816571735220581], [0.19065464503306495, -0.9816571735220581]], [[0.17829167115797728, -0.9839776826715613], [0.17829167115797728, -0.9839776826715613]], [[0.1659004865687139, -0.9861424991127113], [0.1659004865687139, -0.9861424991127113]], [[0.15348305189621775, -0.9881512803111794], [0.15348305189621775, -0.9881512803111794]], [[0.14104133192492, -0.9900037084217637], [0.14104133192492, -0.9900037084217637]], [[0.12857729528187029, -0.9916994903386805], [0.12857729528187029, -0.9916994903386805]], [[0.11609291412523105, -0.9932383577419429], [0.11609291412523105, -0.9932383577419429]], [[0.10359016383224108, -0.9946200671398147], [0.10359016383224108, -0.9946200671398147]], [[0.09107102268664179, -0.9958443999073395], [0.09107102268664179, -0.9958443999073395]], [[0.07853747156566976, -0.996911162320932], [0.07853747156566976, -0.996911162320932]], [[0.0659914936266216, -0.9978201855890306], [0.0659914936266216, -0.9978201855890306]], [[0.05343507399305771, -0.9985713258788059], [0.05343507399305771, -0.9985713258788059]], [[0.04087019944071283, -0.9991644643389177], [0.04087019944071283, -0.9991644643389177]], [[0.028298858083118522, -0.9995995071183216], [0.028298858083118522, -0.9995995071183216]], [[0.01572303905704239, -0.9998763853811183], [0.01572303905704239, -0.9998763853811183]], [[0.003144732207736932, -0.9999950553174458], [0.003144732207736932, -0.9999950553174458]], [[-0.009434072225895224, -0.999955498150411], [-0.009434072225895224, -0.999955498150411]], [[-0.02201138392622685, -0.9997577201390606], [-0.02201138392622685, -0.9997577201390606]], [[-0.03458521281181564, -0.9994017525773914], [-0.03458521281181564, -0.9994017525773914]], [[-0.04715356935230482, -0.9988876517893979], [-0.04715356935230482, -0.9988876517893979]], [[-0.05971446488320808, -0.9982154991201609], [-0.05971446488320808, -0.9982154991201609]], [[-0.07226591192058601, -0.9973854009229762], [-0.07226591192058601, -0.9973854009229762]], [[-0.08480592447550901, -0.9963974885425265], [-0.08480592447550901, -0.9963974885425265]], [[-0.0973325183683015, -0.9952519182940992], [-0.0973325183683015, -0.9952519182940992]], [[-0.1098437115424997, -0.9939488714388522], [-0.1098437115424997, -0.9939488714388522]], [[-0.12233752437845594, -0.9924885541551351], [-0.12233752437845594, -0.9924885541551351]], [[-0.13481198000658376, -0.9908711975058637], [-0.13481198000658376, -0.9908711975058637]], [[-0.14726510462013975, -0.9890970574019616], [-0.14726510462013975, -0.9890970574019616]], [[-0.15969492778754882, -0.9871664145618658], [-0.15969492778754882, -0.9871664145618658]], [[-0.17209948276416748, -0.9850795744671118], [-0.17209948276416748, -0.9850795744671118]], [[-0.18447680680349163, -0.9828368673139949], [-0.18447680680349163, -0.9828368673139949]], [[-0.19682494146770374, -0.9804386479613271], [-0.19682494146770374, -0.9804386479613271]], [[-0.2091419329375665, -0.9778852958742853], [-0.2091419329375665, -0.9778852958742853]], [[-0.22142583232155733, -0.9751772150643726], [-0.22142583232155733, -0.9751772150643726]], [[-0.23367469596425144, -0.9723148340254892], [-0.23367469596425144, -0.9723148340254892]], [[-0.24588658575385006, -0.9692986056661356], [-0.24588658575385006, -0.9692986056661356]], [[-0.2580595694288491, -0.9661290072377483], [-0.2580595694288491, -0.9661290072377483]], [[-0.2701917208837818, -0.9628065402591844], [-0.2701917208837818, -0.9628065402591844]], [[-0.2822811204739704, -0.9593317304373705], [-0.2822811204739704, -0.9593317304373705]], [[-0.29432585531928135, -0.9557051275841171], [-0.29432585531928135, -0.9557051275841171]], [[-0.30632401960678207, -0.951927305529127], [-0.30632401960678207, -0.951927305529127]], [[-0.31827371489230794, -0.9479988620291956], [-0.31827371489230794, -0.9479988620291956]], [[-0.3301730504008353, -0.9439204186736335], [-0.3301730504008353, -0.9439204186736335]], [[-0.342020143325668, -0.9396926207859086], [-0.342020143325668, -0.9396926207859086]], [[-0.35381311912633706, -0.9353161373215435], [-0.35381311912633706, -0.9353161373215435]], [[-0.3655501118252182, -0.9307916607622624], [-0.3655501118252182, -0.9307916607622624]], [[-0.37722926430276815, -0.9261199070064267], [-0.37722926430276815, -0.9261199070064267]], [[-0.3888487285913865, -0.9213016152557545], [-0.3888487285913865, -0.9213016152557545]], [[-0.4004066661678036, -0.9163375478983632], [-0.4004066661678036, -0.9163375478983632]], [[-0.4119012482439916, -0.9112284903881362], [-0.4119012482439916, -0.9112284903881362]], [[-0.4233306560565341, -0.9059752511204401], [-0.4233306560565341, -0.9059752511204401]], [[-0.4346930811543944, -0.9005786613042189], [-0.4346930811543944, -0.9005786613042189]], [[-0.4459867256850755, -0.8950395748304681], [-0.4459867256850755, -0.8950395748304681]], [[-0.4572098026790778, -0.8893588681371309], [-0.4572098026790778, -0.8893588681371309]], [[-0.46836053633265995, -0.8835374400704156], [-0.46836053633265995, -0.8835374400704156]], [[-0.47943716228880834, -0.8775762117425784], [-0.47943716228880834, -0.8775762117425784]], [[-0.4904379279164198, -0.8714761263861728], [-0.4904379279164198, -0.8714761263861728]], [[-0.5013610925876044, -0.8652381492048091], [-0.5013610925876044, -0.8652381492048091]], [[-0.5122049279531135, -0.8588632672204265], [-0.5122049279531135, -0.8588632672204265]], [[-0.5229677182158008, -0.852352489117125], [-0.5229677182158008, -0.852352489117125]], [[-0.5336477604021214, -0.8457068450815567], [-0.5336477604021214, -0.8457068450815567]], [[-0.5442433646315787, -0.8389273866399275], [-0.5442433646315787, -0.8389273866399275]], [[-0.5547528543841161, -0.8320151864916143], [-0.5547528543841161, -0.8320151864916143]], [[-0.5651745667653925, -0.8249713383394304], [-0.5651745667653925, -0.8249713383394304]], [[-0.5755068527698889, -0.8177969567165786], [-0.5755068527698889, -0.8177969567165786]], [[-0.5857480775418389, -0.8104931768102923], [-0.5857480775418389, -0.8104931768102923]], [[-0.5958966206338965, -0.8030611542822266], [-0.5958966206338965, -0.8030611542822266]], [[-0.6059508762635476, -0.7955020650855904], [-0.6059508762635476, -0.7955020650855904]], [[-0.6159092535671783, -0.7878171052790878], [-0.6159092535671783, -0.7878171052790878]], [[-0.6257701768518052, -0.7800074908376589], [-0.6257701768518052, -0.7800074908376589]], [[-0.6355320858443827, -0.7720744574600873], [-0.6355320858443827, -0.7720744574600873]], [[-0.6451934359386927, -0.76401926037347], [-0.6451934359386927, -0.76401926037347]], [[-0.6547526984397336, -0.7558431741346133], [-0.6547526984397336, -0.7558431741346133]], [[-0.6642083608056132, -0.7475474924283543], [-0.6642083608056132, -0.7475474924283543]], [[-0.6735589268868657, -0.7391335278628713], [-0.6735589268868657, -0.7391335278628713]], [[-0.6828029171631881, -0.7306026117619896], [-0.6828029171631881, -0.7306026117619896]], [[-0.6919388689775459, -0.7219560939545248], [-0.6919388689775459, -0.7219560939545248]], [[-0.7009653367675964, -0.7131953425607112], [-0.7009653367675964, -0.7131953425607112]], [[-0.7098808922944282, -0.7043217437757168], [-0.7098808922944282, -0.7043217437757168]], [[-0.7186841248685372, -0.695336701650319], [-0.7186841248685372, -0.695336701650319]], [[-0.7273736415730482, -0.6862416378687342], [-0.7273736415730482, -0.6862416378687342]], [[-0.7359480674841022, -0.6770379915236775], [-0.7359480674841022, -0.6770379915236775]], [[-0.7444060458884184, -0.6677272188886492], [-0.7444060458884184, -0.6677272188886492]], [[-0.7527462384979536, -0.6583107931875202], [-0.7527462384979536, -0.6583107931875202]], [[-0.7609673256616669, -0.648790204361418], [-0.7609673256616669, -0.648790204361418]], [[-0.7690680065743155, -0.6391669588329865], [-0.7690680065743155, -0.6391669588329865]], [[-0.7770469994822877, -0.6294425792680167], [-0.7770469994822877, -0.6294425792680167]], [[-0.7849030418864043, -0.619618604334529], [-0.7849030418864043, -0.619618604334529]], [[-0.7926348907416839, -0.609696588459308], [-0.7926348907416839, -0.609696588459308]], [[-0.8002413226540318, -0.5996781015819452], [-0.8002413226540318, -0.5996781015819452]], [[-0.807721134073806, -0.5895647289064406], [-0.807721134073806, -0.5895647289064406]], [[-0.8150731414862619, -0.5793580706503675], [-0.8150731414862619, -0.5793580706503675]], [[-0.8222961815988086, -0.5690597417916851], [-0.8222961815988086, -0.5690597417916851]], [[-0.8293891115250823, -0.5586713718131927], [-0.8293891115250823, -0.5586713718131927]], [[-0.8363508089657752, -0.5481946044447112], [-0.8363508089657752, -0.5481946044447112]], [[-0.8431801723862219, -0.537631097402988], [-0.8431801723862219, -0.537631097402988]], [[-0.8498761211906855, -0.5269825221294112], [-0.8498761211906855, -0.5269825221294112]], [[-0.8564375958933453, -0.5162505635255297], [-0.8564375958933453, -0.5162505635255297]], [[-0.8628635582859301, -0.5054369196864662], [-0.8628635582859301, -0.5054369196864662]], [[-0.8691529916019983, -0.49454330163221977], [-0.8691529916019983, -0.49454330163221977]], [[-0.8753049006778127, -0.4835714330369447], [-0.8753049006778127, -0.4835714330369447]], [[-0.8813183121098064, -0.4725230499562131], [-0.8813183121098064, -0.4725230499562131]], [[-0.8871922744086038, -0.46139990055231767], [-0.8871922744086038, -0.46139990055231767]], [[-0.8929258581495678, -0.4502037448176746], [-0.8929258581495678, -0.4502037448176746]], [[-0.898518156119867, -0.43893635429633115], [-0.898518156119867, -0.43893635429633115]], [[-0.9039682834620154, -0.42759951180367056], [-0.9039682834620154, -0.42759951180367056]], [[-0.9092753778138881, -0.4161950111443084], [-0.9092753778138881, -0.4161950111443084]], [[-0.914438599445165, -0.40472465682827513], [-0.914438599445165, -0.40472465682827513]], [[-0.919457131390205, -0.39319026378547983], [-0.919457131390205, -0.39319026378547983]], [[-0.9243301795773077, -0.38159365707855025], [-0.9243301795773077, -0.38159365707855025]], [[-0.9290569729543624, -0.36993667161404425], [-0.9290569729543624, -0.36993667161404425]], [[-0.9336367636108461, -0.3582211518521277], [-0.9336367636108461, -0.3582211518521277]], [[-0.9380688268961654, -0.34644895151472466], [-0.9380688268961654, -0.34644895151472466]], [[-0.9423524615343185, -0.3346219332922018], [-0.9423524615343185, -0.3346219332922018]], [[-0.946486989734852, -0.32274196854865056], [-0.946486989734852, -0.32274196854865056]], [[-0.9504717573001114, -0.31081093702577167], [-0.9504717573001114, -0.31081093702577167]], [[-0.9543061337287484, -0.2988307265454612], [-0.9543061337287484, -0.2988307265454612]], [[-0.9579895123154887, -0.2868032327110909], [-0.9579895123154887, -0.2868032327110909]], [[-0.9615213102471251, -0.27473035860758444], [-0.9615213102471251, -0.27473035860758444]], [[-0.9649009686947388, -0.2626140145002827], [-0.9649009686947388, -0.2626140145002827]], [[-0.9681279529021183, -0.25045611753270025], [-0.9681279529021183, -0.25045611753270025]], [[-0.9712017522703761, -0.23825859142316594], [-0.9712017522703761, -0.23825859142316594]], [[-0.9741218804387358, -0.22602336616045093], [-0.9741218804387358, -0.22602336616045093]], [[-0.9768878753614922, -0.21375237769837674], [-0.9768878753614922, -0.21375237769837674]], [[-0.9794992993811164, -0.2014475676495055], [-0.9794992993811164, -0.2014475676495055]], [[-0.9819557392975065, -0.18911088297791753], [-0.9819557392975065, -0.18911088297791753]], [[-0.9842568064333685, -0.17674427569114207], [-0.9842568064333685, -0.17674427569114207]], [[-0.9864021366957143, -0.1643497025313075], [-0.9864021366957143, -0.1643497025313075]], [[-0.9883913906334727, -0.1519291246655162], [-0.9883913906334727, -0.1519291246655162]], [[-0.9902242534911982, -0.1394845073755471], [-0.9902242534911982, -0.1394845073755471]], [[-0.9919004352588768, -0.12701781974687945], [-0.9919004352588768, -0.12701781974687945]], [[-0.9934196707178105, -0.11453103435714257], [-0.9934196707178105, -0.11453103435714257]], [[-0.9947817194825852, -0.10202612696398496], [-0.9947817194825852, -0.10202612696398496]], [[-0.9959863660391042, -0.08950507619246842], [-0.9959863660391042, -0.08950507619246842]], [[-0.9970334197786901, -0.07696986322198038], [-0.9970334197786901, -0.07696986322198038]], [[-0.9979227150282431, -0.0644224714727701], [-0.9979227150282431, -0.0644224714727701]], [[-0.9986541110764564, -0.051864886292102175], [-0.9986541110764564, -0.051864886292102175]], [[-0.9992274921960794, -0.03929909464013164], [-0.9992274921960794, -0.03929909464013164]], [[-0.9996427676622299, -0.026727084775506123], [-0.9996427676622299, -0.026727084775506123]], [[-0.9998998717667489, -0.014150845940762564], [-0.9998998717667489, -0.014150845940762564]], [[-0.9999987638285974, -0.001572368047586014], [-0.9999987638285974, -0.001572368047586014]], [[-0.9999394282002937, 0.0110063586380641], [-0.9999394282002937, 0.0110063586380641]], [[-0.9997218742703887, 0.02358334381085534], [-0.9997218742703887, 0.02358334381085534]], [[-0.9993461364619809, 0.036156597441018276], [-0.9993461364619809, 0.036156597441018276]], [[-0.9988122742272693, 0.04872413008921046], [-0.9988122742272693, 0.04872413008921046]], [[-0.9981203720381463, 0.06128395322131545], [-0.9981203720381463, 0.06128395322131545]], [[-0.9972705393728328, 0.0738340795230701], [-0.9972705393728328, 0.0738340795230701]], [[-0.9962629106985544, 0.08637252321452737], [-0.9962629106985544, 0.08637252321452737]], [[-0.9950976454502662, 0.09889730036424782], [-0.9950976454502662, 0.09889730036424782]], [[-0.9937749280054243, 0.11140642920322712], [-0.9937749280054243, 0.11140642920322712]], [[-0.9922949676548137, 0.12389793043845473], [-0.9922949676548137, 0.12389793043845473]], [[-0.9906579985694319, 0.1363698275660986], [-0.9906579985694319, 0.1363698275660986]], [[-0.9888642797634358, 0.14882014718424852], [-0.9888642797634358, 0.14882014718424852]], [[-0.9869140950531602, 0.16124691930515087], [-0.9869140950531602, 0.16124691930515087]], [[-0.9848077530122081, 0.17364817766692972], [-0.9848077530122081, 0.17364817766692972]], [[-0.9825455869226281, 0.18602196004469043], [-0.9825455869226281, 0.18602196004469043]], [[-0.9801279547221767, 0.19836630856101212], [-0.9801279547221767, 0.19836630856101212]], [[-0.9775552389476866, 0.21067926999572462], [-0.9775552389476866, 0.21067926999572462]], [[-0.9748278466745344, 0.2229588960949763], [-0.9748278466745344, 0.2229588960949763]], [[-0.9719462094522341, 0.23520324387948816], [-0.9719462094522341, 0.23520324387948816]], [[-0.9689107832361499, 0.24741037595200138], [-0.9689107832361499, 0.24741037595200138]], [[-0.9657220483153551, 0.25957836080381363], [-0.9657220483153551, 0.25957836080381363]], [[-0.9623805092366339, 0.27170527312041143], [-0.9623805092366339, 0.27170527312041143]], [[-0.9588866947246498, 0.2837891940860965], [-0.9588866947246498, 0.2837891940860965]], [[-0.9552411575982872, 0.29582821168760115], [-0.9552411575982872, 0.29582821168760115]], [[-0.9514444746831768, 0.30782042101662727], [-0.9514444746831768, 0.30782042101662727]], [[-0.9474972467204302, 0.31976392457124386], [-0.9474972467204302, 0.31976392457124386]], [[-0.9434000982715814, 0.3316568325561384], [-0.9434000982715814, 0.3316568325561384]], [[-0.9391536776197683, 0.3434972631816217], [-0.9391536776197683, 0.3434972631816217]], [[-0.9347586566671513, 0.35528334296139286], [-0.9347586566671513, 0.35528334296139286]], [[-0.9302157308286049, 0.3670132070089637], [-0.9302157308286049, 0.3670132070089637]], [[-0.9255256189216783, 0.3786849993327492], [-0.9255256189216783, 0.3786849993327492]], [[-0.9206890630528639, 0.3902968731297237], [-0.9206890630528639, 0.3902968731297237]], [[-0.9157068285001696, 0.40184699107765015], [-0.9157068285001696, 0.40184699107765015]], [[-0.9105797035920364, 0.41333352562578207], [-0.9105797035920364, 0.41333352562578207]], [[-0.9053084995825972, 0.4247546592840467], [-0.9053084995825972, 0.4247546592840467]], [[-0.8998940505233184, 0.4361085849106107], [-0.8998940505233184, 0.4361085849106107]], [[-0.8943372131310279, 0.4473935059978257], [-0.8943372131310279, 0.4473935059978257]], [[-0.8886388666523561, 0.45860763695649037], [-0.8886388666523561, 0.45860763695649037]], [[-0.8827999127246203, 0.4697492033983695], [-0.8827999127246203, 0.4697492033983695]], [[-0.8768212752331539, 0.48081644241696414], [-0.8768212752331539, 0.48081644241696414]], [[-0.8707039001651283, 0.49180760286644026], [-0.8707039001651283, 0.49180760286644026]], [[-0.8644487554598653, 0.502720945638721], [-0.8644487554598653, 0.502720945638721]], [[-0.8580568308556884, 0.5135547439386501], [-0.8580568308556884, 0.5135547439386501]], [[-0.8515291377333118, 0.5243072835572309], [-0.8515291377333118, 0.5243072835572309]], [[-0.8448667089558188, 0.53497686314285], [-0.8448667089558188, 0.53497686314285]], [[-0.838070598705227, 0.5455617944704909], [-0.838070598705227, 0.5455617944704909]], [[-0.8311418823156947, 0.5560604027088458], [-0.8311418823156947, 0.5560604027088458]], [[-0.8240816561033651, 0.5664710266853329], [-0.8240816561033651, 0.5664710266853329]], [[-0.8168910371929057, 0.5767920191489293], [-0.8168910371929057, 0.5767920191489293]], [[-0.8095711633407447, 0.5870217470308176], [-0.8095711633407447, 0.5870217470308176]], [[-0.8021231927550442, 0.5971585917027857], [-0.8021231927550442, 0.5971585917027857]], [[-0.7945483039124446, 0.6072009492333305], [-0.7945483039124446, 0.6072009492333305]], [[-0.7868476953715905, 0.6171472306414546], [-0.7868476953715905, 0.6171472306414546]], [[-0.7790225855834922, 0.6269958621480771], [-0.7790225855834922, 0.6269958621480771]], [[-0.7710742126987252, 0.6367452854250599], [-0.7710742126987252, 0.6367452854250599]], [[-0.7630038343715285, 0.6463939578417678], [-0.7630038343715285, 0.6463939578417678]], [[-0.7548127275607995, 0.6559403527091668], [-0.7548127275607995, 0.6559403527091668]], [[-0.7465021883280534, 0.6653829595213779], [-0.7465021883280534, 0.6653829595213779]], [[-0.7380735316323398, 0.6747202841946918], [-0.7380735316323398, 0.6747202841946918]], [[-0.7295280911221899, 0.6839508493039641], [-0.7295280911221899, 0.6839508493039641]], [[-0.7208672189245859, 0.6930731943163961], [-0.7208672189245859, 0.6930731943163961]], [[-0.7120922854310258, 0.7020858758226223], [-0.7120922854310258, 0.7020858758226223]], [[-0.703204679080685, 0.7109874677651012], [-0.703204679080685, 0.7109874677651012]], [[-0.694205806140723, 0.719776561663763], [-0.694205806140723, 0.719776561663763]], [[-0.685097090483782, 0.7284517668388598], [-0.685097090483782, 0.7284517668388598]], [[-0.6758799733626797, 0.7370117106310208], [-0.6758799733626797, 0.7370117106310208]], [[-0.6665559131823733, 0.745455038618435], [-0.6665559131823733, 0.745455038618435]], [[-0.6571263852691893, 0.7537804148311689], [-0.6571263852691893, 0.7537804148311689]], [[-0.6475928816373955, 0.7619865219625438], [-0.6475928816373955, 0.7619865219625438]], [[-0.6379569107531127, 0.7700720615775806], [-0.6379569107531127, 0.7700720615775806]], [[-0.6282199972956439, 0.7780357543184383], [-0.6282199972956439, 0.7780357543184383]], [[-0.6183836819162163, 0.7858763401068541], [-0.6183836819162163, 0.7858763401068541]], [[-0.6084495209942188, 0.7935925783435136], [-0.6084495209942188, 0.7935925783435136]], [[-0.5984190863909279, 0.8011832481043567], [-0.5984190863909279, 0.8011832481043567]], [[-0.5882939652008056, 0.8086471483337546], [-0.5882939652008056, 0.8086471483337546]], [[-0.5780757595003719, 0.8159830980345537], [-0.5780757595003719, 0.8159830980345537]], [[-0.5677660860947084, 0.8231899364549449], [-0.5677660860947084, 0.8231899364549449]], [[-0.5573665762616435, 0.8302665232721198], [-0.5573665762616435, 0.8302665232721198]], [[-0.546878875493628, 0.8372117387727103], [-0.546878875493628, 0.8372117387727103]], [[-0.5363046432373839, 0.8440244840299495], [-0.5363046432373839, 0.8440244840299495]], [[-0.5256455526313215, 0.850703681077561], [-0.5256455526313215, 0.850703681077561]], [[-0.5149032902408143, 0.8572482730803158], [-0.5149032902408143, 0.8572482730803158]], [[-0.5040795557913256, 0.86365722450126], [-0.5040795557913256, 0.86365722450126]], [[-0.49317606189947616, 0.8699295212655587], [-0.49317606189947616, 0.8699295212655587]], [[-0.4821945338020488, 0.8760641709209576], [-0.4821945338020488, 0.8760641709209576]], [[-0.4711367090830182, 0.8820602027948112], [-0.4711367090830182, 0.8820602027948112]], [[-0.46000433739861224, 0.8879166681476723], [-0.46000433739861224, 0.8879166681476723]], [[-0.44879918020046267, 0.893632640323412], [-0.44879918020046267, 0.893632640323412]], [[-0.43752301045690567, 0.8992072148958361], [-0.43752301045690567, 0.8992072148958361]], [[-0.4261776123724359, 0.9046395098117977], [-0.4261776123724359, 0.9046395098117977]], [[-0.4147647811054085, 0.909928665530756], [-0.4147647811054085, 0.909928665530756]], [[-0.403286322483982, 0.9150738451607857], [-0.403286322483982, 0.9150738451607857]], [[-0.39174405272039897, 0.9200742345909907], [-0.39174405272039897, 0.9200742345909907]], [[-0.3801397981235976, 0.9249290426203247], [-0.3801397981235976, 0.9249290426203247]], [[-0.3684753948102517, 0.9296375010827764], [-0.3684753948102517, 0.9296375010827764]], [[-0.3567526884142328, 0.9341988649689195], [-0.3567526884142328, 0.9341988649689195]], [[-0.34497353379459245, 0.9386124125437886], [-0.34497353379459245, 0.9386124125437886]], [[-0.33313979474205874, 0.9428774454610838], [-0.33313979474205874, 0.9428774454610838]], [[-0.3212533436841441, 0.9469932888736632], [-0.3212533436841441, 0.9469932888736632]], [[-0.30931606138887024, 0.9509592915403249], [-0.30931606138887024, 0.9509592915403249]], [[-0.2973298366671729, 0.9547748259288534], [-0.2973298366671729, 0.9547748259288534]], [[-0.28529656607405124, 0.9584392883153082], [-0.28529656607405124, 0.9584392883153082]], [[-0.2732181536084666, 0.9619520988795546], [-0.2732181536084666, 0.9619520988795546]], [[-0.26109651041208987, 0.9653127017970029], [-0.26109651041208987, 0.9653127017970029]], [[-0.24893355446689247, 0.9685205653265596], [-0.24893355446689247, 0.9685205653265596]], [[-0.2367312102916815, 0.9715751818947599], [-0.2367312102916815, 0.9715751818947599]], [[-0.22449140863757358, 0.974476068176083], [-0.22449140863757358, 0.974476068176083]], [[-0.2122160861825098, 0.9772227651694252], [-0.2122160861825098, 0.9772227651694252]], [[-0.19990718522480572, 0.9798148382707292], [-0.19990718522480572, 0.9798148382707292]], [[-0.1875666533758392, 0.9822518773417477], [-0.1875666533758392, 0.9822518773417477]], [[-0.17519644325187023, 0.9845334967749417], [-0.17519644325187023, 0.9845334967749417]], [[-0.16279851216509478, 0.9866593355544919], [-0.16279851216509478, 0.9866593355544919]], [[-0.1503748218139381, 0.9886290573134224], [-0.1503748218139381, 0.9886290573134224]], [[-0.1379273379726542, 0.9904423503868245], [-0.1379273379726542, 0.9904423503868245]], [[-0.12545803018029758, 0.9920989278611683], [-0.12545803018029758, 0.9920989278611683]], [[-0.11296887142907358, 0.9935985276197029], [-0.11296887142907358, 0.9935985276197029]], [[-0.10046183785216964, 0.9949409123839287], [-0.10046183785216964, 0.9949409123839287]], [[-0.08793890841106214, 0.9961258697511428], [-0.08793890841106214, 0.9961258697511428]], [[-0.07540206458240344, 0.9971532122280462], [-0.07540206458240344, 0.9971532122280462]], [[-0.06285329004448297, 0.9980227772604111], [-0.06285329004448297, 0.9980227772604111]], [[-0.05029457036336817, 0.9987344272588005], [-0.05029457036336817, 0.9987344272588005]], [[-0.037727892678718344, 0.99928804962034], [-0.037727892678718344, 0.99928804962034]], [[-0.025155245389377974, 0.9996835567465338], [-0.025155245389377974, 0.9996835567465338]], [[-0.012578617838742366, 0.9999208860571255], [-0.012578617838742366, 0.9999208860571255]], [[-4.898587196589413e-16, 1.0], [-4.898587196589413e-16, 1.0]]], "init_spikes": [[0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0], [0.0, 0.0]], "n_neurons": 2, "intercept_": [-3.0, -3.0]} \ No newline at end of file diff --git a/tests/test_glm.py b/tests/test_glm.py index d32e0d0c..7a60e2b8 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -383,8 +383,8 @@ def test_score_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonG X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) _test_class_method(model, "score", [X, y], {}, error, match_str) @@ -403,8 +403,8 @@ def test_score_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonG X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) _test_class_method(model, "score", [X, y], {}, error, match_str) @@ -422,8 +422,8 @@ def test_score_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_mo X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] if delta_dim == -1: # remove a dimension @@ -448,8 +448,8 @@ def test_score_y_dimensionality(self, delta_dim, error, match_str, poissonGLM_mo X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, _ = X.shape # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] if delta_dim == -1: # remove a dimension @@ -474,8 +474,8 @@ def test_score_n_feature_consistency_x(self, delta_n_features, error, match_str, """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] if delta_n_features == 1: # add a feature X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) @@ -516,8 +516,8 @@ def test_score_time_points_x(self, delta_tp, error, match_str, poissonGLM_model_ Ensure that the number of time-points in X and y matches. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) _test_class_method(model, "score", [X, y], {}, error, match_str) @@ -536,8 +536,8 @@ def test_score_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_ Ensure that the number of time-points in X and y matches. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) _test_class_method(model, "score", [X, y], {}, error, match_str) @@ -555,8 +555,8 @@ def test_score_type_r2(self, score_type, error, match_str, poissonGLM_model_inst """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation match_str = match_str % score_type if type(match_str) is str else None - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] _test_class_method(model, "score", [X, y], {"score_type": score_type}, error, match_str) def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): @@ -566,8 +566,8 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] # get the rate mean_firing = model.predict(X) # compute the log-likelihood using jax.scipy @@ -595,8 +595,8 @@ def test_predict_n_neuron_match_x(self, delta_n_neuron, error, match_str, poisso X, _, model, true_params, _ = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) _test_class_method(model, "predict", [X], {}, error, match_str) @@ -614,8 +614,8 @@ def test_predict_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] if delta_dim == -1: # remove a dimension X = np.zeros((n_samples, n_neurons)) @@ -633,12 +633,12 @@ def test_predict_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_ def test_predict_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): """ Test the `predict` method ensuring the number of features in x input data - is consistent with the model's `model.`basis_coeff_`. + is consistent with the model's `model.coef_`. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] if delta_n_features == 1: # add a feature X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) @@ -680,7 +680,7 @@ def test_simulate_n_neuron_match_input(self, delta_n_neuron, error, match_str, """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate - n_neurons, n_features = model.basis_coeff_.shape + n_neurons, n_features = model.coef_.shape n_time_points, _, n_basis_input = feedforward_input.shape if delta_n_neuron != 0: feedforward_input = np.zeros((n_time_points, n_neurons+delta_n_neuron, n_basis_input)) @@ -819,7 +819,7 @@ def test_simulate_is_fit(self, is_fit, error, match_str, poissonGLM_coupled_model_config_simulate if not is_fit: - model.baseline_link_fr_ = None + model.intercept_ = None _test_class_method( model, "simulate_recurrent", @@ -917,7 +917,7 @@ def test_simulate_feature_consistency_input(self, delta_features, error, match_s Notes ----- - The total feature number `model.basis_coeff_.shape[1]` must be equal to + The total feature number `model.coef_.shape[1]` must be equal to `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ @@ -955,7 +955,7 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, error Notes ----- - The total feature number `model.basis_coeff_.shape[1]` must be equal to + The total feature number `model.coef_.shape[1]` must be equal to `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ @@ -985,8 +985,8 @@ def test_simulate_feedforward_GLM_not_fit(self, poissonGLM_model_instantiation): def test_simulate_feedforward_GLM(self, poissonGLM_model_instantiation): """Test that simulate goes through""" X, y, model, params, rate = poissonGLM_model_instantiation - model.basis_coeff_ = params[0] - model.baseline_link_fr_ = params[1] + model.coef_ = params[0] + model.intercept_ = params[1] ysim, ratesim = model.simulate(jax.random.PRNGKey(123), X) # check that the expected dimensionality is returned assert ysim.ndim == 2 @@ -1009,8 +1009,8 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set model coeff - model.basis_coeff_ = true_params[0] - model.baseline_link_fr_ = true_params[1] + model.coef_ = true_params[0] + model.intercept_ = true_params[1] # get the rate dev = sm.families.Poisson().deviance(y, firing_rate) dev_model = model.observation_model.deviance(firing_rate, y).sum() From 9d7ccd2db0604051e435ed7b2d34eae4094b5c02 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 14:35:48 -0500 Subject: [PATCH 170/250] added a warning on the demo --- docs/examples/plot_glm_demo.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 120d69cc..89381eef 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -1,6 +1,12 @@ """ # GLM Demo: Toy Model Examples +!!! warning + This demonstration is currently in its alpha stage. It presents various regularization techniques on + GLMs trained on a Gaussian noise stimuli, and a minimal example of fitting and simulating a pair of coupled + neurons. More work needs to be done to properly compare the performance of the regularization strategies on + realistic simulations and real neural recordings. + ## Introduction In this demo we will work through two toy example of a Poisson-GLM on synthetic data: a purely feed-forward input model From 2b87edca7aef8a56c70db88ab21f5a30aac374e3 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 17:54:48 -0500 Subject: [PATCH 171/250] no change --- src/{neurostatslib => nemos}/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{neurostatslib => nemos}/__init__.py (100%) diff --git a/src/neurostatslib/__init__.py b/src/nemos/__init__.py similarity index 100% rename from src/neurostatslib/__init__.py rename to src/nemos/__init__.py From 13135ceafd564775860ec136deba416b866f5f51 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:07:59 -0500 Subject: [PATCH 172/250] removed refs to neurostatslib --- .github/workflows/deploy-pure-python.yml | 73 +++++++++++++++++++ CONTRIBUTING.md | 2 +- .../developers_notes/03-observation_models.md | 2 +- docs/developers_notes/04-solver.md | 6 +- docs/developers_notes/README.md | 10 +-- docs/examples/README.md | 2 +- docs/examples/plot_ND_basis_function.py | 14 ++-- docs/examples/plot_example_convolution.py | 18 ++--- docs/examples/plot_glm_demo.py | 26 +++---- docs/index.md | 4 +- src/{neurostatslib => nemos}/basis.py | 6 +- src/{neurostatslib => nemos}/exceptions.py | 4 +- .../observation_models.py | 4 +- .../proximal_operator.py | 0 src/{neurostatslib => nemos}/sample_points.py | 0 src/{neurostatslib => nemos}/utils.py | 0 tests/test_base_class.py | 8 +- tests/test_basis.py | 2 +- tests/test_convolution_1d.py | 2 +- tests/test_glm.py | 18 ++--- tests/test_observation_models.py | 4 +- tests/test_solver.py | 10 +-- 22 files changed, 144 insertions(+), 71 deletions(-) create mode 100644 .github/workflows/deploy-pure-python.yml rename src/{neurostatslib => nemos}/basis.py (99%) rename src/{neurostatslib => nemos}/exceptions.py (83%) rename src/{neurostatslib => nemos}/observation_models.py (98%) rename src/{neurostatslib => nemos}/proximal_operator.py (100%) rename src/{neurostatslib => nemos}/sample_points.py (100%) rename src/{neurostatslib => nemos}/utils.py (100%) diff --git a/.github/workflows/deploy-pure-python.yml b/.github/workflows/deploy-pure-python.yml new file mode 100644 index 00000000..a878d5b1 --- /dev/null +++ b/.github/workflows/deploy-pure-python.yml @@ -0,0 +1,73 @@ +name: Build and upload to PyPI for pure python +on: + release: + types: [published] + +jobs: + build: + name: Build and test package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # this is necessary for setuptools_scm to work properly with github + # actions, see https://github.com/pypa/setuptools_scm/issues/480 and + # https://stackoverflow.com/a/68959339 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Build package + run: | + pip install build + python -m build --outdir dist/ --sdist --wheel + - name: Check there's only one sdist and one whl file created + shell: bash + # because the following two tests will be weird otherwise. see + # https://askubuntu.com/a/454568 for why this is the right way to handle + # it. using [[ BOOLEAN ]] || EXPR is a compact way of writing IF NOT + # BOOLEAN THEN EXPR in bash + run: | + [[ $(find dist/ -type f -name "*whl" -printf x | wc -c) == 1 ]] || exit 1 + [[ $(find dist/ -type f -name "*tar.gz" -printf x | wc -c) == 1 ]] || exit 1 + - name: Check setuptools_scm version against git tag + shell: bash + run: | + # we use the error code of this comparison: =~ is bash's regex + # operator, so it checks whether the right side is contained in the + # left side. In particular, we succeed if the path of the source code + # ends in the most recent git tag, fail if it does not. + [[ "$(ls dist/*tar.gz)" =~ "-$(git describe --tags).tar.gz" ]] + - name: Check we can install from wheel + # note that this is how this works in bash (different shells might be + # slightly different). we've checked there's only one .whl file in an + # earlier step, so the bit in `$()` will expand to that single file, + # then we pass [dev] to get specify the optional dev dependencies, and + # we wrap the whole thing in quotes so bash doesn't try to interpret the + # square brackets but passes them directly to pip install + shell: bash + run: | + pip install "$(ls dist/*whl)[dev]" + - name: Run some tests + # modify the following as necessary to e.g., run notebooks + run: | + pytest + - uses: actions/upload-artifact@v3 + with: + path: dist/* + + publish: + name: Upload release to PyPI + needs: [build] + environment: pypi + runs-on: ubuntu-latest + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + steps: + - uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + - name: Publish package to pypi + uses: pypa/gh-action-pypi-publish@release/v1 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d46272da..ae7ca41a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -The `neurostatslib` package is designed to provide a robust set of statistical analysis tools for neuroscience research. While the repository is managed by a core team of data scientists at the Center for Computational Neuroscience of the Flatiron Institute, we warmly welcome contributions from external collaborators. +The `nemos` package is designed to provide a robust set of statistical analysis tools for neuroscience research. While the repository is managed by a core team of data scientists at the Center for Computational Neuroscience of the Flatiron Institute, we warmly welcome contributions from external collaborators. ## General Guidelines diff --git a/docs/developers_notes/03-observation_models.md b/docs/developers_notes/03-observation_models.md index 9b01babc..a05bf4b0 100644 --- a/docs/developers_notes/03-observation_models.md +++ b/docs/developers_notes/03-observation_models.md @@ -4,7 +4,7 @@ The `observation_models` module provides objects representing the observations of GLM-like models. -The abstract class `Observations` defines the structure of the subclasses which specify observation types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`neurostatslib.glm.GLM`](../03-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. +The abstract class `Observations` defines the structure of the subclasses which specify observation types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`nemos.glm.GLM`](../03-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. ## The Abstract class `Observations` diff --git a/docs/developers_notes/04-solver.md b/docs/developers_notes/04-solver.md index 240ad1ea..fac92584 100644 --- a/docs/developers_notes/04-solver.md +++ b/docs/developers_notes/04-solver.md @@ -4,7 +4,7 @@ The `solver` module introduces an archetype class `Solver` which provides the structural components for each concrete sub-class. -Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`neurostatslib.glm.GLM`](../03-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. +Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../03-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). We choose to rely on `jaxopt` because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX, that are highly customizable. @@ -38,7 +38,7 @@ Additionally, the class provides auxiliary methods for checking that the solver `Solver` objects define the following abstract method: -- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function. The loss function must be a `Callable` from either the `jax` or the `neurostatslib` namespace. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. +- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function. The loss function must be a `Callable` from either the `jax` or the `nemos` namespace. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. ### Public Methods @@ -50,7 +50,7 @@ Additionally, the class provides auxiliary methods for checking that the solver - **`_check_solver_kwargs`**: This method checks if the provided keyword arguments are valid for the specified solver. This helps in catching and preventing potential errors in solver configuration. -- **`_check_is_callable_from_jax`**: This method checks if the provided function is callable and whether it belongs to the `jax` or `neurostatslib` namespace, ensuring compatibility and safety when using jax-based operations. +- **`_check_is_callable_from_jax`**: This method checks if the provided function is callable and whether it belongs to the `jax` or `nemos` namespace, ensuring compatibility and safety when using jax-based operations. ## The `UnRegularizedSolver` Class diff --git a/docs/developers_notes/README.md b/docs/developers_notes/README.md index 98e7e00e..59c9b4bd 100644 --- a/docs/developers_notes/README.md +++ b/docs/developers_notes/README.md @@ -1,6 +1,6 @@ # Introduction -Welcome to the Developer Notes of the `neurostatslib` project. These notes aim to provide detailed technical information about the various modules, classes, and functions that make up this library, as well as guidelines on how to write code that integrates nicely with our package. They are intended to help current and future developers understand the design decisions, structure, and functioning of the library, and to provide guidance on how to modify, extend, and maintain the codebase. +Welcome to the Developer Notes of the `nemos` project. These notes aim to provide detailed technical information about the various modules, classes, and functions that make up this library, as well as guidelines on how to write code that integrates nicely with our package. They are intended to help current and future developers understand the design decisions, structure, and functioning of the library, and to provide guidance on how to modify, extend, and maintain the codebase. ## Intended Audience @@ -11,7 +11,7 @@ These notes are primarily intended for the following groups: - **Future Developers**: These notes can help onboard new developers to the project, providing them with detailed explanations of the codebase and its underlying architecture. -- **Contributors**: If you wish to contribute to the `neurostatslib` project, the Developer Notes can provide a solid foundation of understanding, helping to ensure that your contributions align with the existing structure and design principles of the library. +- **Contributors**: If you wish to contribute to the `nemos` project, the Developer Notes can provide a solid foundation of understanding, helping to ensure that your contributions align with the existing structure and design principles of the library. - **Advanced Users**: While the primary focus of these notes is on development, they might also be of interest to advanced users who want a deeper understanding of the library's functionality. @@ -19,7 +19,7 @@ Please note that these notes assume a certain level of programming knowledge. Fa ## Navigating the Developer Notes -The Developer Notes are divided into sections, each focusing on a different module or class within the `neurostatslib` library. Each section provides an overview of the class or module, explains its role and functionality within the library, and offers a comprehensive guide to its classes and functions. Typically, we will provide instructions on how to extend the existing modules. We generally advocate for the use of inheritance and encourage consistency with the existing codebase. In creating developer instructions, we follow the conventions outlined below: +The Developer Notes are divided into sections, each focusing on a different module or class within the `nemos` library. Each section provides an overview of the class or module, explains its role and functionality within the library, and offers a comprehensive guide to its classes and functions. Typically, we will provide instructions on how to extend the existing modules. We generally advocate for the use of inheritance and encourage consistency with the existing codebase. In creating developer instructions, we follow the conventions outlined below: - **Must**: This denotes a requirement. Any method or function that fails to meet the requirement will not be merged. - **Should**: This denotes a suggestion. Reasons should be provided if a suggestion is not followed. @@ -27,9 +27,9 @@ The Developer Notes are divided into sections, each focusing on a different modu ## Interact with us -If you're considering contributing to the library, first of all, welcome aboard! As a first step, we recommend that you read the [`CONTRIBUTING.md`](https://github.com/flatironinstitute/generalized-linear-models/blob/main/CONTRIBUTING.md) guidelines. These will help you understand how to interact with other contributors and how to submit your changes. +If you're considering contributing to the library, first of all, welcome aboard! As a first step, we recommend that you read the [`CONTRIBUTING.md`](https://github.com/flatironinstitute/nemos/blob/main/CONTRIBUTING.md) guidelines. These will help you understand how to interact with other contributors and how to submit your changes. -If you have any questions or need further clarification on any of the topics covered in these notes, please don't hesitate to reach out to us. You can do so via the [discussion](https://github.com/flatironinstitute/generalized-linear-models/discussions/landing) forum on GitHub. +If you have any questions or need further clarification on any of the topics covered in these notes, please don't hesitate to reach out to us. You can do so via the [discussion](https://github.com/flatironinstitute/nemos/discussions/landing) forum on GitHub. We're looking forward to your contributions and to answering any queries you might have! diff --git a/docs/examples/README.md b/docs/examples/README.md index 85573549..8ad350e2 100644 --- a/docs/examples/README.md +++ b/docs/examples/README.md @@ -1,3 +1,3 @@ # Examples -A gallery of tutorials on the current `neurostatslib` functionalities. \ No newline at end of file +A gallery of tutorials on the current `nemos` functionalities. \ No newline at end of file diff --git a/docs/examples/plot_ND_basis_function.py b/docs/examples/plot_ND_basis_function.py index eff939aa..94cbd36f 100644 --- a/docs/examples/plot_ND_basis_function.py +++ b/docs/examples/plot_ND_basis_function.py @@ -63,14 +63,14 @@ # $$ # Here, we simply add two basis objects, `a_basis` and `b_basis`, together to define the additive basis. -import matplotlib.pyplot as plt import numpy as np +import matplotlib.pyplot as plt -import neurostatslib as nsl +import nemos as nmo # Define 1D basis objects -a_basis = nsl.basis.MSplineBasis(n_basis_funcs=15, order=3) -b_basis = nsl.basis.RaisedCosineBasisLog(n_basis_funcs=14) +a_basis = nmo.basis.MSplineBasis(n_basis_funcs=15, order=3) +b_basis = nmo.basis.RaisedCosineBasisLog(n_basis_funcs=14) # Define the 2D additive basis object additive_basis = a_basis + b_basis @@ -239,9 +239,9 @@ T = 10 n_basis = 8 -a_basis = nsl.basis.RaisedCosineBasisLinear(n_basis_funcs=n_basis) -b_basis = nsl.basis.RaisedCosineBasisLinear(n_basis_funcs=n_basis) -c_basis = nsl.basis.RaisedCosineBasisLinear(n_basis_funcs=n_basis) +a_basis = nmo.basis.RaisedCosineBasisLinear(n_basis_funcs=n_basis) +b_basis = nmo.basis.RaisedCosineBasisLinear(n_basis_funcs=n_basis) +c_basis = nmo.basis.RaisedCosineBasisLinear(n_basis_funcs=n_basis) prod_basis_3 = a_basis * b_basis * c_basis samples = np.linspace(0, 1, T) diff --git a/docs/examples/plot_example_convolution.py b/docs/examples/plot_example_convolution.py index 02337ed3..ddcb1675 100644 --- a/docs/examples/plot_example_convolution.py +++ b/docs/examples/plot_example_convolution.py @@ -1,16 +1,16 @@ """ -# One-dimensional convolutions +One-dimensional convolutions """ # %% # ## Generate synthetic data # Generate some simulated spike counts. -import matplotlib.patches as patches -import matplotlib.pylab as plt import numpy as np +import matplotlib.pylab as plt +import matplotlib.patches as patches -import neurostatslib as nsl +import nemos as nmo np.random.seed(10) ws = 11 @@ -40,7 +40,7 @@ # create three filters -basis_obj = nsl.basis.RaisedCosineBasisLinear(n_basis_funcs=3) +basis_obj = nmo.basis.RaisedCosineBasisLinear(n_basis_funcs=3) _, w = basis_obj.evaluate_on_grid(ws) plt.plot(w) @@ -49,7 +49,7 @@ # the function requires an iterable (one element per trial) # and returns a list of convolutions -spk_conv = nsl.utils.convolve_1d_trials(w, [spk, np.zeros((20, 1))]) +spk_conv = nmo.utils.convolve_1d_trials(w, [spk, np.zeros((20, 1))]) print(f"Shape of spk: {spk.shape}\nShape of w: {w.shape}") # valid convolution should be of shape n_samples - ws + 1 @@ -73,9 +73,9 @@ # pad according to the causal direction of the filter, after squeeze, the dimension is (n_filters, n_samples) -spk_causal_utils = np.squeeze(nsl.utils.nan_pad_conv(spk_conv, ws, filter_type="causal")[0]) -spk_anticausal_utils = np.squeeze(nsl.utils.nan_pad_conv(spk_conv, ws, filter_type="anti-causal")[0]) -spk_acausal_utils = np.squeeze(nsl.utils.nan_pad_conv(spk_conv, ws, filter_type="acausal")[0]) +spk_causal_utils = np.squeeze(nmo.utils.nan_pad_conv(spk_conv, ws, filter_type="causal")[0]) +spk_anticausal_utils = np.squeeze(nmo.utils.nan_pad_conv(spk_conv, ws, filter_type="anti-causal")[0]) +spk_acausal_utils = np.squeeze(nmo.utils.nan_pad_conv(spk_conv, ws, filter_type="acausal")[0]) # %% diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 89381eef..a56bd3f1 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -32,7 +32,7 @@ import sklearn.model_selection as sklearn_model_selection from matplotlib.patches import Rectangle -import neurostatslib as nsl +import nemos as nmo # Enable float64 precision (optional) jax.config.update("jax_enable_x64", True) @@ -56,20 +56,20 @@ # ## The Feed-Forward GLM # # ### Model Definition -# The class implementing the feed-forward GLM is `neurostatslib.glm.GLM`. +# The class implementing the feed-forward GLM is `generalized-linear-models.glm.GLM`. # In order to define the class, one **must** provide: # # - **Observation Model**: The observation model for the GLM, e.g. an object of the class of type -# `neurostatslib.observation_models.Observations`. So far, only the `PoissonObservations` +# `generalized-linear-models.observation_models.Observations`. So far, only the `PoissonObservations` # model has been implemented. -# - **Solver**: The desired solver, e.g. an object of the `neurostatslib.solver.Solver` class. +# - **Solver**: The desired solver, e.g. an object of the `generalized-linear-models.solver.Solver` class. # Currently, we implemented the un-regularized, Ridge, Lasso, and Group-Lasso solver. # # The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge solver. # Here is how to define the model. # default Poisson GLM with Ridge solver and Poisson observation model. -model = nsl.glm.GLM() +model = nmo.glm.GLM() print("Solver type: ", type(model.solver)) print("Observation model:", type(model.observation_model)) @@ -96,17 +96,17 @@ # set after the model is initialized with the following syntax: # Poisson observation model with soft-plus NL -observation_models = nsl.observation_models.PoissonObservations(jax.nn.softplus) +observation_models = nmo.observation_models.PoissonObservations(jax.nn.softplus) # Observation model -solver = nsl.solver.RidgeSolver( +solver = nmo.solver.RidgeSolver( solver_name="LBFGS", regularizer_strength=0.1, solver_kwargs={"tol":10**-10} ) # define the GLM -model = nsl.glm.GLM( +model = nmo.glm.GLM( observation_model=observation_models, solver=solver, ) @@ -118,7 +118,7 @@ # Hyperparameters can be set at any moment via the `set_params` method. model.set_params( - solver=nsl.solver.LassoSolver(), + solver=nmo.solver.LassoSolver(), observation_model__inverse_link_function=jax.numpy.exp ) @@ -142,7 +142,7 @@ # The same exact syntax works for any configuration. # Fit a ridge regression Poisson GLM -model = nsl.glm.GLM() +model = nmo.glm.GLM() model.set_params(solver__regularizer_strength=0.1) model.fit(X, spikes) @@ -174,7 +174,7 @@ # # **Lasso** -model.set_params(solver=nsl.solver.LassoSolver()) +model.set_params(solver=nmo.solver.LassoSolver()) cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) @@ -191,7 +191,7 @@ mask[0, [0, -1]] = 1 mask[1, 1:-1] = 1 -solver = nsl.solver.GroupLassoSolver("ProximalGradient", mask=mask) +solver = nmo.solver.GroupLassoSolver("ProximalGradient", mask=mask) model.set_params(solver=solver) cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) @@ -268,7 +268,7 @@ # %% # We can now simulate spikes by calling the `simulate_recurrent` method. -model = nsl.glm.GLMRecurrent() +model = nmo.glm.GLMRecurrent() model.coef_ = jax.numpy.asarray(basis_coeff) model.intercept_ = jax.numpy.asarray(intercept) diff --git a/docs/index.md b/docs/index.md index d1aa074f..f4ff1443 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,7 +1,7 @@ -# neurostatslib +# nemos A toolbox of statistical analysis for neuroscience. ## Disclaimer Please note that this package is currently under development. While you can download and test the functionalities that are already present, it's important to be aware that the code stability and systematic testing cannot be guaranteed at this stage. -See our [README](https://github.com/flatironinstitute/generalized-linear-models/blob/main/README.md) for more info. \ No newline at end of file +See our [README](https://github.com/flatironinstitute/nemos/blob/main/README.md) for more info. diff --git a/src/neurostatslib/basis.py b/src/nemos/basis.py similarity index 99% rename from src/neurostatslib/basis.py rename to src/nemos/basis.py index 0da14e82..04a4dc48 100644 --- a/src/neurostatslib/basis.py +++ b/src/nemos/basis.py @@ -12,7 +12,7 @@ from numpy.typing import ArrayLike, NDArray from scipy.interpolate import splev -from neurostatslib.utils import row_wise_kron +from .utils import row_wise_kron __all__ = [ "MSplineBasis", @@ -734,8 +734,8 @@ def _evaluate(self, sample_pts: NDArray) -> NDArray: sample_pts : (number of samples,) Spacing for basis functions, holding elements on interval [0, 1). A good default is - ``nsl.sample_points.raised_cosine_log`` for log spacing (as used in - [2]_) or ``nsl.sample_points.raised_cosine_linear`` for linear + ``nmo.sample_points.raised_cosine_log`` for log spacing (as used in + [2]_) or ``nmo.sample_points.raised_cosine_linear`` for linear spacing. Returns diff --git a/src/neurostatslib/exceptions.py b/src/nemos/exceptions.py similarity index 83% rename from src/neurostatslib/exceptions.py rename to src/nemos/exceptions.py index 9fe3ce0b..7e6b2678 100644 --- a/src/neurostatslib/exceptions.py +++ b/src/nemos/exceptions.py @@ -9,8 +9,8 @@ class NotFittedError(ValueError, AttributeError): Examples -------- - >>> from neurostatslib.glm import GLM - >>> from neurostatslib.exceptions import NotFittedError + >>> from generalized-linear-models.glm import GLM + >>> from generalized-linear-models.exceptions import NotFittedError >>> try: ... GLM().predict([[[1, 2], [2, 3], [3, 4]]]) ... except NotFittedError as e: diff --git a/src/neurostatslib/observation_models.py b/src/nemos/observation_models.py similarity index 98% rename from src/neurostatslib/observation_models.py rename to src/nemos/observation_models.py index f7610aea..40b29e02 100644 --- a/src/neurostatslib/observation_models.py +++ b/src/nemos/observation_models.py @@ -36,7 +36,7 @@ class Observations(Base, abc.ABC): See Also -------- - [PoissonObservations](./#neurostatslib.observation_models.PoissonObservations) : A specific implementation of a + [PoissonObservations](./#generalized-linear-models.observation_models.PoissonObservations) : A specific implementation of a observation model using the Poisson distribution. """ @@ -279,7 +279,7 @@ class PoissonObservations(Observations): See Also -------- - [Observations](./#neurostatslib.observation_models.Observations) : Base class for observation models. + [Observations](./#generalized-linear-models.observation_models.Observations) : Base class for observation models. """ def __init__(self, inverse_link_function=jnp.exp): diff --git a/src/neurostatslib/proximal_operator.py b/src/nemos/proximal_operator.py similarity index 100% rename from src/neurostatslib/proximal_operator.py rename to src/nemos/proximal_operator.py diff --git a/src/neurostatslib/sample_points.py b/src/nemos/sample_points.py similarity index 100% rename from src/neurostatslib/sample_points.py rename to src/nemos/sample_points.py diff --git a/src/neurostatslib/utils.py b/src/nemos/utils.py similarity index 100% rename from src/neurostatslib/utils.py rename to src/nemos/utils.py diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 1a983500..ae0f3335 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -5,9 +5,9 @@ import pytest from numpy.typing import NDArray -import neurostatslib as nsl -from neurostatslib.base_class import Base, BaseRegressor -from neurostatslib.utils import check_invalid_entry, convert_to_jnp_ndarray +import nemos as nmo +from nemos.base_class import Base, BaseRegressor +from nemos.utils import check_invalid_entry, convert_to_jnp_ndarray @pytest.fixture @@ -323,7 +323,7 @@ def test_target_device_put(device_name: Literal["cpu", "gpu", "tpu"], mock_regre Put array to device and checks that the device is matched after put, if device is found. Raise error otherwise. """ - raise_exception = not nsl.utils.has_local_device(device_name) + raise_exception = not nmo.utils.has_local_device(device_name) x = jnp.array([1]) if raise_exception: if device_name != "tpu": diff --git a/tests/test_basis.py b/tests/test_basis.py index 7f31fe2e..a8ee3941 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -6,7 +6,7 @@ import pytest import utils_testing -import neurostatslib.basis as basis +import nemos.basis as basis # automatic define user accessible basis and check the methods diff --git a/tests/test_convolution_1d.py b/tests/test_convolution_1d.py index 46cf2786..fa66a2a3 100644 --- a/tests/test_convolution_1d.py +++ b/tests/test_convolution_1d.py @@ -1,7 +1,7 @@ import numpy as np import pytest -import neurostatslib.utils as utils +import nemos.utils as utils class Test1DConvolution: diff --git a/tests/test_glm.py b/tests/test_glm.py index 7a60e2b8..2c92fa1d 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -5,7 +5,7 @@ import statsmodels.api as sm from sklearn.model_selection import GridSearchCV -import neurostatslib as nsl +import nemos as nmo def _test_class_initialization(cls, kwargs, error, match_str): @@ -28,7 +28,7 @@ class TestGLM: """ Unit tests for the PoissonGLM class. """ - cls = nsl.glm.GLM + cls = nmo.glm.GLM ####################### # Test model.__init__ @@ -36,9 +36,9 @@ class TestGLM: @pytest.mark.parametrize( "solver, error, match_str", [ - (nsl.solver.RidgeSolver("BFGS"), None, None), + (nmo.solver.RidgeSolver("BFGS"), None, None), (None, AttributeError, "The provided `solver` doesn't implement "), - (nsl.solver.RidgeSolver, TypeError, "The provided `solver` cannot be instantiated") + (nmo.solver.RidgeSolver, TypeError, "The provided `solver` cannot be instantiated") ] ) def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiation): @@ -51,8 +51,8 @@ def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiat @pytest.mark.parametrize( "observation, error, match_str", [ - (nsl.observation_models.PoissonObservations(), None, None), - (nsl.solver.Solver, AttributeError, "The provided object does not have the required"), + (nmo.observation_models.PoissonObservations(), None, None), + (nmo.solver.Solver, AttributeError, "The provided object does not have the required"), (1, AttributeError, "The provided object does not have the required") ] ) @@ -363,7 +363,7 @@ def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_in def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation): """Test that the group lasso fit goes through""" X, y, model, params, rate, mask = group_sparse_poisson_glm_model_instantiation - model.set_params(solver=nsl.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask)) + model.set_params(solver=nmo.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask)) model.fit(X, y) ####################### @@ -978,7 +978,7 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, error def test_simulate_feedforward_GLM_not_fit(self, poissonGLM_model_instantiation): X, y, model, params, rate = poissonGLM_model_instantiation - with pytest.raises(nsl.exceptions.NotFittedError, + with pytest.raises(nmo.exceptions.NotFittedError, match="This GLM instance is not fitted yet"): model.simulate(jax.random.PRNGKey(123), X) @@ -1041,7 +1041,7 @@ def test_end_to_end_fit_and_simulate(self, # convolve basis and spikes # (n_trials, n_timepoints - ws + 1, n_neurons, n_coupling_basis) conv_spikes = jnp.asarray( - nsl.utils.convolve_1d_trials(coupling_basis, [spikes]), + nmo.utils.convolve_1d_trials(coupling_basis, [spikes]), dtype=jnp.float32 ) diff --git a/tests/test_observation_models.py b/tests/test_observation_models.py index cd94980d..bdc45627 100644 --- a/tests/test_observation_models.py +++ b/tests/test_observation_models.py @@ -5,11 +5,11 @@ import scipy.stats as sts import statsmodels.api as sm -import neurostatslib as nsl +import nemos as nmo class TestPoissonObservations: - cls = nsl.observation_models.PoissonObservations + cls = nmo.observation_models.PoissonObservations @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) def test_initialization_link_is_callable(self, link_function): diff --git a/tests/test_solver.py b/tests/test_solver.py index 0bc59502..17ab6ff8 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -5,11 +5,11 @@ import statsmodels.api as sm from sklearn.linear_model import PoissonRegressor -import neurostatslib as nsl +import nemos as nmo class TestUnRegularizedSolver: - cls = nsl.solver.UnRegularizedSolver + cls = nmo.solver.UnRegularizedSolver @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -119,7 +119,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): class TestRidgeSolver: - cls = nsl.solver.RidgeSolver + cls = nmo.solver.RidgeSolver @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -229,7 +229,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): class TestLassoSolver: - cls = nsl.solver.LassoSolver + cls = nmo.solver.LassoSolver @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -317,7 +317,7 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): class TestGroupLassoSolver: - cls = nsl.solver.GroupLassoSolver + cls = nmo.solver.GroupLassoSolver @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): From 14a17b82dc51a757fd66d07e1266afbd3838db0b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:08:14 -0500 Subject: [PATCH 173/250] removed refs to neurostatslib --- README.md | 11 +++--- docs/developers_notes/02-base_class.md | 4 +-- docs/developers_notes/05-glm.md | 12 +++---- docs/examples/plot_1D_basis_function.py | 10 +++--- mkdocs.yml | 4 +-- pyproject.toml | 10 +++--- src/{neurostatslib => nemos}/base_class.py | 6 ++-- src/{neurostatslib => nemos}/glm.py | 18 +++++----- src/{neurostatslib => nemos}/solver.py | 2 +- tests/conftest.py | 40 +++++++++++----------- tests/test_proximal_operator.py | 2 +- 11 files changed, 60 insertions(+), 59 deletions(-) rename src/{neurostatslib => nemos}/base_class.py (98%) rename src/{neurostatslib => nemos}/glm.py (97%) rename src/{neurostatslib => nemos}/solver.py (99%) diff --git a/README.md b/README.md index eae842ef..6db51750 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ ![LOGO](CCN-logo-wText.png) -# neurostatslib -A toolbox of statistical analysis for neuroscience. +# nemos +NEural MOdelS, a statistical modeling framework for neuroscience. + +## Disclaimer +This is an alpha version, the code is in active development and the API is subject to change. ## Setup To install, clone this repo and install using `pip`: ``` sh -git clone git@github.com:flatironinstitute/generalized-linear-models.git -cd generalized-linear-models/ +git clone git@github.com:flatironinstitute/nemos.git +cd nemos/ pip install -e . ``` diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 51e2a3de..6acf7f74 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -8,7 +8,7 @@ The `Base` class is envisioned as the foundational component for any object type Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. -Below a scheme of how we envision the architecture of the `neurostatslib` models. +Below a scheme of how we envision the architecture of the `nemos` models. ``` Abstract Class Base @@ -41,7 +41,7 @@ Abstract Class Base ``` !!! Example - The current package version includes a concrete class named `neurostatslib.glm.GLM`. This class inherits from `BaseRegressor`, which in turn inherits `Base`, since it falls under the " GLM regression" category. + The current package version includes a concrete class named `nemos.glm.GLM`. This class inherits from `BaseRegressor`, which in turn inherits `Base`, since it falls under the " GLM regression" category. As any `BaseRegressor`, it **must** implement the `fit`, `score`, `predict`, and `simulate` methods. diff --git a/docs/developers_notes/05-glm.md b/docs/developers_notes/05-glm.md index d04061e2..ace3b5be 100644 --- a/docs/developers_notes/05-glm.md +++ b/docs/developers_notes/05-glm.md @@ -6,7 +6,7 @@ Generalized Linear Models (GLM) provide a flexible framework for modeling a variety of data types while establishing a relationship between multiple predictors and a response variable. A GLM extends the traditional linear regression by allowing for response variables that have error distribution models other than a normal distribution, such as binomial or Poisson distributions. -The `neurostatslib.glm` module currently offers implementations of two GLM classes: +The `nemos.glm` module currently offers implementations of two GLM classes: 1. **`GLM`:** A direct implementation of a feedforward GLM. 2. **`RecurrentGLM`:** An implementation of a recurrent GLM. This class inherits from `GLM` and redefines the `simulate` method to generate spikes akin to a recurrent neural network. @@ -15,7 +15,7 @@ Our design aligns with the `scikit-learn` API, facilitating seamless integration The classes provided here are modular by design offering a standard foundation for any GLM variant. -Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`neurostatslib.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) and [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) objects, respectively. +Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`nemos.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) and [`nemos.solver.Solver`](../05-solver/#the-abstract-class-solver) objects, respectively.

@@ -35,8 +35,8 @@ The `GLM` class provides a direct implementation of the GLM model and is designe ### Attributes -- **`solver`**: Refers to the optimization solver - an object of the [`neurostatslib.solver.Solver`](../05-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. -- **`observation_models`**: Represents the GLM observation model, which is an object of the [`neurostatslib.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. +- **`solver`**: Refers to the optimization solver - an object of the [`nemos.solver.Solver`](../05-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. +- **`observation_models`**: Represents the GLM observation model, which is an object of the [`nemos.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. - **`coef_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`intercept_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`solver_state`**: Indicates the solver's state. For specific solver states, refer to the [`jaxopt` documentation](https://jaxopt.github.io/stable/index.html#). @@ -71,6 +71,6 @@ When crafting a functional (i.e., concrete) GLM class: - **Must** inherit from `BaseRegressor` or one of its derivatives. - **Must** realize the `predict`, `fit`, `score`, and `simulate` methods, either directly or through inheritance. -- **Should** incorporate a `observation_models` attribute of type `neurostatslib.observation_models.Observations` to specify the link-function, emission probability, and likelihood. -- **Should** include a `solver` attribute of type `neurostatslib.solver.Solver` to establish the solver based on penalization type. +- **Should** incorporate a `observation_models` attribute of type `nemos.observation_models.Observations` to specify the link-function, emission probability, and likelihood. +- **Should** include a `solver` attribute of type `nemos.solver.Solver` to establish the solver based on penalization type. - **May** embed additional parameter and input checks if required by the specific GLM subclass. diff --git a/docs/examples/plot_1D_basis_function.py b/docs/examples/plot_1D_basis_function.py index 2344d0ea..f0c3aa28 100644 --- a/docs/examples/plot_1D_basis_function.py +++ b/docs/examples/plot_1D_basis_function.py @@ -12,17 +12,17 @@ - The order of the spline, which should be an integer greater than 1. """ -import matplotlib.pylab as plt import numpy as np +import matplotlib.pylab as plt -import neurostatslib as nsl +import nemos as nmo # Initialize hyperparameters order = 4 n_basis = 10 # Define the 1D basis function object -mspline_basis = nsl.basis.MSplineBasis(n_basis_funcs=n_basis, order=order) +mspline_basis = nmo.basis.MSplineBasis(n_basis_funcs=n_basis, order=order) # %% # Evaluating a Basis @@ -64,12 +64,12 @@ # Other Basis Types # ----------------- # Each basis type may necessitate specific hyperparameters for instantiation. For a comprehensive description, -# please refer to the [Code References](../../../reference/neurostatslib/basis). After instantiation, all classes +# please refer to the [Code References](../../../reference/generalized-linear-models/basis). After instantiation, all classes # share the same syntax for basis evaluation. The following is an example of how to instantiate and # evaluate a log-spaced cosine raised function basis. # Instantiate the basis noting that the `RaisedCosineBasisLog` does not require an `order` parameter -raised_cosine_log = nsl.basis.RaisedCosineBasisLog(n_basis_funcs=10) +raised_cosine_log = nmo.basis.RaisedCosineBasisLog(n_basis_funcs=10) # Evaluate the raised cosine basis at the equi-spaced sample points # (same method in all Basis elements) diff --git a/mkdocs.yml b/mkdocs.yml index 79df1891..dd7d1d40 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ -site_name: neurostatslib -repo_url: https://github.com/flatironinstitute/generalized-linear-models +site_name: generalized-linear-models +repo_url: https://github.com/flatironinstitute/nemos theme: name: 'material' # The theme name, using the 'material' theme diff --git a/pyproject.toml b/pyproject.toml index 4981a5a9..ec51de06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools", "setuptools-scm"] build-backend = "setuptools.build_meta" [project] -name = "neurostatslib" +name = "nemos" version = "0.1.0" authors = [ {name = "Edoardo Balzani", email = "ebalzani@flatironinstitute.org"}, @@ -11,7 +11,7 @@ authors = [ {name = "Guillaume Vejo", email = "gviejo@flatironinstitute.org"}, {name = "Alex Williams", email = "alex.h.williams@nyu.edu"} ] -description = "Toolbox for basic Generalized Linear Models (GLMs) for neural data analysis" +description = "NEural MOdelS, a statistical modeling framework for neuroscience." readme = "README.md" requires-python = ">=3.8" keywords = ["neuroscience", "Poisson-GLM"] @@ -41,7 +41,7 @@ dependencies = [ # Configure package discovery for setuptools [tool.setuptools.packages.find] where = ["src"] # The directory where package modules are located -include = ["neurostatslib"] # The specific package(s) to include in the distribution +include = ["nemos"] # The specific package(s) to include in the distribution # Define optional dependencies for the project @@ -54,8 +54,6 @@ dev = [ "flake8", # Code linter "coverage", # Test coverage measurement "pytest-cov", # Test coverage plugin for pytest - "statsmodels", # Used to compare model pseudo-r2 in testing - "scikit-learn", # Testing compatibility with CV & pipelines ] docs = [ "mkdocs", # Documentation generator @@ -99,8 +97,8 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] +addopts = "--cov=nemos" # Additional options to pass to pytest, enabling coverage for the 'generalized-linear-models' package testpaths = ["tests"] # Specify the directory where test files are located -addopts = "--cov=src" [tool.coverage.report] exclude_lines = [ diff --git a/src/neurostatslib/base_class.py b/src/nemos/base_class.py similarity index 98% rename from src/neurostatslib/base_class.py rename to src/nemos/base_class.py index 7182276a..5b82ad06 100644 --- a/src/neurostatslib/base_class.py +++ b/src/nemos/base_class.py @@ -14,7 +14,7 @@ class Base: - """Base class for neurostatslib estimators. + """Base class for generalized-linear-models estimators. A base class for estimators with utilities for getting and setting parameters, and for interacting with specific devices like CPU, GPU, and TPU. @@ -209,8 +209,8 @@ class BaseRegressor(Base, abc.ABC): -------- Concrete models: - - [`GLM`](../glm/#neurostatslib.glm.GLM): A feed-forward GLM implementation. - - [`GLMRecurrent`](../glm/#neurostatslib.glm.GLMRecurrent): A recurrent GLM implementation. + - [`GLM`](../glm/#generalized-linear-models.glm.GLM): A feed-forward GLM implementation. + - [`GLMRecurrent`](../glm/#generalized-linear-models.glm.GLMRecurrent): A recurrent GLM implementation. """ FLOAT_EPS = jnp.finfo(float).eps diff --git a/src/neurostatslib/glm.py b/src/nemos/glm.py similarity index 97% rename from src/neurostatslib/glm.py rename to src/nemos/glm.py index 2eaea0bb..5ea739c3 100644 --- a/src/neurostatslib/glm.py +++ b/src/nemos/glm.py @@ -43,8 +43,8 @@ class GLM(BaseRegressor): Raises ------ TypeError - If provided `solver` or `observation_model` are not valid or implemented in `neurostatslib.solver` and - `neurostatslib.observation_models` respectively. + If provided `solver` or `observation_model` are not valid or implemented in `generalized-linear-models.solver` and + `generalized-linear-models.observation_models` respectively. """ def __init__( @@ -155,11 +155,11 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: See Also -------- - - [score](./#neurostatslib.glm.GLM.score) + - [score](./#generalized-linear-models.glm.GLM.score) Score predicted rates against target spike counts. - - [simulate (feed-forward only)](../glm/#neurostatslib.glm.GLM.simulate) + - [simulate (feed-forward only)](../glm/#generalized-linear-models.glm.GLM.simulate) Simulate neural activity in response to a feed-forward input . - - [simulate (feed-forward + coupling)](../glm/#neurostatslib.glm.GLMRecurrent.simulate) + - [simulate (feed-forward + coupling)](../glm/#generalized-linear-models.glm.GLMRecurrent.simulate) Simulate neural activity in response to a feed-forward input using the GLM as a recurrent network. """ @@ -291,7 +291,7 @@ def score( predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate, see references[$^1$](#--references). - Refer to the `nsl.observation_models.Observations` concrete subclasses for the specific likelihood equations. + Refer to the `nmo.observation_models.Observations` concrete subclasses for the specific likelihood equations. References @@ -417,7 +417,7 @@ def simulate( See Also -------- - [predict](./#neurostatslib.glm.GLM.predict) : + [predict](./#generalized-linear-models.glm.GLM.predict) : Method to predict rates based on the model's parameters. """ # check if the model is fit @@ -454,7 +454,7 @@ class GLMRecurrent(GLM): See Also -------- - [GLM](./#neurostatslib.glm.GLM) : Base class for the generalized linear model. + [GLM](./#generalized-linear-models.glm.GLM) : Base class for the generalized linear model. Notes ----- @@ -523,7 +523,7 @@ def simulate_recurrent( See Also -------- - [predict](./#neurostatslib.glm.GLM.predict) : + [predict](./#generalized-linear-models.glm.GLM.predict) : Method to predict rates based on the model's parameters. Notes diff --git a/src/neurostatslib/solver.py b/src/nemos/solver.py similarity index 99% rename from src/neurostatslib/solver.py rename to src/nemos/solver.py index 07260af5..95ec01e5 100644 --- a/src/neurostatslib/solver.py +++ b/src/nemos/solver.py @@ -207,7 +207,7 @@ class are defined in the `allowed_optimizers` attribute. See Also -------- - [Solver](./#neurostatslib.solver.Solver) : Base solver class from which this class inherits. + [Solver](./#generalized-linear-models.solver.Solver) : Base solver class from which this class inherits. """ allowed_algorithms = [ diff --git a/tests/conftest.py b/tests/conftest.py index 0a60dcc0..8cec475c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,12 @@ """ -Testing configurations for the `neurostatslib` library. +Testing configurations for the `generalized-linear-models` library. This module contains test fixtures required to set up and verify the functionality -of the modules of the `neurostatslib` library. +of the modules of the `generalized-linear-models` library. Note: This module primarily serves as a utility for test configurations, setting up initial conditions, - and loading predefined parameters for testing various functionalities of the `neurostatslib` library. + and loading predefined parameters for testing various functionalities of the `generalized-linear-models` library. """ import inspect import json @@ -17,7 +17,7 @@ import numpy as np import pytest -import neurostatslib as nsl +import nemos as nmo @pytest.fixture @@ -32,7 +32,7 @@ def poissonGLM_model_instantiation(): tuple: A tuple containing: - X (numpy.ndarray): Simulated input data. - np.random.poisson(rate) (numpy.ndarray): Simulated spike responses. - - model (nsl.glm.PoissonGLM): Initialized model instance. + - model (nmo.glm.PoissonGLM): Initialized model instance. - (w_true, b_true) (tuple): True weight and bias parameters. - rate (jax.numpy.ndarray): Simulated rate of response. """ @@ -40,9 +40,9 @@ def poissonGLM_model_instantiation(): X = np.random.normal(size=(100, 1, 5)) b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) - observation_model = nsl.observation_models.PoissonObservations(jnp.exp) - solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) - model = nsl.glm.GLM(observation_model, solver) + observation_model = nmo.observation_models.PoissonObservations(jnp.exp) + solver = nmo.solver.UnRegularizedSolver('GradientDescent', {}) + model = nmo.glm.GLM(observation_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -56,7 +56,7 @@ def poissonGLM_coupled_model_config_simulate(): Returns: tuple: A tuple containing: - - model (nsl.glm.PoissonGLM): Initialized model instance. + - model (nmo.glm.PoissonGLM): Initialized model instance. - coupling_basis (jax.numpy.ndarray): Coupling basis values from the config. - feedforward_input (jax.numpy.ndarray): Feedforward input values from the config. - init_spikes (jax.numpy.ndarray): Initial spike values from the config. @@ -68,9 +68,9 @@ def poissonGLM_coupled_model_config_simulate(): "simulate_coupled_neurons_params.json"), "r") as fh: config_dict = json.load(fh) - observations = nsl.observation_models.PoissonObservations(jnp.exp) - solver = nsl.solver.RidgeSolver("BFGS", regularizer_strength=0.1) - model = nsl.glm.GLMRecurrent(observation_model=observations, solver=solver) + observations = nmo.observation_models.PoissonObservations(jnp.exp) + solver = nmo.solver.RidgeSolver("BFGS", regularizer_strength=0.1) + model = nmo.glm.GLMRecurrent(observation_model=observations, solver=solver) model.coef_ = jnp.asarray(config_dict["coef_"]) model.intercept_ = jnp.asarray(config_dict["intercept_"]) coupling_basis = jnp.asarray(config_dict["coupling_basis"]) @@ -104,7 +104,7 @@ def group_sparse_poisson_glm_model_instantiation(): tuple: A tuple containing: - X (numpy.ndarray): Simulated input data. - np.random.poisson(rate) (numpy.ndarray): Simulated spike responses. - - model (nsl.glm.PoissonGLM): Initialized model instance. + - model (nmo.glm.PoissonGLM): Initialized model instance. - (w_true, b_true) (tuple): True weight and bias parameters. - rate (jax.numpy.ndarray): Simulated rate of response. """ @@ -116,9 +116,9 @@ def group_sparse_poisson_glm_model_instantiation(): mask = np.zeros((2, 5)) mask[0, 1:4] = 1 mask[1, [0,4]] = 1 - observation_model = nsl.observation_models.PoissonObservations(jnp.exp) - solver = nsl.solver.UnRegularizedSolver('GradientDescent', {}) - model = nsl.glm.GLM(observation_model, solver) + observation_model = nmo.observation_models.PoissonObservations(jnp.exp) + solver = nmo.solver.UnRegularizedSolver('GradientDescent', {}) + model = nmo.glm.GLM(observation_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask @@ -137,17 +137,17 @@ def example_data_prox_operator(): @pytest.fixture def poisson_observation_model(): - return nsl.observation_models.PoissonObservations(jnp.exp) + return nmo.observation_models.PoissonObservations(jnp.exp) @pytest.fixture def ridge_solver(): - return nsl.solver.RidgeSolver(solver_name="LBFGS", regularizer_strength=0.1) + return nmo.solver.RidgeSolver(solver_name="LBFGS", regularizer_strength=0.1) @pytest.fixture def lasso_solver(): - return nsl.solver.LassoSolver(solver_name="ProximalGradient", regularizer_strength=0.1) + return nmo.solver.LassoSolver(solver_name="ProximalGradient", regularizer_strength=0.1) @pytest.fixture @@ -155,7 +155,7 @@ def group_lasso_2groups_5features_solver(): mask = np.zeros((2, 5)) mask[0, :2] = 1 mask[1, 2:] = 1 - return nsl.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) + return nmo.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) @pytest.fixture diff --git a/tests/test_proximal_operator.py b/tests/test_proximal_operator.py index 96ba47a5..432db4c5 100644 --- a/tests/test_proximal_operator.py +++ b/tests/test_proximal_operator.py @@ -1,6 +1,6 @@ import jax.numpy as jnp -from neurostatslib.proximal_operator import _vmap_norm2_masked_2, prox_group_lasso +from nemos.proximal_operator import _vmap_norm2_masked_2, prox_group_lasso def test_prox_group_lasso_returns_tuple(example_data_prox_operator): From b315f46c2803e699b6e6f6ee0557bba9d02f0374 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:09:31 -0500 Subject: [PATCH 174/250] removed refs to neurostatslib --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index dd7d1d40..660465a8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: generalized-linear-models +site_name: nemos repo_url: https://github.com/flatironinstitute/nemos theme: From 1f408fadb864db44960161483b9f37549d7fd44c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:13:29 -0500 Subject: [PATCH 175/250] removed gen lin models and linted --- pyproject.toml | 2 +- src/nemos/glm.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ec51de06..19f2b0a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] -addopts = "--cov=nemos" # Additional options to pass to pytest, enabling coverage for the 'generalized-linear-models' package +addopts = "--cov=nemos" # Additional options to pass to pytest, enabling coverage for the 'nemos' package testpaths = ["tests"] # Specify the directory where test files are located [tool.coverage.report] diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 5ea739c3..744dc115 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -547,7 +547,7 @@ def simulate_recurrent( n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.intercept_.shape[0] - w_feedforward = self.coef_[:, n_basis_coupling * n_neurons:] + w_feedforward = self.coef_[:, n_basis_coupling * n_neurons :] w_recurrent = self.coef_[:, : n_basis_coupling * n_neurons] bs = self.intercept_ From 77065a047916dec1bfb25f2b30360add7c45a3ea Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:17:51 -0500 Subject: [PATCH 176/250] linted --- src/nemos/glm.py | 4 ++-- src/nemos/observation_models.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 744dc115..0e491d7b 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -43,8 +43,8 @@ class GLM(BaseRegressor): Raises ------ TypeError - If provided `solver` or `observation_model` are not valid or implemented in `generalized-linear-models.solver` and - `generalized-linear-models.observation_models` respectively. + If provided `solver` or `observation_model` are not valid or implemented in `nemos.solver` and + `nemos.observation_models` respectively. """ def __init__( diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 40b29e02..ece46a7d 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -36,7 +36,7 @@ class Observations(Base, abc.ABC): See Also -------- - [PoissonObservations](./#generalized-linear-models.observation_models.PoissonObservations) : A specific implementation of a + [PoissonObservations](./#nemos.observation_models.PoissonObservations) : A specific implementation of a observation model using the Poisson distribution. """ From b902219aeda20cc6857dc9c7cf98e9d50bd4c440 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:20:15 -0500 Subject: [PATCH 177/250] removed gen-lin-mod --- docs/examples/plot_1D_basis_function.py | 4 ++-- docs/examples/plot_ND_basis_function.py | 2 +- docs/examples/plot_example_convolution.py | 4 ++-- docs/examples/plot_glm_demo.py | 6 +++--- src/nemos/base_class.py | 6 +++--- src/nemos/exceptions.py | 4 ++-- src/nemos/glm.py | 12 ++++++------ src/nemos/observation_models.py | 2 +- src/nemos/solver.py | 2 +- tests/conftest.py | 6 +++--- 10 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/examples/plot_1D_basis_function.py b/docs/examples/plot_1D_basis_function.py index f0c3aa28..b6b8bf94 100644 --- a/docs/examples/plot_1D_basis_function.py +++ b/docs/examples/plot_1D_basis_function.py @@ -12,8 +12,8 @@ - The order of the spline, which should be an integer greater than 1. """ -import numpy as np import matplotlib.pylab as plt +import numpy as np import nemos as nmo @@ -64,7 +64,7 @@ # Other Basis Types # ----------------- # Each basis type may necessitate specific hyperparameters for instantiation. For a comprehensive description, -# please refer to the [Code References](../../../reference/generalized-linear-models/basis). After instantiation, all classes +# please refer to the [Code References](../../../reference/nemos/basis). After instantiation, all classes # share the same syntax for basis evaluation. The following is an example of how to instantiate and # evaluate a log-spaced cosine raised function basis. diff --git a/docs/examples/plot_ND_basis_function.py b/docs/examples/plot_ND_basis_function.py index 94cbd36f..5f34a679 100644 --- a/docs/examples/plot_ND_basis_function.py +++ b/docs/examples/plot_ND_basis_function.py @@ -63,8 +63,8 @@ # $$ # Here, we simply add two basis objects, `a_basis` and `b_basis`, together to define the additive basis. -import numpy as np import matplotlib.pyplot as plt +import numpy as np import nemos as nmo diff --git a/docs/examples/plot_example_convolution.py b/docs/examples/plot_example_convolution.py index ddcb1675..c40934d6 100644 --- a/docs/examples/plot_example_convolution.py +++ b/docs/examples/plot_example_convolution.py @@ -6,9 +6,9 @@ # ## Generate synthetic data # Generate some simulated spike counts. -import numpy as np -import matplotlib.pylab as plt import matplotlib.patches as patches +import matplotlib.pylab as plt +import numpy as np import nemos as nmo diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index a56bd3f1..7f267b76 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -56,13 +56,13 @@ # ## The Feed-Forward GLM # # ### Model Definition -# The class implementing the feed-forward GLM is `generalized-linear-models.glm.GLM`. +# The class implementing the feed-forward GLM is `nemos.glm.GLM`. # In order to define the class, one **must** provide: # # - **Observation Model**: The observation model for the GLM, e.g. an object of the class of type -# `generalized-linear-models.observation_models.Observations`. So far, only the `PoissonObservations` +# `nemos.observation_models.Observations`. So far, only the `PoissonObservations` # model has been implemented. -# - **Solver**: The desired solver, e.g. an object of the `generalized-linear-models.solver.Solver` class. +# - **Solver**: The desired solver, e.g. an object of the `nemos.solver.Solver` class. # Currently, we implemented the un-regularized, Ridge, Lasso, and Group-Lasso solver. # # The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge solver. diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 5b82ad06..8611e9c3 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -14,7 +14,7 @@ class Base: - """Base class for generalized-linear-models estimators. + """Base class for nemos estimators. A base class for estimators with utilities for getting and setting parameters, and for interacting with specific devices like CPU, GPU, and TPU. @@ -209,8 +209,8 @@ class BaseRegressor(Base, abc.ABC): -------- Concrete models: - - [`GLM`](../glm/#generalized-linear-models.glm.GLM): A feed-forward GLM implementation. - - [`GLMRecurrent`](../glm/#generalized-linear-models.glm.GLMRecurrent): A recurrent GLM implementation. + - [`GLM`](../glm/#nemos.glm.GLM): A feed-forward GLM implementation. + - [`GLMRecurrent`](../glm/#nemos.glm.GLMRecurrent): A recurrent GLM implementation. """ FLOAT_EPS = jnp.finfo(float).eps diff --git a/src/nemos/exceptions.py b/src/nemos/exceptions.py index 7e6b2678..bcb44de7 100644 --- a/src/nemos/exceptions.py +++ b/src/nemos/exceptions.py @@ -9,8 +9,8 @@ class NotFittedError(ValueError, AttributeError): Examples -------- - >>> from generalized-linear-models.glm import GLM - >>> from generalized-linear-models.exceptions import NotFittedError + >>> from nemos.glm import GLM + >>> from nemos.exceptions import NotFittedError >>> try: ... GLM().predict([[[1, 2], [2, 3], [3, 4]]]) ... except NotFittedError as e: diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 0e491d7b..a1d19212 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -155,11 +155,11 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: See Also -------- - - [score](./#generalized-linear-models.glm.GLM.score) + - [score](./#nemos.glm.GLM.score) Score predicted rates against target spike counts. - - [simulate (feed-forward only)](../glm/#generalized-linear-models.glm.GLM.simulate) + - [simulate (feed-forward only)](../glm/#nemos.glm.GLM.simulate) Simulate neural activity in response to a feed-forward input . - - [simulate (feed-forward + coupling)](../glm/#generalized-linear-models.glm.GLMRecurrent.simulate) + - [simulate (feed-forward + coupling)](../glm/#nemos.glm.GLMRecurrent.simulate) Simulate neural activity in response to a feed-forward input using the GLM as a recurrent network. """ @@ -417,7 +417,7 @@ def simulate( See Also -------- - [predict](./#generalized-linear-models.glm.GLM.predict) : + [predict](./#nemos.glm.GLM.predict) : Method to predict rates based on the model's parameters. """ # check if the model is fit @@ -454,7 +454,7 @@ class GLMRecurrent(GLM): See Also -------- - [GLM](./#generalized-linear-models.glm.GLM) : Base class for the generalized linear model. + [GLM](./#nemos.glm.GLM) : Base class for the generalized linear model. Notes ----- @@ -523,7 +523,7 @@ def simulate_recurrent( See Also -------- - [predict](./#generalized-linear-models.glm.GLM.predict) : + [predict](./#nemos.glm.GLM.predict) : Method to predict rates based on the model's parameters. Notes diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index ece46a7d..aefb1156 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -279,7 +279,7 @@ class PoissonObservations(Observations): See Also -------- - [Observations](./#generalized-linear-models.observation_models.Observations) : Base class for observation models. + [Observations](./#nemos.observation_models.Observations) : Base class for observation models. """ def __init__(self, inverse_link_function=jnp.exp): diff --git a/src/nemos/solver.py b/src/nemos/solver.py index 95ec01e5..b5f53f49 100644 --- a/src/nemos/solver.py +++ b/src/nemos/solver.py @@ -207,7 +207,7 @@ class are defined in the `allowed_optimizers` attribute. See Also -------- - [Solver](./#generalized-linear-models.solver.Solver) : Base solver class from which this class inherits. + [Solver](./#nemos.solver.Solver) : Base solver class from which this class inherits. """ allowed_algorithms = [ diff --git a/tests/conftest.py b/tests/conftest.py index 8cec475c..803b48c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,12 @@ """ -Testing configurations for the `generalized-linear-models` library. +Testing configurations for the `nemos` library. This module contains test fixtures required to set up and verify the functionality -of the modules of the `generalized-linear-models` library. +of the modules of the `nemos` library. Note: This module primarily serves as a utility for test configurations, setting up initial conditions, - and loading predefined parameters for testing various functionalities of the `generalized-linear-models` library. + and loading predefined parameters for testing various functionalities of the `nemos` library. """ import inspect import json From 3aee2698f6165e8438448565fe5fea8e2459ce68 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:21:19 -0500 Subject: [PATCH 178/250] added dev deps --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 19f2b0a0..a0e608b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,8 @@ dev = [ "flake8", # Code linter "coverage", # Test coverage measurement "pytest-cov", # Test coverage plugin for pytest + "statsmodels", + "scikit-learn" ] docs = [ "mkdocs", # Documentation generator From cb381ffee78ffa81c025e011f975602dc6c869a6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:26:50 -0500 Subject: [PATCH 179/250] commit pyproject.toml --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a0e608b2..5885c24b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,8 +54,8 @@ dev = [ "flake8", # Code linter "coverage", # Test coverage measurement "pytest-cov", # Test coverage plugin for pytest - "statsmodels", - "scikit-learn" + "statsmodels", # Used to compare model pseudo-r2 in testing + "scikit-learn", # Testing compatibility with CV & pipelines ] docs = [ "mkdocs", # Documentation generator @@ -99,8 +99,8 @@ profile = "black" # Configure pytest [tool.pytest.ini_options] -addopts = "--cov=nemos" # Additional options to pass to pytest, enabling coverage for the 'nemos' package testpaths = ["tests"] # Specify the directory where test files are located +addopts = "--cov=src" [tool.coverage.report] exclude_lines = [ From db62140ff69cc0c24639d5934bf85b6330c8c23b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:30:16 -0500 Subject: [PATCH 180/250] linted --- src/nemos/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index a1d19212..4f711d9c 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -547,7 +547,7 @@ def simulate_recurrent( n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.intercept_.shape[0] - w_feedforward = self.coef_[:, n_basis_coupling * n_neurons :] + w_feedforward = self.coef_[:, n_basis_coupling * n_neurons:] w_recurrent = self.coef_[:, : n_basis_coupling * n_neurons] bs = self.intercept_ From 724522f8def3f8ff61eaee94ae842c52d32d150b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:35:42 -0500 Subject: [PATCH 181/250] fixed links --- docs/developers_notes/03-observation_models.md | 2 +- docs/developers_notes/04-solver.md | 2 +- docs/developers_notes/05-glm.md | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developers_notes/03-observation_models.md b/docs/developers_notes/03-observation_models.md index a05bf4b0..190686fd 100644 --- a/docs/developers_notes/03-observation_models.md +++ b/docs/developers_notes/03-observation_models.md @@ -4,7 +4,7 @@ The `observation_models` module provides objects representing the observations of GLM-like models. -The abstract class `Observations` defines the structure of the subclasses which specify observation types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`nemos.glm.GLM`](../03-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. +The abstract class `Observations` defines the structure of the subclasses which specify observation types, such as Poisson, Gamma, etc. These objects serve as attributes of the [`nemos.glm.GLM`](../05-glm/#the-concrete-class-glm) class, equipping the GLM with a negative log-likelihood. This is used to define the optimization objective, the deviance which measures model fit quality, and the emission of new observations, for simulating new data. ## The Abstract class `Observations` diff --git a/docs/developers_notes/04-solver.md b/docs/developers_notes/04-solver.md index fac92584..bf855460 100644 --- a/docs/developers_notes/04-solver.md +++ b/docs/developers_notes/04-solver.md @@ -4,7 +4,7 @@ The `solver` module introduces an archetype class `Solver` which provides the structural components for each concrete sub-class. -Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../03-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. +Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../05-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). We choose to rely on `jaxopt` because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX, that are highly customizable. diff --git a/docs/developers_notes/05-glm.md b/docs/developers_notes/05-glm.md index ace3b5be..0ce65e20 100644 --- a/docs/developers_notes/05-glm.md +++ b/docs/developers_notes/05-glm.md @@ -15,7 +15,7 @@ Our design aligns with the `scikit-learn` API, facilitating seamless integration The classes provided here are modular by design offering a standard foundation for any GLM variant. -Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`nemos.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) and [`nemos.solver.Solver`](../05-solver/#the-abstract-class-solver) objects, respectively. +Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`nemos.observation_models.Observations`](../03-observation_models/#the-abstract-class-observations) and [`nemos.solver.Solver`](../04-solver/#the-abstract-class-solver) objects, respectively.
@@ -35,8 +35,8 @@ The `GLM` class provides a direct implementation of the GLM model and is designe ### Attributes -- **`solver`**: Refers to the optimization solver - an object of the [`nemos.solver.Solver`](../05-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. -- **`observation_models`**: Represents the GLM observation model, which is an object of the [`nemos.observation_models.Observations`](../04-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. +- **`solver`**: Refers to the optimization solver - an object of the [`nemos.solver.Solver`](../04-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. +- **`observation_models`**: Represents the GLM observation model, which is an object of the [`nemos.observation_models.Observations`](../03-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. - **`coef_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`intercept_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`solver_state`**: Indicates the solver's state. For specific solver states, refer to the [`jaxopt` documentation](https://jaxopt.github.io/stable/index.html#). From 15f062cdb755e25f8b6cadfa668d0491c340e6b4 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:38:39 -0500 Subject: [PATCH 182/250] fixed links --- src/nemos/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 4f711d9c..de298cb5 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -159,7 +159,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Score predicted rates against target spike counts. - [simulate (feed-forward only)](../glm/#nemos.glm.GLM.simulate) Simulate neural activity in response to a feed-forward input . - - [simulate (feed-forward + coupling)](../glm/#nemos.glm.GLMRecurrent.simulate) + - [simulate_recurrent (feed-forward + coupling)](../glm/#nemos.glm.GLMRecurrent.simulate_recurrent) Simulate neural activity in response to a feed-forward input using the GLM as a recurrent network. """ From 1ef0eda8ccc0ade51510310d8c826adba45930d8 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 7 Nov 2023 18:43:53 -0500 Subject: [PATCH 183/250] linted --- src/nemos/glm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index de298cb5..35b66fc8 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -547,7 +547,7 @@ def simulate_recurrent( n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.intercept_.shape[0] - w_feedforward = self.coef_[:, n_basis_coupling * n_neurons:] + w_feedforward = self.coef_[:, n_basis_coupling * n_neurons :] w_recurrent = self.coef_[:, : n_basis_coupling * n_neurons] bs = self.intercept_ From a2a5cb7dd90f28086a66c619b3256fd79b690e63 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 8 Nov 2023 11:11:23 -0500 Subject: [PATCH 184/250] fixed spacing --- docs/examples/plot_glm_demo.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 7f267b76..f154ccf7 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -158,6 +158,7 @@ # back-end. # # Here is an example of how we can perform 5-fold cross-validation via `scikit-learn`. +# # **Ridge** parameter_grid = {"solver__regularizer_strength": np.logspace(-1.5, 1.5, 6)} @@ -253,11 +254,11 @@ n_basis_coupling = coupling_basis.shape[1] fig, axs = plt.subplots(2,2) plt.suptitle("Coupling filters") -for neu_i in range(2): - for neu_j in range(2): - axs[neu_i,neu_j].set_title(f"neu {neu_j} -> neu {neu_i}") - coeff = basis_coeff[neu_i, neu_j*n_basis_coupling: (neu_j+1)*n_basis_coupling] - axs[neu_i, neu_j].plot(np.dot(coupling_basis, coeff)) +for unit_i in range(2): + for unit_j in range(2): + axs[unit_i,unit_j].set_title(f"unit {unit_j} -> unit {unit_i}") + coeff = basis_coeff[unit_i, unit_j * n_basis_coupling: (unit_j + 1) * n_basis_coupling] + axs[unit_i, unit_j].plot(np.dot(coupling_basis, coeff)) plt.tight_layout() fig, axs = plt.subplots(1,1) From 4ea027655a0b4774c21c276cbbe3e8d481a9ba98 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 8 Nov 2023 11:21:11 -0500 Subject: [PATCH 185/250] fixed the shapes --- src/nemos/glm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 35b66fc8..407dd9e3 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -140,7 +140,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Returns ------- : - The predicted rates with shape (n_neurons, n_time_bins). + The predicted rates with shape (n_time_bins, n_neurons). Raises ------ @@ -395,15 +395,15 @@ def simulate( feedforward_input : External input matrix to the model, representing factors like convolved currents, light intensities, etc. When not provided, the simulation is done with coupling-only. - Expected shape: (n_timesteps, n_neurons, n_basis_input). + Expected shape: (n_time_bins, n_neurons, n_basis_input). Returns ------- simulated_activity : Simulated activity (spike counts for PoissonGLMs) for each neuron over time. - Shape: (n_neurons, n_timesteps). + Shape: (n_time_bins, n_neurons). firing_rates : - Simulated rates for each neuron over time. Shape, (n_neurons, n_timesteps). + Simulated rates for each neuron over time. Shape, (n_neurons, n_time_bins). Raises ------ @@ -495,7 +495,7 @@ def simulate_recurrent( feedforward_input : External input matrix to the model, representing factors like convolved currents, light intensities, etc. When not provided, the simulation is done with coupling-only. - Expected shape: (n_timesteps, n_neurons, n_basis_input). + Expected shape: (n_time_bins, n_neurons, n_basis_input). init_y : Initial observation (spike counts for PoissonGLM) matrix that kickstarts the simulation. Expected shape: (window_size, n_neurons). @@ -507,9 +507,9 @@ def simulate_recurrent( ------- simulated_activity : Simulated activity (spike counts for PoissonGLMs) for each neuron over time. - Shape, (n_neurons, n_timesteps). + Shape, (n_time_bins, n_neurons). firing_rates : - Simulated rates for each neuron over time. Shape, (n_neurons, n_timesteps). + Simulated rates for each neuron over time. Shape, (n_time_bins, n_neurons,). Raises ------ From 2a389cf83f876606d6e2b22ed4af626dfe956d57 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:03:55 -0500 Subject: [PATCH 186/250] removed unused funcs --- src/nemos/utils.py | 58 ---------------------------------------- tests/test_base_class.py | 4 ++- 2 files changed, 3 insertions(+), 59 deletions(-) diff --git a/src/nemos/utils.py b/src/nemos/utils.py index 1bf495d9..f8581c89 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -387,28 +387,6 @@ def row_wise_kron(a, c): return K -def has_local_device(device_type: str) -> bool: - """Scan for local device availability. - - Looks for local device availability and returns True if the specified - type of device (e.g., GPU, TPU) is available. - - Parameters - ---------- - device_type: - The device type in lower-case, e.g. `gpu`, `tpu`... - - Returns - ------- - : - True if the jax finds the device, False otherwise. - - """ - return any( - device_type in device.device_kind.lower() for device in jax.local_devices() - ) - - def is_list_like(obj) -> bool: """Check if the object is an iterable (not including strings or bytes) that supports item retrieval by index but isn't a dictionary.""" @@ -459,42 +437,6 @@ def check_invalid_entry(array: jnp.ndarray) -> None: raise ValueError("Input array contains NaNs!") -def count_positional_params(func: Callable) -> int: - """ - Count the number of positional parameters a function accepts. - - This function counts the number of POSITIONAL_OR_KEYWORD and POSITIONAL_ONLY parameters. - - For example, all the following callable will return a count of two: - - - `def func(x, y, *args)`, since x and y are POSITIONAL_OR_KEYWORD, *args is VAR_POSITIONAL - - `def func(x, y, *args, z)`, since x and y are POSITIONAL_OR_KEYWORD, - *args is VAR_POSITIONAL, z is KEYWORD_ONLY - - `def func(x, y, *args, z, **kwargs)`, since x and y are POSITIONAL_OR_KEYWORD, - *args is VAR_POSITIONAL, z is KEYWORD_ONLY, - **kwargs is VAR_KEYWORD - - `def func(x, /, y, *args, z, **kwargs)`, since x POSITIONAL_ONLY, y are POSITIONAL_OR_KEYWORD, - *args is VAR_POSITIONAL, z is KEYWORD_ONLY, **kwargs is VAR_KEYWORD - - Parameters - ---------- - func : - The function whose signature is to be inspected. - - Returns - ------- - : - The count of POSITIONAL_OR_KEYWORD parameters for the given function. - """ - # Count parameters excluding any *args or **kwargs - return sum( - 1 - for param in inspect.signature(func).parameters.values() - if param.kind - in [inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.POSITIONAL_ONLY] - ) - - def assert_has_attribute(obj: Any, attr_name: str): """Ensure the object has the given attribute.""" if not hasattr(obj, attr_name): diff --git a/tests/test_base_class.py b/tests/test_base_class.py index ae0f3335..f2384e44 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -323,7 +323,9 @@ def test_target_device_put(device_name: Literal["cpu", "gpu", "tpu"], mock_regre Put array to device and checks that the device is matched after put, if device is found. Raise error otherwise. """ - raise_exception = not nmo.utils.has_local_device(device_name) + raise_exception = not any( + device_name in device.device_kind.lower() for device in jax.local_devices() + ) x = jnp.array([1]) if raise_exception: if device_name != "tpu": From 252b78ba39b0c2b6fc8f5b8869d4087c2e9acefb Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:21:34 -0500 Subject: [PATCH 187/250] fixed test --- src/nemos/base_class.py | 22 ++++++++++------------ src/nemos/utils.py | 10 ---------- tests/test_glm.py | 4 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 8611e9c3..70fccb8e 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -10,7 +10,7 @@ import jax.numpy as jnp from numpy.typing import ArrayLike, NDArray -from .utils import check_invalid_entry, convert_to_jnp_ndarray, is_list_like +from .utils import check_invalid_entry, convert_to_jnp_ndarray class Base: @@ -257,26 +257,24 @@ def _check_and_convert_params( It ensures that the parameters and data are compatible for the model. """ - if not is_list_like(params): - raise TypeError("Initial parameters must be array-like!") - - if len(params) != 2: - raise ValueError("Params needs to be array-like of length two.") - try: - params = ( - jnp.asarray(params[0], dtype=data_type), - jnp.asarray(params[1], dtype=data_type), + params = tuple( + jnp.asarray(par, dtype=data_type) + for par in params ) except (ValueError, TypeError): raise TypeError( - "Initial parameters must be array-like of array-like objects" + "Initial parameters must be array-like of array-like objects " "with numeric data-type!" ) + if len(params) != 2: + raise ValueError("Params needs to be array-like of length two.") + + if params[0].ndim != 2: raise ValueError( - "params[0] must be of shape (n_neurons, n_features), but" + "params[0] must be of shape (n_neurons, n_features), but " f"params[0] has {params[0].ndim} dimensions!" ) if params[1].ndim != 1: diff --git a/src/nemos/utils.py b/src/nemos/utils.py index f8581c89..ffab4d5a 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -387,16 +387,6 @@ def row_wise_kron(a, c): return K -def is_list_like(obj) -> bool: - """Check if the object is an iterable (not including strings or bytes) - that supports item retrieval by index but isn't a dictionary.""" - return ( - hasattr(obj, "__iter__") - and hasattr(obj, "__getitem__") - and not isinstance(obj, (str, bytes, dict)) - ) - - def convert_to_jnp_ndarray( *args: Union[NDArray, jnp.ndarray], data_type: Optional[jnp.dtype] = None ) -> Tuple[jnp.ndarray, ...]: diff --git a/tests/test_glm.py b/tests/test_glm.py index 2c92fa1d..8ef0fc47 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -159,10 +159,10 @@ def test_fit_intercepts_dimensionality(self, dim_intercepts, error, match_str, p "init_params, error, match_str", [ ([jnp.zeros((1, 5)), jnp.zeros((1,))], None, None), - (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), TypeError, "Initial parameters must be array-like"), + (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), None, None), (dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), TypeError, "Initial parameters must be array-like"), (0, TypeError, "Initial parameters must be array-like"), - ({0, 1}, TypeError, "Initial parameters must be array-like"), + ({0, 1}, ValueError, r"params\[0\] must be of shape"), ([jnp.zeros((1, 5)), ""], TypeError, "Initial parameters must be array-like"), (["", jnp.zeros((1,))], TypeError, "Initial parameters must be array-like") ] From 4592cfdf52484edd80a596558e8563196d3ea1a2 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:28:17 -0500 Subject: [PATCH 188/250] removed convert utils --- src/nemos/base_class.py | 9 ++++----- src/nemos/glm.py | 8 ++++---- src/nemos/utils.py | 21 --------------------- tests/test_base_class.py | 8 -------- 4 files changed, 8 insertions(+), 38 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 70fccb8e..8232458d 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -271,10 +271,9 @@ def _check_and_convert_params( if len(params) != 2: raise ValueError("Params needs to be array-like of length two.") - if params[0].ndim != 2: raise ValueError( - "params[0] must be of shape (n_neurons, n_features), but " + "params[0] must be of shape (n_neurons, n_features), but" f"params[0] has {params[0].ndim} dimensions!" ) if params[1].ndim != 1: @@ -401,7 +400,7 @@ def _preprocess_fit( ValueError If there are inconsistencies in the input shapes or if NaNs or Infs are detected. """ - X, y = convert_to_jnp_ndarray(X, y) + X, y = jnp.asarray(X, dtype=float), jnp.asarray(y, dtype=float) # check input dimensionality self._check_input_dimensionality(X, y) @@ -468,7 +467,7 @@ def _preprocess_simulate( If the feedforward_input contains NaNs or Infs. If the dimensionality or consistency checks fail for the provided data and parameters. """ - (feedforward_input,) = convert_to_jnp_ndarray(feedforward_input) + feedforward_input = jnp.asarray(feedforward_input, dtype=float) self._check_input_dimensionality(X=feedforward_input) self._check_input_and_params_consistency( params_feedforward, X=feedforward_input @@ -483,7 +482,7 @@ def _preprocess_simulate( ) # If both are provided, perform checks and conversions elif init_y is not None and params_recurrent is not None: - init_y = convert_to_jnp_ndarray(init_y)[0] + init_y = jnp.asarray(init_y, dtype=float) self._check_input_dimensionality(y=init_y) self._check_input_and_params_consistency(params_recurrent, y=init_y) return feedforward_input, init_y diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 407dd9e3..d1c04be5 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -169,7 +169,7 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: Ws = self.coef_ bs = self.intercept_ - (X,) = utils.convert_to_jnp_ndarray(X) + X = jnp.asarray(X, dtype=float) # check input dimensionality self._check_input_dimensionality(X=X) @@ -304,7 +304,7 @@ def score( Ws = self.coef_ bs = self.intercept_ - X, y = utils.convert_to_jnp_ndarray(X, y) + X, y = jnp.asarray(X, dtype=float), jnp.asarray(y, dtype=float) self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) @@ -540,8 +540,8 @@ def simulate_recurrent( self._check_is_fit() # convert to jnp.ndarray - (coupling_basis_matrix,) = utils.convert_to_jnp_ndarray( - coupling_basis_matrix, data_type=jnp.float_ + coupling_basis_matrix = jnp.asarray( + coupling_basis_matrix, dtype=float ) n_basis_coupling = coupling_basis_matrix.shape[1] diff --git a/src/nemos/utils.py b/src/nemos/utils.py index ffab4d5a..3f970119 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -387,27 +387,6 @@ def row_wise_kron(a, c): return K -def convert_to_jnp_ndarray( - *args: Union[NDArray, jnp.ndarray], data_type: Optional[jnp.dtype] = None -) -> Tuple[jnp.ndarray, ...]: - """Convert provided arrays to jnp.ndarray of specified type. - - Parameters - ---------- - *args : - Input arrays to convert. - data_type : - Data type to convert to. Default is None, which means that the data-type - is inferred from the input. - - Returns - ------- - : - Converted arrays. - """ - return tuple(jnp.asarray(arg, dtype=data_type) for arg in args) - - def check_invalid_entry(array: jnp.ndarray) -> None: """Check if the array has nans or infs. diff --git a/tests/test_base_class.py b/tests/test_base_class.py index f2384e44..2069dfdf 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -118,14 +118,6 @@ def test_get_param_names(): assert "std_param" in param_names -def test_convert_to_jnp_ndarray(): - """Test data conversion to JAX NumPy arrays.""" - data = [1, 2, 3] - (jnp_data,) = convert_to_jnp_ndarray(data) - assert isinstance(jnp_data, jnp.ndarray) - assert jnp.all(jnp_data == jnp.array(data, dtype=jnp.float32)) - - def test_check_invalid_entry(): """Test validation of data arrays.""" valid_data = jnp.array([1, 2, 3]) From 38d6d519a0c4fa3bbc7921b20a729a8c049f7cc9 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:31:22 -0500 Subject: [PATCH 189/250] fixed tests exceptions --- src/nemos/base_class.py | 2 +- tests/test_base_class.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 8232458d..37a32238 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -10,7 +10,7 @@ import jax.numpy as jnp from numpy.typing import ArrayLike, NDArray -from .utils import check_invalid_entry, convert_to_jnp_ndarray +from .utils import check_invalid_entry class Base: diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 2069dfdf..4a9cdcb1 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -7,7 +7,7 @@ import nemos as nmo from nemos.base_class import Base, BaseRegressor -from nemos.utils import check_invalid_entry, convert_to_jnp_ndarray +from nemos.utils import check_invalid_entry @pytest.fixture @@ -171,7 +171,7 @@ def test_preprocess_fit_invalid_datatypes(mock_regressor): """Test behavior with invalid data types.""" X = "invalid_data_type" y = "invalid_data_type" - with pytest.raises(TypeError): + with pytest.raises(ValueError): mock_regressor._preprocess_fit(X, y) @@ -253,8 +253,8 @@ def test_preprocess_simulate_invalid_datatypes(mock_regressor): feedforward_input = "invalid_data_type" params_f = (jnp.array([[]]),) with pytest.raises( - TypeError, - match="Value 'invalid_data_type' with dtype .+ is not a valid JAX array type.", + ValueError, + match="could not convert string", ): mock_regressor._preprocess_simulate(feedforward_input, params_f) From a2981bcb5ebd2e36cfa51f00ac0c2bf61bae8bcd Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:39:10 -0500 Subject: [PATCH 190/250] fixed tests exceptions --- src/nemos/base_class.py | 6 +++--- src/nemos/utils.py | 8 +++++--- tests/test_base_class.py | 14 +++++++------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 37a32238..882689c2 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -406,8 +406,8 @@ def _preprocess_fit( self._check_input_dimensionality(X, y) self._check_input_n_timepoints(X, y) - check_invalid_entry(X) - check_invalid_entry(y) + check_invalid_entry(X, "X") + check_invalid_entry(y, "y") _, n_neurons = y.shape n_features = X.shape[2] @@ -473,7 +473,7 @@ def _preprocess_simulate( params_feedforward, X=feedforward_input ) - check_invalid_entry(feedforward_input) + check_invalid_entry(feedforward_input, "feedforward_input") # Ensure that both or neither of `init_y` and `params_r` are provided if (init_y is None) != (params_recurrent is None): diff --git a/src/nemos/utils.py b/src/nemos/utils.py index 3f970119..6070925b 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -387,13 +387,15 @@ def row_wise_kron(a, c): return K -def check_invalid_entry(array: jnp.ndarray) -> None: +def check_invalid_entry(array: jnp.ndarray, array_name: str) -> None: """Check if the array has nans or infs. Parameters ---------- array: The array to be checked. + array_name: + The array name. Raises ------ @@ -401,9 +403,9 @@ def check_invalid_entry(array: jnp.ndarray) -> None: """ if jnp.any(jnp.isinf(array)): - raise ValueError("Input array contains Infs!") + raise ValueError(f"Input array '{array_name}' contains Infs!") elif jnp.any(jnp.isnan(array)): - raise ValueError("Input array contains NaNs!") + raise ValueError(f"Input array '{array_name}' contains NaNs!") def assert_has_attribute(obj: Any, attr_name: str): diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 4a9cdcb1..b35d4b48 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -123,11 +123,11 @@ def test_check_invalid_entry(): valid_data = jnp.array([1, 2, 3]) invalid_data_nan = jnp.array([1, 2, jnp.nan]) invalid_data_inf = jnp.array([1, jnp.inf, 2]) - check_invalid_entry(valid_data) - with pytest.raises(ValueError, match="Input array contains NaN"): - check_invalid_entry(invalid_data_nan) - with pytest.raises(ValueError, match="Input array contains Inf"): - check_invalid_entry(invalid_data_inf) + check_invalid_entry(valid_data, "valid_data") + with pytest.raises(ValueError, match="Input array 'invalid_data_nan' contains NaN"): + check_invalid_entry(invalid_data_nan, "invalid_data_nan") + with pytest.raises(ValueError, match="Input array 'invalid_data_inf' contains Inf"): + check_invalid_entry(invalid_data_inf, "invalid_data_inf") # To ensure abstract methods aren't callable @@ -263,7 +263,7 @@ def test_preprocess_simulate_with_nan(mock_regressor): """Test behavior with NaN values in feedforward_input.""" feedforward_input = jnp.array([[[jnp.nan]]]) params_f = (jnp.array([[1]]), jnp.array([1])) - with pytest.raises(ValueError, match="Input array contains"): + with pytest.raises(ValueError, match="Input array .+ contains"): mock_regressor._preprocess_simulate(feedforward_input, params_f) @@ -271,7 +271,7 @@ def test_preprocess_simulate_with_inf(mock_regressor): """Test behavior with infinite values in feedforward_input.""" feedforward_input = jnp.array([[[jnp.inf]]]) params_f = (jnp.array([[1]]), jnp.array([1])) - with pytest.raises(ValueError, match="Input array contains"): + with pytest.raises(ValueError, match="Input array .+ contains"): mock_regressor._preprocess_simulate(feedforward_input, params_f) From 59393f9c2a40dccb61ba9101c29d03c69efcdd41 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:41:21 -0500 Subject: [PATCH 191/250] fixed regex tests --- tests/test_base_class.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_base_class.py b/tests/test_base_class.py index b35d4b48..f27231b2 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -179,7 +179,7 @@ def test_preprocess_fit_with_nan_in_X(mock_regressor): """Test behavior with NaN values in data.""" X = jnp.array([[[1, 2], [jnp.nan, 4]]]) y = jnp.array([[1, 2]]) - with pytest.raises(ValueError, match="Input array contains"): + with pytest.raises(ValueError, match="Input array .+ contains"): mock_regressor._preprocess_fit(X, y) @@ -187,7 +187,7 @@ def test_preprocess_fit_with_inf_in_X(mock_regressor): """Test behavior with inf values in data.""" X = jnp.array([[[1, 2], [jnp.inf, 4]]]) y = jnp.array([[1, 2]]) - with pytest.raises(ValueError, match="Input array contains"): + with pytest.raises(ValueError, match="Input array .+ contains"): mock_regressor._preprocess_fit(X, y) @@ -195,7 +195,7 @@ def test_preprocess_fit_with_nan_in_y(mock_regressor): """Test behavior with NaN values in data.""" X = jnp.array([[[1, 2], [2, 4]]]) y = jnp.array([[1, jnp.nan]]) - with pytest.raises(ValueError, match="Input array contains"): + with pytest.raises(ValueError, match="Input array .+ contains"): mock_regressor._preprocess_fit(X, y) @@ -203,7 +203,7 @@ def test_preprocess_fit_with_inf_in_y(mock_regressor): """Test behavior with inf values in data.""" X = jnp.array([[[1, 2], [2, 4]]]) y = jnp.array([[1, jnp.inf]]) - with pytest.raises(ValueError, match="Input array contains"): + with pytest.raises(ValueError, match="Input array .+ contains"): mock_regressor._preprocess_fit(X, y) From d40a272c19e3d531f4e11073905b5716afbaf11a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 21 Nov 2023 17:43:45 -0500 Subject: [PATCH 192/250] fixed regex tests glm --- tests/test_glm.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 8ef0fc47..b15182f1 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -91,11 +91,11 @@ def test_fit_param_length(self, n_params, error, match_str, poissonGLM_model_ins @pytest.mark.parametrize("add_entry, add_to, error, match_str", [ (0, "X", None, None), - (np.nan, "X", ValueError, "Input array contains"), - (np.inf, "X", ValueError, "Input array contains"), + (np.nan, "X", ValueError, "Input array .+ contains"), + (np.inf, "X", ValueError, "Input array .+ contains"), (0, "y", None, None), - (np.nan, "y", ValueError, "Input array contains"), - (np.inf, "y", ValueError, "Input array contains"), + (np.nan, "y", ValueError, "Input array .+ contains"), + (np.inf, "y", ValueError, "Input array .+ contains"), ]) def test_fit_param_values(self, add_entry, add_to, error, match_str, poissonGLM_model_instantiation): """ From 3b43e0417d1e08628d62190956dd88f46af5f7a8 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 22 Nov 2023 12:35:01 -0500 Subject: [PATCH 193/250] fixed hyperlink --- src/nemos/observation_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index aefb1156..ee5b3e9a 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -209,7 +209,7 @@ def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): r"""Pseudo-$R^2$ calculation for a GLM. - Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002)[$^1$](#--references). + Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002)[$^1$](#references). This metric evaluates the goodness-of-fit of the model relative to a null (baseline) model that assumes a constant mean for the observations. While the pseudo-$R^2$ is bounded between 0 and 1 for the training set, From 84e892510722d65182c3e09e3af5cf7e2c84d804 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 22 Nov 2023 12:47:57 -0500 Subject: [PATCH 194/250] improved _score docstrings --- src/nemos/glm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index d1c04be5..9ddcb017 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -185,7 +185,9 @@ def _score( ) -> jnp.ndarray: r"""Score the predicted rates against target neural activity. - This computes the negative log-likelihood up to a constant term. + This method computes the negative log-likelihood up to a constant term. Unlike `score`, + it does not conduct parameter checks prior to evaluation. Passed directly to the solver, + it serves to establish the optimization objective for learning the model parameters. Parameters ---------- From 9debb466e925d986844c58565abacc5a0b8049ba Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Fri, 24 Nov 2023 13:00:23 -0500 Subject: [PATCH 195/250] added 2 types of pr2 --- src/nemos/base_class.py | 7 +- src/nemos/glm.py | 42 ++++-------- src/nemos/observation_models.py | 110 ++++++++++++++++++++++++------- tests/test_glm.py | 3 +- tests/test_observation_models.py | 12 ++-- 5 files changed, 108 insertions(+), 66 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 882689c2..214930da 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -258,10 +258,7 @@ def _check_and_convert_params( """ try: - params = tuple( - jnp.asarray(par, dtype=data_type) - for par in params - ) + params = tuple(jnp.asarray(par, dtype=data_type) for par in params) except (ValueError, TypeError): raise TypeError( "Initial parameters must be array-like of array-like objects " @@ -482,7 +479,7 @@ def _preprocess_simulate( ) # If both are provided, perform checks and conversions elif init_y is not None and params_recurrent is not None: - init_y = jnp.asarray(init_y, dtype=float) + init_y = jnp.asarray(init_y, dtype=float) self._check_input_dimensionality(y=init_y) self._check_input_and_params_consistency(params_recurrent, y=init_y) return feedforward_input, init_y diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 9ddcb017..3129cfe0 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -211,7 +211,8 @@ def score( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], - score_type: Literal["log-likelihood", "pseudo-r2"] = "pseudo-r2", + score_type: Literal["log-likelihood", "pseudo-r2"] = "pseudo-r2-McFadden", + pseudo_r2_type: Literal["McFadden", "Cox"] = "McFadden", ) -> jnp.ndarray: r"""Evaluates the goodness-of-fit of the model to the observed neural data. @@ -231,6 +232,8 @@ def score( during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). score_type : Type of scoring: either log-likelihood or pseudo-r2. + pseudo_r2_type : + Type of pseudo-r2 to be reported. Returns ------- @@ -278,28 +281,8 @@ def score( Therefore, even the Pearson residuals performs poorly as a measure of fit quality, especially for GLM modeling counting data. - The pseudo-$R^2$ can be computed as follows, - - $$ - \begin{aligned} - R^2_{\text{pseudo}} &= \frac{D_{\text{null}} - D_{\text{model}}}{D_{\text{null}}} \\\ - &= \frac{\log \text{LL}(\hat{\lambda}| y) - \log \text{LL}(\bar{\lambda}| y)}{\log \text{LL}(y| y) - - \log \text{LL}(\bar{\lambda}| y)}, - \end{aligned} - $$ - - where LL is the log-likelihood, $D_{\text{null}}$ is the deviance for a null model, $D_{\text{model}}$ is - the deviance for the current model, $y_{tn}$ and $\hat{\lambda}_{tn}$ are the observed activity and the model - predicted rate for neuron $n$ at time-point $t$, and $\bar{\lambda}$ is the mean firing rate, - see references[$^1$](#--references). - - Refer to the `nmo.observation_models.Observations` concrete subclasses for the specific likelihood equations. - - - References - ---------- - 1. Cohen, Jacob, et al. Applied multiple regression/correlation analysis for the behavioral sciences. - Routledge, 2013. + Refer to the `nmo.observation_models.Observations` concrete subclasses for the likelihood and + pseudo-$R^2$ equations. """ self._check_is_fit() @@ -315,12 +298,15 @@ def score( if score_type == "log-likelihood": norm_constant = jax.scipy.special.gammaln(y + 1).mean() score = -self._score((Ws, bs), X, y) - norm_constant - elif score_type == "pseudo-r2": - score = self._observation_model.pseudo_r2(self._predict((Ws, bs), X), y) + elif score_type.startswith("pseudo-r2"): + score = self._observation_model.pseudo_r2( + self._predict((Ws, bs), X), y, score_type=score_type + ) else: raise NotImplementedError( f"Scoring method {score_type} not implemented! " - f"`score_type` must be either 'log-likelihood', or 'pseudo-r2'." + "`score_type` must be either 'log-likelihood', 'pseudo-r2-McFadden', " + "or 'pseudo-r2-Choen'." ) return score @@ -542,9 +528,7 @@ def simulate_recurrent( self._check_is_fit() # convert to jnp.ndarray - coupling_basis_matrix = jnp.asarray( - coupling_basis_matrix, dtype=float - ) + coupling_basis_matrix = jnp.asarray(coupling_basis_matrix, dtype=float) n_basis_coupling = coupling_basis_matrix.shape[1] n_neurons = self.intercept_.shape[0] diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index ee5b3e9a..282c1493 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -1,7 +1,7 @@ """Observation model classes for GLMs.""" import abc -from typing import Callable, Union +from typing import Callable, Literal, Union import jax import jax.numpy as jnp @@ -206,14 +206,19 @@ def estimate_scale(self, predicted_rate: jnp.ndarray) -> None: """ pass - def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): + def pseudo_r2( + self, + predicted_rate: jnp.ndarray, + y: jnp.ndarray, + score_type: Literal["pseudo-r2-McFadden", "pseudo-r2-Choen"] = "pseudo-r2-McFadden", + ) -> jnp.ndarray: r"""Pseudo-$R^2$ calculation for a GLM. - Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002)[$^1$](#references). + Compute the pseudo-$R^2$ metric for the GLM. This metric evaluates the goodness-of-fit of the model relative to a null (baseline) model that assumes a constant mean for the observations. While the pseudo-$R^2$ is bounded between 0 and 1 for the training set, - it can yield negative values on out-of-sample data, indicating potential overfitting. + it can yield negative values on out-of-sample data, indicating potential over-fitting. Parameters ---------- @@ -221,6 +226,8 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): The mean neural activity. Expected shape: (n_time_bins, n_neurons) y: The neural activity. Expected shape: (n_time_bins, n_neurons) + score_type: + The pseudo-R$^2$ type. Returns ------- @@ -230,38 +237,91 @@ def pseudo_r2(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): Notes ----- - The pseudo-$R^2$ score is calculated as follows, + - The McFadden pseudo-$R^2$ is given by: + $$ + R^2_{\text{mcf}} = 1 - \frac{\log(L_{M})}{\log(L_0)} + $$ + - The Choen pseudo-$R^2$ is given by: + $$ + \begin{aligned} + R^2_{\text{Choen}} &= \frac{D_0 - D_M}{D_0} \\\ + &= 1 - \frac{\log(L_s) - \log(L_M)}{\log(L_s)-\log(L_0)}, + \end{aligned} + $$ + where $L_M$, $L_0$ and $L_s$ are the likelihood of the fitted model, the null model (a + model with only the intercept term), and the saturated model (a model with one parameter per + sample, i.e. the maximum value that the likelihood could possibly achieve). $D_M$ and $D_0$ are + the model and the null deviance, $D_i = -2 \left[ \log(L_s) - \log(L_i) \right]$ for $i=M,0$. - $$ - \begin{aligned} - R_{\text{pseudo}}^2 &= \frac{LL(\bm{y}| \bm{\hat{\mu}}) - LL(\bm{y}| \bm{\mu_0})}{LL(\bm{y}| \bm{y}) - - LL(\bm{y}| \bm{\mu_0})}\\ - &= \frac{D(\bm{y}; \bm{\mu_0}) - D(\bm{y}; \bm{\hat{\mu}})}{D(\bm{y}; \bm{\mu_0})}, - \end{aligned} - $$ - - where $\bm{y}=[y_1,\dots, y_T]$, $\bm{\hat{\mu}} = \left[\hat{\mu}_1, \dots, \hat{\mu}_T \right]$ and, - $\bm{\mu_0} = \left[\mu_0, \dots, \mu_0 \right]$ are the counts, the model predicted rate and the average - firing rates respectively, $LL$ is the log-likelihood averaged over the samples, and - $D(\cdot\; ;\cdot)$ is the deviance averaged over samples, - $$ - D(\bm{y}; \bm{\mu}) = 2 \left( LL(\bm{y}| \bm{y}) - LL(\bm{y}| \bm{\mu}) \right). - $$ References ---------- - 1. Jacob Cohen, Patricia Cohen, Steven G. West, Leona S. Aiken. + 1. McFadden D (1979). Quantitative methods for analysing travel behavior of individuals: Some recent developments. In D. A. Hensher & P. R. Stopher (Eds.), *Behavioural travel modelling* (pp. 279-318). London: Croom Helm. + 2. Jacob Cohen, Patricia Cohen, Steven G. West, Leona S. Aiken. *Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences*. 3rd edition. Routledge, 2002. p.502. ISBN 978-0-8058-2223-6. (May 2012) """ - res_dev_t = self.deviance(predicted_rate, y) - resid_deviance = jnp.sum(res_dev_t**2) + + if score_type == "pseudo-r2-McFadden": + pseudo_r2 = self._pseudo_r2_mcfadden(predicted_rate, y) + elif score_type == "pseudo-r2-Choen": + pseudo_r2 = self._pseudo_r2_choen(predicted_rate, y) + else: + raise NotImplementedError(f"Score {score_type} not implemented!") + return pseudo_r2 + + def _pseudo_r2_choen(self, predicted_rate: jnp.ndarray, y: jnp.ndarray) -> jnp.ndarray: + r"""Choen's pseudo-$R^2$. + + Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002). See + [`pseudo_r2`](#pseudo_r2) for additional information. + + Parameters + ---------- + predicted_rate: + The mean neural activity. Expected shape: (n_time_bins, n_neurons) + y: + The neural activity. Expected shape: (n_time_bins, n_neurons) + + Returns + ------- + : + The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, + whereas a value closer to 0 suggests that the model doesn't improve much over the null model. + """ + model_dev_t = self.deviance(predicted_rate, y) + model_deviance = jnp.sum(model_dev_t) null_mu = jnp.ones(y.shape, dtype=jnp.float32) * y.mean() null_dev_t = self.deviance(null_mu, y) - null_deviance = jnp.sum(null_dev_t**2) + null_deviance = jnp.sum(null_dev_t) + return (null_deviance - model_deviance) / null_deviance + + def _pseudo_r2_mcfadden(self, predicted_rate: jnp.ndarray, y: jnp.ndarray): + """ + McFadden's pseudo-$R^2$. + + Compute the pseudo-$R^2$ metric as defined by McFadden et al. (1979). See + [`pseudo_r2`](#pseudo_r2) for additional information. + + Parameters + ---------- + predicted_rate: + The mean neural activity. Expected shape: (n_time_bins, n_neurons) + y: + The neural activity. Expected shape: (n_time_bins, n_neurons) - return (null_deviance - resid_deviance) / null_deviance + Returns + ------- + : + The pseudo-$R^2$ of the model. A value closer to 1 indicates a better model fit, + whereas a value closer to 0 suggests that the model doesn't improve much over the null model. + """ + norm = -jax.scipy.special.gammaln(y + 1).mean() + mean_y = jnp.ones(y.shape) * y.mean(axis=0) + ll_null = -self.negative_log_likelihood(mean_y, y) + norm + ll_model = -self.negative_log_likelihood(predicted_rate, y) + norm + return 1 - ll_model / ll_null class PoissonObservations(Observations): diff --git a/tests/test_glm.py b/tests/test_glm.py index b15182f1..0d8f8a44 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -543,7 +543,8 @@ def test_score_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_ _test_class_method(model, "score", [X, y], {}, error, match_str) @pytest.mark.parametrize("score_type, error, match_str", [ - ("pseudo-r2", None, None), + ("pseudo-r2-McFadden", None, None), + ("pseudo-r2-Choen", None, None), ("log-likelihood", None, None), ("not-implemented", NotImplementedError, "Scoring method %s not implemented") ] diff --git a/tests/test_observation_models.py b/tests/test_observation_models.py index bdc45627..502a8be8 100644 --- a/tests/test_observation_models.py +++ b/tests/test_observation_models.py @@ -90,23 +90,23 @@ def test_loglikelihood_against_scipy(self, poissonGLM_model_instantiation): if not np.allclose(ll_model, ll_scipy): raise ValueError("Log-likelihood doesn't match scipy!") - - def test_pseudo_r2_range(self, poissonGLM_model_instantiation): + @pytest.mark.parametrize("score_type", ["pseudo-r2-Choen", "pseudo-r2-McFadden"]) + def test_pseudo_r2_range(self, score_type, poissonGLM_model_instantiation): """ Compute the pseudo-r2 and check that is < 1. """ _, y, model, _, firing_rate = poissonGLM_model_instantiation - pseudo_r2 = model.observation_model.pseudo_r2(firing_rate, y) + pseudo_r2 = model.observation_model.pseudo_r2(firing_rate, y, score_type=score_type) if (pseudo_r2 > 1) or (pseudo_r2 < 0): raise ValueError(f"pseudo-r2 of {pseudo_r2} outside the [0,1] range!") - - def test_pseudo_r2_mean(self, poissonGLM_model_instantiation): + @pytest.mark.parametrize("score_type", ["pseudo-r2-Choen", "pseudo-r2-McFadden"]) + def test_pseudo_r2_mean(self, score_type, poissonGLM_model_instantiation): """ Check that the pseudo-r2 of the null model is 0. """ _, y, model, _, _ = poissonGLM_model_instantiation - pseudo_r2 = model.observation_model.pseudo_r2(y.mean(), y) + pseudo_r2 = model.observation_model.pseudo_r2(y.mean(), y,score_type=score_type) if not np.allclose(pseudo_r2, 0): raise ValueError(f"pseudo-r2 of {pseudo_r2} for the null model. Should be equal to 0!") From 2bdd29346bd827d7f0adbf564af72a1236452310 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 12:45:39 -0500 Subject: [PATCH 196/250] float_eps removed --- src/nemos/base_class.py | 9 --------- src/nemos/observation_models.py | 8 ++------ 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 214930da..5d9b55b0 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -199,12 +199,6 @@ class BaseRegressor(Base, abc.ABC): scoring the model, simulating responses, and preprocessing data. Concrete classes are expected to provide specific implementations of the abstract methods defined here. - Attributes - ---------- - FLOAT_EPS : float - A small float representing machine epsilon for float32, used to handle numerical - stability issues. - See Also -------- Concrete models: @@ -212,9 +206,6 @@ class BaseRegressor(Base, abc.ABC): - [`GLM`](../glm/#nemos.glm.GLM): A feed-forward GLM implementation. - [`GLMRecurrent`](../glm/#nemos.glm.GLMRecurrent): A recurrent GLM implementation. """ - - FLOAT_EPS = jnp.finfo(float).eps - @abc.abstractmethod def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): """Fit the model to neural activity.""" diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 282c1493..09500fb6 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -29,8 +29,6 @@ class Observations(Base, abc.ABC): Attributes ---------- - FLOAT_EPS : - A small value used to ensure numerical stability. Set to the machine epsilon for float32. inverse_link_function : A function that transforms a set of predictors to the domain of the model parameter. @@ -40,8 +38,6 @@ class Observations(Base, abc.ABC): observation model using the Poisson distribution. """ - FLOAT_EPS = jnp.finfo(float).eps - def __init__(self, inverse_link_function: Callable, **kwargs): super().__init__(**kwargs) self.inverse_link_function = inverse_link_function @@ -389,7 +385,7 @@ def negative_log_likelihood( The $\log({y\_{tn}!})$ term is not a function of the parameters and can be disregarded when computing the loss-function. This is why we incorporated it into the `const` term. """ - predicted_rate = jnp.clip(predicted_rate, a_min=self.FLOAT_EPS) + predicted_rate = jnp.clip(predicted_rate, a_min=jnp.finfo(predicted_rate.dtype).eps) x = y * jnp.log(predicted_rate) # see above for derivation of this. return jnp.mean(predicted_rate - x) @@ -451,7 +447,7 @@ def deviance( log-likelihood. Lower values of deviance indicate a better fit. """ # this takes care of 0s in the log - ratio = jnp.clip(spike_counts / predicted_rate, self.FLOAT_EPS, jnp.inf) + ratio = jnp.clip(spike_counts / predicted_rate, jnp.finfo(predicted_rate.dtype).eps, jnp.inf) deviance = 2 * (spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate)) return deviance From 06dc039fd8c9086dbfe80d5c2aebe62feace0620 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 12:54:29 -0500 Subject: [PATCH 197/250] moved multi-array device put to utils.py --- src/nemos/base_class.py | 29 ----------------------------- src/nemos/utils.py | 29 +++++++++++++++++++++++++++++ tests/test_base_class.py | 27 +-------------------------- 3 files changed, 30 insertions(+), 55 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 5d9b55b0..0ff30596 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -121,35 +121,6 @@ def set_params(self, **params: Any): return self - @staticmethod - def device_put( - *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] - ) -> Union[Any, jnp.ndarray]: - """Send arrays to device. - - This function sends the arrays to the target device, if the arrays are - not already there. - - Parameters - ---------- - *args: - NDArray - device: - A target device between "cpu", "tpu", "gpu". - - Returns - ------- - : - The arrays on the desired device. - """ - device_obj = jax.devices(device)[0] - return tuple( - jax.device_put(arg, device_obj) - if arg.device_buffer.device() != device_obj - else arg - for arg in args - ) - @classmethod def _get_param_names(cls): """Get parameter names for the estimator.""" diff --git a/src/nemos/utils.py b/src/nemos/utils.py index 6070925b..69209003 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -460,3 +460,32 @@ def assert_scalar_func(func: Callable, inputs: List[jnp.ndarray], func_name: str f"The `{func_name}` should return a scalar! " f"Array of shape {array_out.shape} returned instead!" ) + + +def multi_array_device_put( + *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] +) -> Union[Any, jnp.ndarray]: + """Send arrays to device. + + This function sends the arrays to the target device, if the arrays are + not already there. + + Parameters + ---------- + *args: + NDArray + device: + A target device between "cpu", "tpu", "gpu". + + Returns + ------- + : + The arrays on the desired device. + """ + device_obj = jax.devices(device)[0] + return tuple( + jax.device_put(arg, device_obj) + if arg.device_buffer.device() != device_obj + else arg + for arg in args + ) diff --git a/tests/test_base_class.py b/tests/test_base_class.py index f27231b2..7cd3114a 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -7,7 +7,7 @@ import nemos as nmo from nemos.base_class import Base, BaseRegressor -from nemos.utils import check_invalid_entry +from nemos.utils import check_invalid_entry, multi_array_device_put @pytest.fixture @@ -308,31 +308,6 @@ def test_empty_set(mock_regressor): assert mock_regressor.set_params() is mock_regressor -@pytest.mark.parametrize("device_name", ["cpu", "gpu", "tpu", "unknown"]) -def test_target_device_put(device_name: Literal["cpu", "gpu", "tpu"], mock_regressor): - """Test that put works. - - Put array to device and checks that the device is matched after put, if device is found. - Raise error otherwise. - """ - raise_exception = not any( - device_name in device.device_kind.lower() for device in jax.local_devices() - ) - x = jnp.array([1]) - if raise_exception: - if device_name != "tpu": - with pytest.raises(RuntimeError, match=f"Unknown backend"): - mock_regressor.device_put(x, device=device_name) - else: - with pytest.raises( - RuntimeError, match=f"Backend '{device_name}' failed to initialize: " - ): - mock_regressor.device_put(x, device=device_name) - else: - (x,) = mock_regressor.device_put(x, device=device_name) - assert x.device().device_kind == device_name - - def test_glm_varargs_error(): """Test that variable number of argument in __init__ is not allowed.""" bad_estimator = BadEstimator(1) From ef170e3f1dbfa7c1d6323b2ac4bb316d2680558a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 13:03:34 -0500 Subject: [PATCH 198/250] improved refs --- src/nemos/observation_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 09500fb6..0a8dbcd4 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -210,7 +210,8 @@ def pseudo_r2( ) -> jnp.ndarray: r"""Pseudo-$R^2$ calculation for a GLM. - Compute the pseudo-$R^2$ metric for the GLM. + Compute the pseudo-$R^2$ metric for the GLM, as defined by McFadden et al.[$^1$](#references) + or by Choen et al.[$^2$](#references). This metric evaluates the goodness-of-fit of the model relative to a null (baseline) model that assumes a constant mean for the observations. While the pseudo-$R^2$ is bounded between 0 and 1 for the training set, From 3a0e99044124afa09466a70e125db1d646b735b9 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 15:45:50 -0500 Subject: [PATCH 199/250] refractored names --- docs/examples/plot_glm_demo.py | 8 ++++---- src/nemos/glm.py | 6 +++--- src/nemos/observation_models.py | 6 +++--- src/nemos/solver.py | 16 ++++++++-------- tests/conftest.py | 4 ++-- tests/test_glm.py | 8 ++++---- tests/test_solver.py | 8 ++++---- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index f154ccf7..9b9a11d6 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -99,7 +99,7 @@ observation_models = nmo.observation_models.PoissonObservations(jax.nn.softplus) # Observation model -solver = nmo.solver.RidgeSolver( +solver = nmo.solver.Ridge( solver_name="LBFGS", regularizer_strength=0.1, solver_kwargs={"tol":10**-10} @@ -118,7 +118,7 @@ # Hyperparameters can be set at any moment via the `set_params` method. model.set_params( - solver=nmo.solver.LassoSolver(), + solver=nmo.solver.Lasso(), observation_model__inverse_link_function=jax.numpy.exp ) @@ -175,7 +175,7 @@ # # **Lasso** -model.set_params(solver=nmo.solver.LassoSolver()) +model.set_params(solver=nmo.solver.Lasso()) cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) @@ -192,7 +192,7 @@ mask[0, [0, -1]] = 1 mask[1, 1:-1] = 1 -solver = nmo.solver.GroupLassoSolver("ProximalGradient", mask=mask) +solver = nmo.solver.GroupLasso("ProximalGradient", mask=mask) model.set_params(solver=solver) cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 3129cfe0..8dc597ca 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -50,7 +50,7 @@ class GLM(BaseRegressor): def __init__( self, observation_model: obs.Observations = obs.PoissonObservations(), - solver: slv.Solver = slv.RidgeSolver("GradientDescent"), + solver: slv.Regularizer = slv.Ridge("GradientDescent"), ): super().__init__() @@ -67,7 +67,7 @@ def solver(self): return self._solver @solver.setter - def solver(self, solver: slv.Solver): + def solver(self, solver: slv.Regularizer): if not hasattr(solver, "instantiate_solver"): raise AttributeError( "The provided `solver` doesn't implement the `instantiate_sovler` method." @@ -457,7 +457,7 @@ class GLMRecurrent(GLM): def __init__( self, observation_model: obs.Observations = obs.PoissonObservations(), - solver: slv.Solver = slv.RidgeSolver(), + solver: slv.Regularizer = slv.Ridge(), ): super().__init__(observation_model=observation_model, solver=solver) diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 0a8dbcd4..199a9eb0 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -518,7 +518,7 @@ def check_observation_model(observation_model): "inverse_link_function": { "input": [jnp.array([1.0, 1.0, 1.0])], "test_differentiable": True, - "test_preserve_shape": 0, + "test_preserve_shape": False, }, "negative_log_likelihood": { "input": [0.5 * jnp.array([1.0, 1.0, 1.0]), jnp.array([1.0, 1.0, 1.0])], @@ -530,7 +530,7 @@ def check_observation_model(observation_model): }, "sample_generator": { "input": [jax.random.PRNGKey(123), 0.5 * jnp.array([1.0, 1.0, 1.0])], - "test_preserve_shape": 1, + "test_preserve_shape": True, }, } @@ -550,7 +550,7 @@ def check_observation_model(observation_model): utils.assert_differentiable(func, attr_name) if "test_preserve_shape" in check_info: - index = check_info["test_preserve_shape"] + index = int(check_info["test_preserve_shape"]) utils.assert_preserve_shape( func, check_info["input"], attr_name, input_index=index ) diff --git a/src/nemos/solver.py b/src/nemos/solver.py index b5f53f49..accccd02 100644 --- a/src/nemos/solver.py +++ b/src/nemos/solver.py @@ -39,14 +39,14 @@ Tuple[jnp.ndarray, jnp.ndarray], ] -__all__ = ["UnRegularizedSolver", "RidgeSolver", "LassoSolver", "GroupLassoSolver"] +__all__ = ["UnRegularized", "Ridge", "Lasso", "GroupLasso"] def __dir__() -> list[str]: return __all__ -class Solver(Base, abc.ABC): +class Regularizer(Base, abc.ABC): """ Abstract base class for optimization solvers. @@ -192,7 +192,7 @@ def solver_run( return solver_run -class UnRegularizedSolver(Solver): +class UnRegularized(Regularizer): """ Solver class for optimizing unregularized models. @@ -226,7 +226,7 @@ def __init__( super().__init__(solver_name, solver_kwargs=solver_kwargs) -class RidgeSolver(Solver): +class Ridge(Regularizer): """ Solver for Ridge regularization using various optimization algorithms. @@ -305,7 +305,7 @@ def penalized_loss(params, X, y): return self.get_runner(penalized_loss) -class ProxGradientSolver(Solver, abc.ABC): +class ProxGradientRegularizer(Regularizer, abc.ABC): """ Solver for optimization using the Proximal Gradient method. @@ -367,9 +367,9 @@ def instantiate_solver( return super().instantiate_solver(loss, self.regularizer_strength) -class LassoSolver(ProxGradientSolver): +class Lasso(ProxGradientRegularizer): """ - Solver for optimization using the Lasso (L1 regularization) method with Proximal Gradient. + Regularizer for optimization using the Lasso (L1 regularization) method with Proximal Gradient. This class is a specialized version of the ProxGradientSolver with the proximal operator set for L1 regularization (Lasso). It utilizes the `jaxopt` library's proximal gradient optimizer. @@ -404,7 +404,7 @@ def prox_op(params, l1reg, scaling=1.0): return prox_op -class GroupLassoSolver(ProxGradientSolver): +class GroupLasso(ProxGradientRegularizer): """ Solver for optimization using the Group Lasso regularization method with Proximal Gradient. diff --git a/tests/conftest.py b/tests/conftest.py index 803b48c2..e82adbbc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,7 +41,7 @@ def poissonGLM_model_instantiation(): b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) observation_model = nmo.observation_models.PoissonObservations(jnp.exp) - solver = nmo.solver.UnRegularizedSolver('GradientDescent', {}) + solver = nmo.solver.UnRegularized('GradientDescent', {}) model = nmo.glm.GLM(observation_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -117,7 +117,7 @@ def group_sparse_poisson_glm_model_instantiation(): mask[0, 1:4] = 1 mask[1, [0,4]] = 1 observation_model = nmo.observation_models.PoissonObservations(jnp.exp) - solver = nmo.solver.UnRegularizedSolver('GradientDescent', {}) + solver = nmo.solver.UnRegularized('GradientDescent', {}) model = nmo.glm.GLM(observation_model, solver) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask diff --git a/tests/test_glm.py b/tests/test_glm.py index 0d8f8a44..83ececdf 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -36,9 +36,9 @@ class TestGLM: @pytest.mark.parametrize( "solver, error, match_str", [ - (nmo.solver.RidgeSolver("BFGS"), None, None), + (nmo.solver.Ridge("BFGS"), None, None), (None, AttributeError, "The provided `solver` doesn't implement "), - (nmo.solver.RidgeSolver, TypeError, "The provided `solver` cannot be instantiated") + (nmo.solver.Ridge, TypeError, "The provided `solver` cannot be instantiated") ] ) def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiation): @@ -52,7 +52,7 @@ def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiat "observation, error, match_str", [ (nmo.observation_models.PoissonObservations(), None, None), - (nmo.solver.Solver, AttributeError, "The provided object does not have the required"), + (nmo.solver.Regularizer, AttributeError, "The provided object does not have the required"), (1, AttributeError, "The provided object does not have the required") ] ) @@ -363,7 +363,7 @@ def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_in def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation): """Test that the group lasso fit goes through""" X, y, model, params, rate, mask = group_sparse_poisson_glm_model_instantiation - model.set_params(solver=nmo.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask)) + model.set_params(solver=nmo.solver.GroupLasso(solver_name="ProximalGradient", mask=mask)) model.fit(X, y) ####################### diff --git a/tests/test_solver.py b/tests/test_solver.py index 17ab6ff8..1d48d3e0 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -9,7 +9,7 @@ class TestUnRegularizedSolver: - cls = nmo.solver.UnRegularizedSolver + cls = nmo.solver.UnRegularized @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -119,7 +119,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): class TestRidgeSolver: - cls = nmo.solver.RidgeSolver + cls = nmo.solver.Ridge @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -229,7 +229,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): class TestLassoSolver: - cls = nmo.solver.LassoSolver + cls = nmo.solver.Lasso @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -317,7 +317,7 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): class TestGroupLassoSolver: - cls = nmo.solver.GroupLassoSolver + cls = nmo.solver.GroupLasso @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): From 412727e1038a2cc2efdbb7355e405a9f5cae25ab Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 15:49:22 -0500 Subject: [PATCH 200/250] fixed tests --- tests/conftest.py | 14 +++++++------- tests/test_glm.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index e82adbbc..7fdd7b41 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -69,7 +69,7 @@ def poissonGLM_coupled_model_config_simulate(): config_dict = json.load(fh) observations = nmo.observation_models.PoissonObservations(jnp.exp) - solver = nmo.solver.RidgeSolver("BFGS", regularizer_strength=0.1) + solver = nmo.solver.Ridge("BFGS", regularizer_strength=0.1) model = nmo.glm.GLMRecurrent(observation_model=observations, solver=solver) model.coef_ = jnp.asarray(config_dict["coef_"]) model.intercept_ = jnp.asarray(config_dict["intercept_"]) @@ -141,21 +141,21 @@ def poisson_observation_model(): @pytest.fixture -def ridge_solver(): - return nmo.solver.RidgeSolver(solver_name="LBFGS", regularizer_strength=0.1) +def ridge_regularizer(): + return nmo.solver.Ridge(solver_name="LBFGS", regularizer_strength=0.1) @pytest.fixture -def lasso_solver(): - return nmo.solver.LassoSolver(solver_name="ProximalGradient", regularizer_strength=0.1) +def lasso_regularizer(): + return nmo.solver.Lasso(solver_name="ProximalGradient", regularizer_strength=0.1) @pytest.fixture -def group_lasso_2groups_5features_solver(): +def group_lasso_2groups_5features_regularizer(): mask = np.zeros((2, 5)) mask[0, :2] = 1 mask[1, 2:] = 1 - return nmo.solver.GroupLassoSolver(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) + return nmo.solver.GroupLasso(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) @pytest.fixture diff --git a/tests/test_glm.py b/tests/test_glm.py index 83ececdf..a8f83cb2 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -56,12 +56,12 @@ def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiat (1, AttributeError, "The provided object does not have the required") ] ) - def test_init_observation_type(self, observation, error, match_str, ridge_solver): + def test_init_observation_type(self, observation, error, match_str, ridge_regularizer): """ Test initialization with different solver names. Check if an appropriate exception is raised when the solver name is not present in jaxopt. """ - _test_class_initialization(self.cls, {'solver': ridge_solver, 'observation_model': observation}, error, match_str) + _test_class_initialization(self.cls, {'solver': ridge_regularizer, 'observation_model': observation}, error, match_str) ####################### # Test model.fit From 5ec84304dc44e0042eee0b572a42c91971ec2a21 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 15:51:51 -0500 Subject: [PATCH 201/250] improved refs to solver --- docs/developers_notes/02-base_class.md | 6 +++--- docs/examples/plot_glm_demo.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 6acf7f74..4fea1fc4 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -23,11 +23,11 @@ Abstract Class Base │ │ │ ... │ -├─ Abstract Subclass Solver +├─ Abstract Subclass Regularizer │ │ -│ ├─ Concrete Subclass UnRegularizedSolver +│ ├─ Concrete Subclass UnRegularized │ │ -│ ├─ Concrete Subclass RidgeSolver +│ ├─ Concrete Subclass Ridge │ ... │ ├─ Abstract Subclass Observations diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 9b9a11d6..1f0abcd7 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -62,8 +62,8 @@ # - **Observation Model**: The observation model for the GLM, e.g. an object of the class of type # `nemos.observation_models.Observations`. So far, only the `PoissonObservations` # model has been implemented. -# - **Solver**: The desired solver, e.g. an object of the `nemos.solver.Solver` class. -# Currently, we implemented the un-regularized, Ridge, Lasso, and Group-Lasso solver. +# - **Regularizer**: The desired regularizer, e.g. an object of the `nemos.solver.Regularizer` class. +# Currently, we implemented the un-regularized, Ridge, Lasso, and Group-Lasso regularization. # # The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge solver. # Here is how to define the model. From 5665711e6384d96749baa9bececbe8c13ed96f4f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 27 Nov 2023 15:53:50 -0500 Subject: [PATCH 202/250] refractor note solver part1 --- docs/developers_notes/04-solver.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/developers_notes/04-solver.md b/docs/developers_notes/04-solver.md index bf855460..63a45d54 100644 --- a/docs/developers_notes/04-solver.md +++ b/docs/developers_notes/04-solver.md @@ -2,27 +2,27 @@ ## Introduction -The `solver` module introduces an archetype class `Solver` which provides the structural components for each concrete sub-class. +The `solver` module introduces an archetype class `Regularizer` which provides the structural components for each concrete sub-class. -Objects of type `Solver` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../05-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. +Objects of type `Regularizer` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../05-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). We choose to rely on `jaxopt` because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX, that are highly customizable. -Each solver object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). +Each regularizer object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). ``` -Abstract Class Solver +Abstract Class Regularizer | -├─ Concrete Class UnRegularizedSolver +├─ Concrete Class UnRegularized | -├─ Concrete Class RidgeSolver +├─ Concrete Class Ridge | -└─ Abstract Class ProximalGradientSolver +└─ Abstract Class ProximalGradientRegularizer | - ├─ Concrete Class LassoSolver + ├─ Concrete Class Lasso | - └─ Concrete Class GroupLassoSolver + └─ Concrete Class GroupLasso ``` !!! note From 266c077f17edf8aacfc8f345e752438f2bf45374 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 28 Nov 2023 12:33:10 -0500 Subject: [PATCH 203/250] refractor module name --- docs/developers_notes/02-base_class.md | 2 +- .../{04-solver.md => 04-regularizer.md} | 94 ++++++++--------- docs/developers_notes/05-glm.md | 8 +- docs/developers_notes/GLM_scheme.jpg | Bin 305373 -> 294441 bytes docs/examples/plot_glm_demo.py | 36 +++---- src/nemos/__init__.py | 2 +- src/nemos/glm.py | 39 +++---- src/nemos/{solver.py => regularizer.py} | 42 ++++---- tests/conftest.py | 18 ++-- tests/test_glm.py | 22 ++-- tests/test_solver.py | 96 +++++++++--------- 11 files changed, 177 insertions(+), 182 deletions(-) rename docs/developers_notes/{04-solver.md => 04-regularizer.md} (51%) rename src/nemos/{solver.py => regularizer.py} (92%) diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index 4fea1fc4..f13007a8 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -4,7 +4,7 @@ The `base_class` module introduces the `Base` class and abstract classes defining broad model categories. These abstract classes **must** inherit from `Base`. -The `Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, observation models, solvers etc.). In contrast, abstract classes derived from `Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `observation_models.Observations` is the building block for the Poisson observations, Gamma observations, ... etc.). +The `Base` class is envisioned as the foundational component for any object type (e.g., regression, dimensionality reduction, clustering, observation models, regularizers etc.). In contrast, abstract classes derived from `Base` define overarching object categories (e.g., `base_class.BaseRegressor` is building block for GLMs, GAMS, etc. while `observation_models.Observations` is the building block for the Poisson observations, Gamma observations, ... etc.). Designed to be compatible with the `scikit-learn` API, the class structure aims to facilitate access to `scikit-learn`'s robust pipeline and cross-validation modules. This is achieved while leveraging the accelerated computational capabilities of `jax` and `jaxopt` in the backend, which is essential for analyzing extensive neural recordings and fitting large models. diff --git a/docs/developers_notes/04-solver.md b/docs/developers_notes/04-regularizer.md similarity index 51% rename from docs/developers_notes/04-solver.md rename to docs/developers_notes/04-regularizer.md index 63a45d54..3097fd0b 100644 --- a/docs/developers_notes/04-solver.md +++ b/docs/developers_notes/04-regularizer.md @@ -1,15 +1,15 @@ -# The `solver` Module +# The `regularizer` Module ## Introduction -The `solver` module introduces an archetype class `Regularizer` which provides the structural components for each concrete sub-class. +The `regularizer` module introduces an archetype class `Regularizer` which provides the structural components for each concrete sub-class. -Objects of type `Regularizer` provide methods to define an optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../05-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. +Objects of type `Regularizer` provide methods to define a regularized optimization objective, and instantiate a solver for it. These objects serve as attribute of the [`nemos.glm.GLM`](../05-glm/#the-concrete-class-glm), equipping the glm with a solver for learning model parameters. Solvers are typically optimizers from the `jaxopt` package, but in principle they could be custom optimization routines as long as they respect the `jaxopt` api (i.e., have a `run` and `update` method with the appropriate input/output types). We choose to rely on `jaxopt` because it provides a comprehensive set of robust, GPU accelerated, batchable and differentiable optimizers in JAX, that are highly customizable. -Each regularizer object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). +Each `Regularizer` object defines a set of allowed optimizers, which in turn depends on the loss function characteristics (smooth vs non-smooth) and/or the optimization type (constrained, un-constrained, batched, etc.). ``` Abstract Class Regularizer @@ -28,17 +28,17 @@ Abstract Class Regularizer !!! note If we need advanced adaptive optimizers (e.g., Adam, LAMB etc.) in the future, we should consider adding [`Optax`](https://optax.readthedocs.io/en/latest/) as a dependency, which is compatible with `jaxopt`, see [here](https://jaxopt.github.io/stable/_autosummary/jaxopt.OptaxSolver.html#jaxopt.OptaxSolver). -## The Abstract Class `Solver` +## The Abstract Class `Regularizer` -The abstract class `Solver` enforces the implementation of the `instantiate_solver` method on any concrete realization of a `Solver` object. `Solver` objects are equipped with a method for instantiating a solver runner, i.e., a function that receives as input the initial parameters, the endogenous and the exogenous variables, and outputs the optimization results. +The abstract class `Regularizer` enforces the implementation of the `instantiate_solver` method on any concrete realization of a `Regularizer` object. `Regularizer` objects are equipped with a method for instantiating a solver runner with the appropriately regularized loss function, i.e., a function that receives as input the initial parameters, the endogenous and the exogenous variables, and outputs the optimization results. Additionally, the class provides auxiliary methods for checking that the solver and loss function specifications are valid. ### Abstract Methods -`Solver` objects define the following abstract method: +`Regularizer` objects define the following abstract method: -- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function. The loss function must be a `Callable` from either the `jax` or the `nemos` namespace. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. +- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function. The loss function must be of type `Callable`. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. ### Public Methods @@ -46,122 +46,116 @@ Additionally, the class provides auxiliary methods for checking that the solver ### Auxiliary Methods -- **`_check_solver`**: This method ensures that the provided solver name is in the list of allowed optimizers for the specific `Solver` object. This is crucial for maintaining consistency and correctness in the solver's operation. +- **`_check_solver`**: This method ensures that the provided solver name is in the list of allowed optimizers for the specific `Regularizer` object. This is crucial for maintaining consistency and correctness in the solver's operation. - **`_check_solver_kwargs`**: This method checks if the provided keyword arguments are valid for the specified solver. This helps in catching and preventing potential errors in solver configuration. -- **`_check_is_callable_from_jax`**: This method checks if the provided function is callable and whether it belongs to the `jax` or `nemos` namespace, ensuring compatibility and safety when using jax-based operations. +## The `UnRegularized` Class -## The `UnRegularizedSolver` Class - -The `UnRegularizedSolver` class extends the base `Solver` class and is designed specifically for optimizing unregularized models. This means that this solver class does not add any regularization penalty to the loss function during the optimization process. +The `UnRegularized` class extends the base `Regularizer` class and is designed specifically for optimizing unregularized models. This means that the solver instantiated by this class does not add any regularization penalty to the loss function during the optimization process. ### Attributes -- **`allowed_optimizers`**: A list of string identifiers for the optimization algorithms that can be used with this solver class. The optimization methods listed here are specifically suitable for unregularized optimization problems. +- **`allowed_solvers`**: A list of string identifiers for the optimization algorithms that can be used with this solver class. The optimization methods listed here are specifically suitable for unregularized optimization problems. ### Methods -- **`__init__`**: The constructor method for this class which initializes a new `UnRegularizedSolver` object. It accepts the name of the solver algorithm to use (`solver_name`) and an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver. +- **`__init__`**: The constructor method for this class which initializes a new `UnRegularized` object. It accepts the name of the solver algorithm to use (`solver_name`) and an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver. -- **`instantiate_solver`**: A method which prepares and returns a runner function for the specified loss function. This method ensures that the loss function is callable and prepares the necessary keyword arguments for calling the `get_runner` method from the base `Solver` class. +- **`instantiate_solver`**: A method which prepares and returns a runner function for the specified loss function. This method ensures that the loss function is callable and prepares the necessary keyword arguments for calling the `get_runner` method from the base `Regularizer` class. ### Example Usage ```python -unreg_solver = UnRegularizedSolver(solver_name="GradientDescent") -runner = unreg_solver.instantiate_solver(loss_function) +unregularized = UnRegularized(solver_name="GradientDescent") +runner = unregularized.instantiate_solver(loss_function) optim_results = runner(init_params, exog_vars, endog_vars) ``` -## The `RidgeSolver` Class +## The `Ridge` Class -The `RidgeSolver` class extends the `Solver` class to handle optimization problems with Ridge regularization. Ridge regularization adds a penalty to the loss function, proportional to the sum of squares of the model parameters, to prevent overfitting and stabilize the optimization. +The `Ridge` class extends the `Regularizer` class to handle optimization problems with Ridge regularization. Ridge regularization adds a penalty to the loss function, proportional to the sum of squares of the model parameters, to prevent overfitting and stabilize the optimization. ### Attributes -- **`allowed_optimizers`**: A list containing string identifiers of optimization algorithms compatible with Ridge regularization. +- **`allowed_solvers`**: A list containing string identifiers of optimization algorithms compatible with Ridge regularization. - **`regularizer_strength`**: A floating-point value determining the strength of the Ridge regularization. Higher values correspond to stronger regularization which tends to drive the model parameters towards zero. ### Methods -- **`__init__`**: The constructor method for the `RidgeSolver` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, and the regularization strength (`regularizer_strength`). +- **`__init__`**: The constructor method for the `Ridge` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, and the regularization strength (`regularizer_strength`). - **`penalization`**: A method to compute the Ridge regularization penalty for a given set of model parameters. -- **`instantiate_solver`**: A method that prepares and returns a runner function with a penalized loss function for Ridge regularization. This method modifies the original loss function to include the Ridge penalty, ensures the loss function is callable, and prepares the necessary keyword arguments for calling the `get_runner` method from the base `Solver` class. +- **`instantiate_solver`**: A method that prepares and returns a runner function with a penalized loss function for Ridge regularization. This method modifies the original loss function to include the Ridge penalty, ensures the loss function is callable, and prepares the necessary keyword arguments for calling the `get_runner` method from the base `Regularizer` class. ### Example Usage ```python -ridge_solver = RidgeSolver(solver_name="LBFGS", regularizer_strength=1.0) -runner = ridge_solver.instantiate_solver(loss_function) +ridge = Ridge(solver_name="LBFGS", regularizer_strength=1.0) +runner = ridge.instantiate_solver(loss_function) optim_results = runner(init_params, exog_vars, endog_vars) ``` -## `ProxGradientSolver` Class +## `ProxGradientRegularizer` Class -`ProxGradientSolver` class extends the `Solver` class to utilize the Proximal Gradient method for optimization. It leverages the `jaxopt` library's Proximal Gradient optimizer, introducing the functionality of a proximal operator. +`ProxGradientRegularizer` class extends the `Regularizer` class to utilize the Proximal Gradient method for optimization. It leverages the `jaxopt` library's Proximal Gradient optimizer, introducing the functionality of a proximal operator. ### Attributes: -- **`allowed_optimizers`**: A list containing string identifiers of optimization algorithms compatible with this solver, specifically the "ProximalGradient". - -- **`mask`**: An optional mask array for element-wise operations with shape (n_groups, n_features). +- **`allowed_solvers`**: A list containing string identifiers of optimization algorithms compatible with this solver, specifically the "ProximalGradient". ### Methods: -- **`__init__`**: The constructor method for the `ProxGradientSolver` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, the regularization strength (`regularizer_strength`), and an optional mask array (`mask`). - -- **`mask`**: Property method to get or set the mask array. - -- **`_check_mask`**: Static method to validate the mask array adhering to specific requirements. - +- **`__init__`**: The constructor method for the `ProxGradientRegularizer` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, the regularization strength (`regularizer_strength`), and an optional mask array (`mask`). + - **`get_prox_operator`**: Abstract method to retrieve the proximal operator for this solver. - **`instantiate_solver`**: Method to prepare and return a runner function for optimization with a provided loss function and proximal operator. -## `LassoSolver` Class +## `Lasso` Class -`LassoSolver` class extends `ProxGradientSolver` to specialize in optimization using the Lasso (L1 regularization) method with Proximal Gradient. +`Lasso` class extends `ProxGradientRegularizer` to specialize in optimization using the Lasso (L1 regularization) method with Proximal Gradient. ### Methods: -- **`__init__`**: Constructor method similar to `ProxGradientSolver` but defaults `solver_name` to "ProximalGradient". +- **`__init__`**: Constructor method similar to `ProxGradientRegularizer` but defaults `solver_name` to "ProximalGradient". - **`get_prox_operator`**: Method to retrieve the proximal operator for Lasso regularization (L1 penalty). -## `GroupLassoSolver` Class +## `GroupLasso` Class -`GroupLassoSolver` class extends `ProxGradientSolver` to specialize in optimization using the Group Lasso regularization method with Proximal Gradient. It induces sparsity on groups of features rather than individual features. +`GroupLasso` class extends `ProxGradientRegularizer` to specialize in optimization using the Group Lasso regularization method with Proximal Gradient. It induces sparsity on groups of features rather than individual features. ### Attributes: - **`mask`**: A mask array indicating groups of features for regularization. ### Methods: -- **`__init__`**: Constructor method similar to `ProxGradientSolver`, but additionally requires a `mask` array to identify groups of features. +- **`__init__`**: Constructor method similar to `ProxGradientRegularizer`, but additionally requires a `mask` array to identify groups of features. - **`get_prox_operator`**: Method to retrieve the proximal operator for Group Lasso regularization. +- **`_check_mask`**: Static method to check that the provided mask is a float `jax.numpy.ndarray` of 0s and 1s. The mask must be in floats to be applied correctly through the linear algebra operations of the `nemos.proimal_operator.prox_group_lasso` function. + ### Example Usage ```python -lasso_solver = LassoSolver(regularizer_strength=1.0) -runner = lasso_solver.instantiate_solver(loss_function) +lasso = Lasso(regularizer_strength=1.0) +runner = lasso.instantiate_solver(loss_function) optim_results = runner(init_params, exog_vars, endog_vars) -group_lasso_solver = GroupLassoSolver(solver_name="ProximalGradient", mask=group_mask, regularizer_strength=1.0) -runner = group_lasso_solver.instantiate_solver(loss_function) +group_lasso = GroupLasso(solver_name="ProximalGradient", mask=group_mask, regularizer_strength=1.0) +runner = group_lasso.instantiate_solver(loss_function) optim_results = runner(init_params, exog_vars, endog_vars) ``` ## Contributor Guidelines -### Implementing Solver Subclasses +### Implementing `Regularizer` Subclasses -When developing a functional (i.e., concrete) Solver class: +When developing a functional (i.e., concrete) `Regularizer` class: -- **Must** inherit from `Solver` or one of its derivatives. +- **Must** inherit from `Regularizer` or one of its derivatives. - **Must** implement the `instantiate_solver` method to tailor the solver instantiation based on the provided loss function. - For any Proximal Gradient method, **must** include a `get_prox_operator` method to define the proximal operator. -- **Must** possess an `allowed_optimizers` attribute to list the optimizer names that are permissible to be used with this solver. +- **Must** possess an `allowed_solvers` attribute to list the optimizer names that are permissible to be used with this solver. - **May** embed additional attributes and methods such as `mask` and `_check_mask` if required by the specific Solver subclass for handling special optimization scenarios. - **May** include a `regularizer_strength` attribute to control the strength of the regularization in scenarios where regularization is applicable. - **May** rely on a custom solver implementation for specific optimization problems, but the implementation **must** adhere to the `jaxopt` API. diff --git a/docs/developers_notes/05-glm.md b/docs/developers_notes/05-glm.md index 0ce65e20..dc01329d 100644 --- a/docs/developers_notes/05-glm.md +++ b/docs/developers_notes/05-glm.md @@ -15,7 +15,7 @@ Our design aligns with the `scikit-learn` API, facilitating seamless integration The classes provided here are modular by design offering a standard foundation for any GLM variant. -Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`nemos.observation_models.Observations`](../03-observation_models/#the-abstract-class-observations) and [`nemos.solver.Solver`](../04-solver/#the-abstract-class-solver) objects, respectively. +Instantiating a specific GLM simply requires providing an observation model (Gamma, Poisson, etc.) and a regularization strategies (Ridge, Lasso, etc.) during initialization. This is done using the [`nemos.observation_models.Observations`](../03-observation_models/#the-abstract-class-observations) and [`nemos.regularizer.Regularizer`](../04-regularizer/#the-abstract-class-regularizer) objects, respectively.
@@ -35,7 +35,7 @@ The `GLM` class provides a direct implementation of the GLM model and is designe ### Attributes -- **`solver`**: Refers to the optimization solver - an object of the [`nemos.solver.Solver`](../04-solver/#the-abstract-class-solver) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. +- **`regularizer`**: Refers to the optimization regularizer - an object of the [`nemos.regularizer.regularizer`](../04-regularizer/#the-abstract-class-regularizer) type. It uses the `jaxopt` solver to minimize the (penalized) negative log-likelihood of the GLM. - **`observation_models`**: Represents the GLM observation model, which is an object of the [`nemos.observation_models.Observations`](../03-observation_models/#the-abstract-class-observations) type. This model determines the log-likelihood and the emission probability mechanism for the `GLM`. - **`coef_`**: Stores the solution for spike basis coefficients as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. - **`intercept_`**: Stores the bias terms' solutions as `jax.ndarray` after the fitting process. It is initialized as `None` during class instantiation. @@ -45,7 +45,7 @@ The `GLM` class provides a direct implementation of the GLM model and is designe - **`predict`**: Validates input and computes the mean rates of the `GLM` by invoking the inverse-link function of the `observation_models` attribute. - **`score`**: Validates input and assesses the Poisson GLM using either log-likelihood or pseudo-$R^2$. This method uses the `observation_models` to determine log-likelihood or pseudo-$R^2$. -- **`fit`**: Validates input and aligns the Poisson GLM with spike train data. It leverages the `observation_models` and `solver` to define the model's loss function and instantiate the solver. +- **`fit`**: Validates input and aligns the Poisson GLM with spike train data. It leverages the `observation_models` and `regularizer` to define the model's loss function and instantiate the regularizer. - **`simulate`**: Simulates spike trains using the GLM as a feedforward network, invoking the `observation_models.sample_generator` method for emission probability. ### Private Methods @@ -72,5 +72,5 @@ When crafting a functional (i.e., concrete) GLM class: - **Must** inherit from `BaseRegressor` or one of its derivatives. - **Must** realize the `predict`, `fit`, `score`, and `simulate` methods, either directly or through inheritance. - **Should** incorporate a `observation_models` attribute of type `nemos.observation_models.Observations` to specify the link-function, emission probability, and likelihood. -- **Should** include a `solver` attribute of type `nemos.solver.Solver` to establish the solver based on penalization type. +- **Should** include a `regularizer` attribute of type `nemos.regularizer.Regularizer` to instantiate the solver based on regularization type. - **May** embed additional parameter and input checks if required by the specific GLM subclass. diff --git a/docs/developers_notes/GLM_scheme.jpg b/docs/developers_notes/GLM_scheme.jpg index 96f86eff8e96833a271dd41a453a75743546bc41..712a98bab0bf8f09161e5b8052a51b248670a49f 100644 GIT binary patch literal 294441 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 XKugR?N6${f6E}Z^z5gTk*Mt8DgXBKz literal 305373 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

!T%g=1zmt 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

=AG|T6=)omaq-)7TK$}V!zpPRoUU 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 diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 1f0abcd7..638451fa 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -62,16 +62,16 @@ # - **Observation Model**: The observation model for the GLM, e.g. an object of the class of type # `nemos.observation_models.Observations`. So far, only the `PoissonObservations` # model has been implemented. -# - **Regularizer**: The desired regularizer, e.g. an object of the `nemos.solver.Regularizer` class. +# - **Regularizer**: The desired regularizer, e.g. an object of the `nemos.regularizer.Regularizer` class. # Currently, we implemented the un-regularized, Ridge, Lasso, and Group-Lasso regularization. # -# The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge solver. +# The default for the GLM class is the `PoissonObservations` with log-link function with a Ridge regularization. # Here is how to define the model. -# default Poisson GLM with Ridge solver and Poisson observation model. +# default Poisson GLM with Ridge regularization and Poisson observation model. model = nmo.glm.GLM() -print("Solver type: ", type(model.solver)) +print("Regularization type: ", type(model.regularizer)) print("Observation model:", type(model.observation_model)) # %% @@ -99,7 +99,7 @@ observation_models = nmo.observation_models.PoissonObservations(jax.nn.softplus) # Observation model -solver = nmo.solver.Ridge( +regularizer = nmo.regularizer.Ridge( solver_name="LBFGS", regularizer_strength=0.1, solver_kwargs={"tol":10**-10} @@ -108,29 +108,29 @@ # define the GLM model = nmo.glm.GLM( observation_model=observation_models, - solver=solver, + regularizer=regularizer, ) -print("Solver type: ", type(model.solver)) +print("Regularizer type: ", type(model.regularizer)) print("Observation model:", type(model.observation_model)) # %% # Hyperparameters can be set at any moment via the `set_params` method. model.set_params( - solver=nmo.solver.Lasso(), + regularizer=nmo.regularizer.Lasso(), observation_model__inverse_link_function=jax.numpy.exp ) -print("Updated solver: ", model.solver) +print("Updated regularizer: ", model.regularizer) print("Updated NL: ", model.observation_model.inverse_link_function) # %% # !!! warning -# Each `Solver` has an associated attribute `Solver.allowed_optimizers` +# Each `Regularizer` has an associated attribute `Regularizer.allowed_optimizers` # which lists the optimizers that are suited for each optimization problem. -# For example, a RidgeSolver is differentiable and can be fit with `GradientDescent` -# , `BFGS`, etc., while a LassoSolver should use the `ProximalGradient` method instead. +# For example, a `Ridge` is differentiable and can be fit with `GradientDescent` +# , `BFGS`, etc., while a `Lasso` should use the `ProximalGradient` method instead. # If the provided `solver_name` is not listed in the `allowed_optimizers` this will raise an # exception. @@ -143,7 +143,7 @@ # Fit a ridge regression Poisson GLM model = nmo.glm.GLM() -model.set_params(solver__regularizer_strength=0.1) +model.set_params(regularizer__regularizer_strength=0.1) model.fit(X, spikes) print("Ridge results") @@ -161,7 +161,7 @@ # # **Ridge** -parameter_grid = {"solver__regularizer_strength": np.logspace(-1.5, 1.5, 6)} +parameter_grid = {"regularizer__regularizer_strength": np.logspace(-1.5, 1.5, 6)} cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) @@ -171,11 +171,11 @@ print("Recovered weights: ", cls.best_estimator_.coef_) # %% -# We can compare the Ridge cross-validated results with other solvers. +# We can compare the Ridge cross-validated results with other regularization schemes. # # **Lasso** -model.set_params(solver=nmo.solver.Lasso()) +model.set_params(regularizer=nmo.regularizer.Lasso()) cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) @@ -192,8 +192,8 @@ mask[0, [0, -1]] = 1 mask[1, 1:-1] = 1 -solver = nmo.solver.GroupLasso("ProximalGradient", mask=mask) -model.set_params(solver=solver) +regularizer = nmo.regularizer.GroupLasso("ProximalGradient", mask=mask) +model.set_params(regularizer=regularizer) cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) cls.fit(X, spikes) diff --git a/src/nemos/__init__.py b/src/nemos/__init__.py index 999ad7b0..ef1fe63a 100644 --- a/src/nemos/__init__.py +++ b/src/nemos/__init__.py @@ -1,3 +1,3 @@ #!/usr/bin/env python3 -from . import basis, glm, observation_models, sample_points, solver, utils +from . import basis, glm, observation_models, sample_points, regularizer, utils diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 8dc597ca..f57eb7ca 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -6,7 +6,7 @@ from numpy.typing import NDArray from . import observation_models as obs -from . import solver as slv +from . import regularizer as slv from . import utils from .base_class import BaseRegressor from .exceptions import NotFittedError @@ -26,8 +26,9 @@ class GLM(BaseRegressor): observation_model : Observation model to use. The model describes the distribution of the neural activity. Default is the Poisson model. - solver : - Solver to use for model optimization. Defines the optimization algorithm and related parameters. + regularizer : + Regularization to use for model optimization. Defines the regularization scheme, the optimization algorithm, + and related parameters. Default is Ridge regression with gradient descent. Attributes @@ -43,19 +44,19 @@ class GLM(BaseRegressor): Raises ------ TypeError - If provided `solver` or `observation_model` are not valid or implemented in `nemos.solver` and + If provided `regularizer` or `observation_model` are not valid or implemented in `nemos.solver` and `nemos.observation_models` respectively. """ def __init__( self, observation_model: obs.Observations = obs.PoissonObservations(), - solver: slv.Regularizer = slv.Ridge("GradientDescent"), + regularizer: slv.Regularizer = slv.Ridge("GradientDescent"), ): super().__init__() self.observation_model = observation_model - self.solver = solver + self.regularizer = regularizer # initialize to None fit output self.intercept_ = None @@ -63,24 +64,24 @@ def __init__( self.solver_state = None @property - def solver(self): - return self._solver + def regularizer(self): + return self._regularizer - @solver.setter - def solver(self, solver: slv.Regularizer): - if not hasattr(solver, "instantiate_solver"): + @regularizer.setter + def regularizer(self, regularizer: slv.Regularizer): + if not hasattr(regularizer, "instantiate_solver"): raise AttributeError( - "The provided `solver` doesn't implement the `instantiate_sovler` method." + "The provided `solver` doesn't implement the `instantiate_solver` method." ) # test solver instantiation on the GLM loss try: - solver.instantiate_solver(self._score) + regularizer.instantiate_solver(self._score) except Exception: raise TypeError( "The provided `solver` cannot be instantiated on " "the GLM log-likelihood." ) - self._solver = solver + self._regularizer = regularizer @property def observation_model(self): @@ -349,7 +350,7 @@ def fit( X, y, init_params = self._preprocess_fit(X, y, init_params) # Run optimization - runner = self.solver.instantiate_solver(self._score) + runner = self.regularizer.instantiate_solver(self._score) params, state = runner(init_params, X, y) # estimate the GLM scale @@ -437,8 +438,8 @@ class GLMRecurrent(GLM): observation_model : The observation model to use for the GLM. This defines how neural activity is generated based on the underlying firing rate. Common choices include Poisson and Gaussian models. - solver : - The optimization solver to use for fitting the GLM parameters. + regularizer : + The regularization scheme to use for fitting the GLM parameters. See Also -------- @@ -457,9 +458,9 @@ class GLMRecurrent(GLM): def __init__( self, observation_model: obs.Observations = obs.PoissonObservations(), - solver: slv.Regularizer = slv.Ridge(), + regularizer: slv.Regularizer = slv.Ridge(), ): - super().__init__(observation_model=observation_model, solver=solver) + super().__init__(observation_model=observation_model, regularizer=regularizer) def simulate_recurrent( self, diff --git a/src/nemos/solver.py b/src/nemos/regularizer.py similarity index 92% rename from src/nemos/solver.py rename to src/nemos/regularizer.py index accccd02..1a51a2dc 100644 --- a/src/nemos/solver.py +++ b/src/nemos/regularizer.py @@ -1,8 +1,8 @@ """ -A Module for Optimization with Various Regularizations. +A Module for Optimization with Various Regularization Schemes. This module provides a series of classes that facilitate the optimization of models -with different types of regularizations. Each solver class in this module interfaces +with different types of regularizations. Each `Regularizer` class in this module interfaces with various optimization methods, and they can be applied depending on the model's requirements. """ import abc @@ -48,15 +48,15 @@ def __dir__() -> list[str]: class Regularizer(Base, abc.ABC): """ - Abstract base class for optimization solvers. + Abstract base class for regularized solvers. This class is designed to provide a consistent interface for optimization solvers, - enabling users to easily switch between different solvers and ensure compatibility - with various loss functions and regularization schemes. + enabling users to easily switch between different regularizers, ensuring compatibility + with various loss functions and optimization algorithms. Attributes ---------- - allowed_algorithms : + allowed_solvers : List of optimizer names that are allowed for use with this solver. solver_name : Name of the solver being used. @@ -64,7 +64,7 @@ class Regularizer(Base, abc.ABC): Additional keyword arguments to be passed to the solver during instantiation. """ - allowed_algorithms: List[str] = [] + allowed_solvers: List[str] = [] def __init__( self, @@ -113,11 +113,11 @@ def _check_solver(self, solver_name: str): ValueError If the provided solver name is not in the list of allowed optimizers. """ - if solver_name not in self.allowed_algorithms: + if solver_name not in self.allowed_solvers: raise ValueError( f"Solver `{solver_name}` not allowed for " f"{self.__class__} regularization. " - f"Allowed solvers are {self.allowed_algorithms}." + f"Allowed solvers are {self.allowed_solvers}." ) @staticmethod @@ -164,7 +164,7 @@ def get_runner( Parameters ---------- loss : - The loss funciton. + The loss function. run_kwargs : Additional keyword arguments for the solver run. @@ -198,19 +198,19 @@ class UnRegularized(Regularizer): This class provides an interface to various optimization methods for models that do not involve regularization. The optimization methods that are allowed for this - class are defined in the `allowed_optimizers` attribute. + class are defined in the `allowed_solvers` attribute. Attributes ---------- - allowed_optimizers : list of str + allowed_solvers : list of str List of optimizer names that are allowed for this solver class. See Also -------- - [Solver](./#nemos.solver.Solver) : Base solver class from which this class inherits. + [Regularizer](./#nemos.solver.Regularizer) : Base solver class from which this class inherits. """ - allowed_algorithms = [ + allowed_solvers = [ "GradientDescent", "BFGS", "LBFGS", @@ -235,11 +235,11 @@ class Ridge(Regularizer): Attributes ---------- - allowed_optimizers : List[..., str] + allowed_solvers : List[..., str] A list of optimizer names that are allowed to be used with this solver. """ - allowed_algorithms = [ + allowed_solvers = [ "GradientDescent", "BFGS", "LBFGS", @@ -307,18 +307,18 @@ def penalized_loss(params, X, y): class ProxGradientRegularizer(Regularizer, abc.ABC): """ - Solver for optimization using the Proximal Gradient method. + Abstract class for ptimization solvers using the Proximal Gradient method. This class utilizes the `jaxopt` library's Proximal Gradient optimizer. It extends the base Solver class, with the added functionality of a proximal operator. Attributes ---------- - allowed_optimizers : List[...,str] + allowed_solvers : List[...,str] A list of optimizer names that are allowed to be used with this solver. """ - allowed_algorithms = ["ProximalGradient"] + allowed_solvers = ["ProximalGradient"] def __init__( self, @@ -369,7 +369,7 @@ def instantiate_solver( class Lasso(ProxGradientRegularizer): """ - Regularizer for optimization using the Lasso (L1 regularization) method with Proximal Gradient. + Optimization solver using the Lasso (L1 regularization) method with Proximal Gradient. This class is a specialized version of the ProxGradientSolver with the proximal operator set for L1 regularization (Lasso). It utilizes the `jaxopt` library's proximal gradient optimizer. @@ -406,7 +406,7 @@ def prox_op(params, l1reg, scaling=1.0): class GroupLasso(ProxGradientRegularizer): """ - Solver for optimization using the Group Lasso regularization method with Proximal Gradient. + Optimization solver using the Group Lasso regularization method with Proximal Gradient. This class is a specialized version of the ProxGradientSolver with the proximal operator set for Group Lasso regularization. The Group Lasso regularization induces sparsity on groups diff --git a/tests/conftest.py b/tests/conftest.py index 7fdd7b41..16544d0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,8 +41,8 @@ def poissonGLM_model_instantiation(): b_true = np.zeros((1, )) w_true = np.random.normal(size=(1, 5)) observation_model = nmo.observation_models.PoissonObservations(jnp.exp) - solver = nmo.solver.UnRegularized('GradientDescent', {}) - model = nmo.glm.GLM(observation_model, solver) + regularizer = nmo.regularizer.UnRegularized('GradientDescent', {}) + model = nmo.glm.GLM(observation_model, regularizer) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -69,8 +69,8 @@ def poissonGLM_coupled_model_config_simulate(): config_dict = json.load(fh) observations = nmo.observation_models.PoissonObservations(jnp.exp) - solver = nmo.solver.Ridge("BFGS", regularizer_strength=0.1) - model = nmo.glm.GLMRecurrent(observation_model=observations, solver=solver) + regularizer = nmo.regularizer.Ridge("BFGS", regularizer_strength=0.1) + model = nmo.glm.GLMRecurrent(observation_model=observations, regularizer=regularizer) model.coef_ = jnp.asarray(config_dict["coef_"]) model.intercept_ = jnp.asarray(config_dict["intercept_"]) coupling_basis = jnp.asarray(config_dict["coupling_basis"]) @@ -117,8 +117,8 @@ def group_sparse_poisson_glm_model_instantiation(): mask[0, 1:4] = 1 mask[1, [0,4]] = 1 observation_model = nmo.observation_models.PoissonObservations(jnp.exp) - solver = nmo.solver.UnRegularized('GradientDescent', {}) - model = nmo.glm.GLM(observation_model, solver) + regularizer = nmo.regularizer.UnRegularized('GradientDescent', {}) + model = nmo.glm.GLM(observation_model, regularizer) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask @@ -142,12 +142,12 @@ def poisson_observation_model(): @pytest.fixture def ridge_regularizer(): - return nmo.solver.Ridge(solver_name="LBFGS", regularizer_strength=0.1) + return nmo.regularizer.Ridge(solver_name="LBFGS", regularizer_strength=0.1) @pytest.fixture def lasso_regularizer(): - return nmo.solver.Lasso(solver_name="ProximalGradient", regularizer_strength=0.1) + return nmo.regularizer.Lasso(solver_name="ProximalGradient", regularizer_strength=0.1) @pytest.fixture @@ -155,7 +155,7 @@ def group_lasso_2groups_5features_regularizer(): mask = np.zeros((2, 5)) mask[0, :2] = 1 mask[1, 2:] = 1 - return nmo.solver.GroupLasso(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) + return nmo.regularizer.GroupLasso(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) @pytest.fixture diff --git a/tests/test_glm.py b/tests/test_glm.py index a8f83cb2..60c67698 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -34,34 +34,34 @@ class TestGLM: # Test model.__init__ ####################### @pytest.mark.parametrize( - "solver, error, match_str", + "regularizer, error, match_str", [ - (nmo.solver.Ridge("BFGS"), None, None), + (nmo.regularizer.Ridge("BFGS"), None, None), (None, AttributeError, "The provided `solver` doesn't implement "), - (nmo.solver.Ridge, TypeError, "The provided `solver` cannot be instantiated") + (nmo.regularizer.Ridge, TypeError, "The provided `solver` cannot be instantiated") ] ) - def test_solver_type(self, solver, error, match_str, poissonGLM_model_instantiation): + def test_solver_type(self, regularizer, error, match_str, poissonGLM_model_instantiation): """ Test that an error is raised if a non-compatible solver is passed. """ - _test_class_initialization(self.cls, {'solver': solver}, error, match_str) + _test_class_initialization(self.cls, {'regularizer': regularizer}, error, match_str) @pytest.mark.parametrize( "observation, error, match_str", [ (nmo.observation_models.PoissonObservations(), None, None), - (nmo.solver.Regularizer, AttributeError, "The provided object does not have the required"), + (nmo.regularizer.Regularizer, AttributeError, "The provided object does not have the required"), (1, AttributeError, "The provided object does not have the required") ] ) def test_init_observation_type(self, observation, error, match_str, ridge_regularizer): """ - Test initialization with different solver names. Check if an appropriate exception is raised - when the solver name is not present in jaxopt. + Test initialization with different regularizer names. Check if an appropriate exception is raised + when the regularizer name is not present in jaxopt. """ - _test_class_initialization(self.cls, {'solver': ridge_regularizer, 'observation_model': observation}, error, match_str) + _test_class_initialization(self.cls, {'regularizer': ridge_regularizer, 'observation_model': observation}, error, match_str) ####################### # Test model.fit @@ -363,7 +363,7 @@ def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_in def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation): """Test that the group lasso fit goes through""" X, y, model, params, rate, mask = group_sparse_poisson_glm_model_instantiation - model.set_params(solver=nmo.solver.GroupLasso(solver_name="ProximalGradient", mask=mask)) + model.set_params(regularizer=nmo.regularizer.GroupLasso(solver_name="ProximalGradient", mask=mask)) model.fit(X, y) ####################### @@ -1021,7 +1021,7 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): def test_compatibility_with_sklearn_cv(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - param_grid = {"solver__solver_name": ["BFGS", "GradientDescent"]} + param_grid = {"regularizer__solver_name": ["BFGS", "GradientDescent"]} GridSearchCV(model, param_grid).fit(X, y) def test_end_to_end_fit_and_simulate(self, diff --git a/tests/test_solver.py b/tests/test_solver.py index 1d48d3e0..c28785b8 100644 --- a/tests/test_solver.py +++ b/tests/test_solver.py @@ -8,12 +8,12 @@ import nemos as nmo -class TestUnRegularizedSolver: - cls = nmo.solver.UnRegularized +class TestUnRegularized: + cls = nmo.regularizer.UnRegularized @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): - """Test UnRegularizedSolver acceptable solvers.""" + """Test UnRegularized acceptable solvers.""" acceptable_solvers = [ "GradientDescent", "BFGS", @@ -32,7 +32,7 @@ def test_init_solver_name(self, solver_name): @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_set_solver_name_allowed(self, solver_name): - """Test UnRegularizedSolver acceptable solvers.""" + """Test UnRegularized acceptable solvers.""" acceptable_solvers = [ "GradientDescent", "BFGS", @@ -42,13 +42,13 @@ def test_set_solver_name_allowed(self, solver_name): "ScipyBoundedMinimize", "LBFGSB" ] - solver = self.cls("GradientDescent") + regularizer = self.cls("GradientDescent") raise_exception = solver_name not in acceptable_solvers if raise_exception: with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) else: - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) @@ -106,8 +106,8 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 - solver = self.cls("GradientDescent", {"tol": 10**-12}) - runner_bfgs = solver.instantiate_solver(model._score) + regularizer = self.cls("GradientDescent", {"tol": 10**-12}) + runner_bfgs = regularizer.instantiate_solver(model._score) weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=0.) model_skl.fit(X[:,0], y[:, 0]) @@ -115,11 +115,11 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) match_intercepts = np.allclose(model_skl.intercept_, intercepts_bfgs.flatten()) if (not match_weights) or (not match_intercepts): - raise ValueError("Ridge GLM solver estimate does not match sklearn!") + raise ValueError("Ridge GLM regularizer estimate does not match sklearn!") -class TestRidgeSolver: - cls = nmo.solver.Ridge +class TestRidge: + cls = nmo.regularizer.Ridge @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): @@ -152,18 +152,18 @@ def test_set_solver_name_allowed(self, solver_name): "ScipyBoundedMinimize", "LBFGSB" ] - solver = self.cls("GradientDescent") + regularizer = self.cls("GradientDescent") raise_exception = solver_name not in acceptable_solvers if raise_exception: with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) else: - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS"]) @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_name, solver_kwargs): - """Test RidgeSolver acceptable kwargs.""" + """Test Ridge acceptable kwargs.""" raise_exception = "tols" in list(solver_kwargs.keys()) if raise_exception: @@ -216,10 +216,10 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 - solver = self.cls("GradientDescent", {"tol": 10**-12}) - runner_bfgs = solver.instantiate_solver(model._score) + regularizer = self.cls("GradientDescent", {"tol": 10**-12}) + runner_bfgs = regularizer.instantiate_solver(model._score) weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] - model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=solver.regularizer_strength) + model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=regularizer.regularizer_strength) model_skl.fit(X[:,0], y[:, 0]) match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) @@ -228,12 +228,12 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): raise ValueError("Ridge GLM solver estimate does not match sklearn!") -class TestLassoSolver: - cls = nmo.solver.Lasso +class TestLasso: + cls = nmo.regularizer.Lasso @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): - """Test LassoSolver acceptable solvers.""" + """Test Lasso acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] @@ -246,17 +246,17 @@ def test_init_solver_name(self, solver_name): @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_set_solver_name_allowed(self, solver_name): - """Test LassoSolver acceptable solvers.""" + """Test Lasso acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] - solver = self.cls("ProximalGradient") + regularizer = self.cls("ProximalGradient") raise_exception = solver_name not in acceptable_solvers if raise_exception: with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) else: - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_kwargs): @@ -291,8 +291,8 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 - solver = self.cls("ProximalGradient", {"tol": 10**-12}) - runner = solver.instantiate_solver(model._score) + regularizer = self.cls("ProximalGradient", {"tol": 10**-12}) + runner = regularizer.instantiate_solver(model._score) weights, intercepts = runner((true_params[0] * 0., true_params[1]), X, y)[0] # instantiate the glm with statsmodels @@ -301,7 +301,7 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): family=sm.families.Poisson()) # regularize everything except intercept - alpha_sm = np.ones(X.shape[2] + 1) * solver.regularizer_strength + alpha_sm = np.ones(X.shape[2] + 1) * regularizer.regularizer_strength alpha_sm[0] = 0 # pure lasso = elastic net with L1 weight = 1 @@ -316,12 +316,12 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): raise ValueError("Lasso GLM solver estimate does not match statsmodels!") -class TestGroupLassoSolver: - cls = nmo.solver.GroupLasso +class TestGroupLasso: + cls = nmo.regularizer.GroupLasso @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) def test_init_solver_name(self, solver_name): - """Test GroupLassoSolver acceptable solvers.""" + """Test GroupLasso acceptable solvers.""" acceptable_solvers = [ "ProximalGradient" ] @@ -350,17 +350,17 @@ def test_set_solver_name_allowed(self, solver_name): mask[0, :5] = 1 mask[1, 5:] = 1 mask = jnp.asarray(mask) - solver = self.cls("ProximalGradient", mask=mask) + regularizer = self.cls("ProximalGradient", mask=mask) raise_exception = solver_name not in acceptable_solvers if raise_exception: with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) else: - solver.set_params(solver_name=solver_name) + regularizer.set_params(solver_name=solver_name) @pytest.mark.parametrize("solver_kwargs", [{"tol": 10**-10}, {"tols": 10**-10}]) def test_init_solver_kwargs(self, solver_kwargs): - """Test GroupLassoSolver acceptable kwargs.""" + """Test GroupLasso acceptable kwargs.""" raise_exception = "tols" in list(solver_kwargs.keys()) # create a valid mask @@ -538,7 +538,7 @@ def test_mask_validity_groups_set_params(self, mask = np.zeros((2, X.shape[2])) mask[0, :2] = 1 mask[1, 2:] = 1 - solver = self.cls("ProximalGradient", mask) + regularizer = self.cls("ProximalGradient", mask) # change assignment if n_groups_assign == 0: @@ -551,9 +551,9 @@ def test_mask_validity_groups_set_params(self, if raise_exception: with pytest.raises(ValueError, match="Incorrect group assignment. " "Some of the features"): - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) else: - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) @pytest.mark.parametrize("set_entry", [0, 1, -1, 2, 2.5]) def test_mask_validity_entries_set_params(self, set_entry, poissonGLM_model_instantiation): @@ -565,7 +565,7 @@ def test_mask_validity_entries_set_params(self, set_entry, poissonGLM_model_inst mask = np.zeros((2, X.shape[2])) mask[0, :2] = 1 mask[1, 2:] = 1 - solver = self.cls("ProximalGradient", mask) + regularizer = self.cls("ProximalGradient", mask) # assign an entry mask[1, 2] = set_entry @@ -573,9 +573,9 @@ def test_mask_validity_entries_set_params(self, set_entry, poissonGLM_model_inst if raise_exception: with pytest.raises(ValueError, match="Mask elements be 0s and 1s"): - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) else: - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) @pytest.mark.parametrize("n_dim", [0, 1, 2, 3]) def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): @@ -587,7 +587,7 @@ def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): valid_mask = np.zeros((2, X.shape[2])) valid_mask[0, :1] = 1 valid_mask[1, 1:] = 1 - solver = self.cls("ProximalGradient", valid_mask) + regularizer = self.cls("ProximalGradient", valid_mask) # create a mask if n_dim == 0: @@ -607,9 +607,9 @@ def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): if raise_exception: with pytest.raises(ValueError, match="`mask` must be 2-dimensional"): - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) else: - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) @pytest.mark.parametrize("n_groups", [0, 1, 2]) def test_mask_n_groups_set_params(self, n_groups, poissonGLM_model_instantiation): @@ -619,7 +619,7 @@ def test_mask_n_groups_set_params(self, n_groups, poissonGLM_model_instantiation valid_mask = np.zeros((2, X.shape[2])) valid_mask[0, :1] = 1 valid_mask[1, 1:] = 1 - solver = self.cls("ProximalGradient", valid_mask) + regularizer = self.cls("ProximalGradient", valid_mask) # create a mask mask = np.zeros((n_groups, X.shape[2])) @@ -632,8 +632,8 @@ def test_mask_n_groups_set_params(self, n_groups, poissonGLM_model_instantiation if raise_exception: with pytest.raises(ValueError, match=r"Empty mask provided! Mask has "): - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) else: - solver.set_params(mask=mask) + regularizer.set_params(mask=mask) From 7362b0c700c297e8655eca7f6bb81c52dc2a921a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 28 Nov 2023 17:13:20 -0500 Subject: [PATCH 204/250] improved docstring proximal operator --- src/nemos/proximal_operator.py | 2 +- tests/{test_solver.py => test_regularizer.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/{test_solver.py => test_regularizer.py} (100%) diff --git a/src/nemos/proximal_operator.py b/src/nemos/proximal_operator.py index 9049ee1c..6404f1f2 100644 --- a/src/nemos/proximal_operator.py +++ b/src/nemos/proximal_operator.py @@ -62,7 +62,7 @@ def prox_group_lasso( Parameters ---------- params: - Weights, shape (n_neurons, n_features) + Weights, shape (n_neurons, n_features); intercept, shape (n_neurons, ) regularizer_strength: The regularization hyperparameter. mask: diff --git a/tests/test_solver.py b/tests/test_regularizer.py similarity index 100% rename from tests/test_solver.py rename to tests/test_regularizer.py From 1b2e05530745830f2ce8e6eecb3f5873d8afaf6b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 29 Nov 2023 11:53:47 -0500 Subject: [PATCH 205/250] removed ## on module docstrings --- src/nemos/base_class.py | 2 +- src/nemos/utils.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index 0ff30596..e9938e1d 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -1,4 +1,4 @@ -"""## Abstract class for estimators.""" +"""Abstract class for estimators.""" import abc import inspect diff --git a/src/nemos/utils.py b/src/nemos/utils.py index 69209003..cb5f86c5 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -340,14 +340,10 @@ def plot_spike_raster( def row_wise_kron(A: jnp.array, C: jnp.array, jit=False, transpose=True) -> jnp.array: - """Compute the row-wise Kronecker product. + r"""Compute the row-wise Kronecker product. Compute the row-wise Kronecker product between two matrices using JAX. - See[^1] for more details on the Kronecker product. - - [^1]: - Petersen, Kaare Brandt, and Michael Syskind Pedersen. "The matrix cookbook." - Technical University of Denmark 7.15 (2008): 510. + See [\[1\]](#references) for more details on the Kronecker product. Parameters ---------- @@ -370,6 +366,10 @@ def row_wise_kron(A: jnp.array, C: jnp.array, jit=False, transpose=True) -> jnp. This function computes the row-wise Kronecker product between dense matrices A and C using JAX for automatic differentiation and GPU acceleration. + References + ---------- + 1. Petersen, Kaare Brandt, and Michael Syskind Pedersen. "The matrix cookbook." + Technical University of Denmark 7.15 (2008): 510. """ if transpose: A = A.T From 62be95fa39fe74a1af583227fb3fc257dfb52383 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 29 Nov 2023 12:42:51 -0500 Subject: [PATCH 206/250] removed note on prox op, updated baseclass --- docs/developers_notes/02-base_class.md | 2 - docs/theory/proximal_operators.md | 60 -------------------------- mkdocs.yml | 2 - src/nemos/proximal_operator.py | 24 ++++++++++- 4 files changed, 22 insertions(+), 66 deletions(-) delete mode 100644 docs/theory/proximal_operators.md diff --git a/docs/developers_notes/02-base_class.md b/docs/developers_notes/02-base_class.md index f13007a8..521ff992 100644 --- a/docs/developers_notes/02-base_class.md +++ b/docs/developers_notes/02-base_class.md @@ -58,8 +58,6 @@ For a detailed understanding, consult the [`scikit-learn` API Reference](https:/ - **`get_params`**: The `get_params` method retrieves parameters set during model instance initialization. Opting for a deep inspection allows the method to assess nested object parameters, resulting in a comprehensive parameter dictionary. - **`set_params`**: The `set_params` method offers a mechanism to adjust or set an estimator's parameters. It's versatile, accommodating both individual estimators and more complex nested structures like pipelines. Feeding an unrecognized parameter will raise a `ValueError`. -- **`select_target_device`**: Selects either "cpu", "gpu" or "tpu" as the device. -- **`device_put`**: Sends arrays to device, if not on device already. ## The Abstract Class `model_base.BaseRegressor` diff --git a/docs/theory/proximal_operators.md b/docs/theory/proximal_operators.md deleted file mode 100644 index a7da8335..00000000 --- a/docs/theory/proximal_operators.md +++ /dev/null @@ -1,60 +0,0 @@ -# Proximal Methods in Optimization - -## Introduction - -In optimization theory, the proximal operator is a mathematical tool used to solve non-differentiable optimization -problems or to simplify complex ones. - -The proximal operator of a function $ f: \mathbb{R}^n \rightarrow \mathbb{R} \cup \{+\infty\} $ is defined as follows: - -$$ -\text{prox}_f(v) = \arg\min_x \left( f(x) + \frac{1}{2}\Vert x - v\Vert_2 ^2 \right) -$$ - -Here $ \text{prox}_f(v) $ is the value of $ x $ that minimizes the sum of the function $ f(x) $ and the -squared Euclidean distance between $ x $ and some point $ v $. The parameter $ f $ typically represents -a regularization term or a penalty in the optimization problem, and $ v $ is typically a vector -in the domain of $ f $. - -The proximal operator can be thought of as a generalization of the projection operator. When $ f $ is the -indicator function of a convex set $ C $, then $ \text{prox}_f $ is the projection onto $ C $, since -it finds the point in $ C $ closest to $ v $. - -Proximal operators are central to the implementation of proximal gradient[^1] methods and algorithms like where they -help to break down complex optimization problems into simpler sub-problems that can be solved iteratively. - -## Proximal Operators in Proximal Gradient Algorithms - -Proximal gradient algorithms are designed to solve optimization problems of the form: - -$$ -\min_{x \in \mathbb{R}^n} g(x) + f(x) -$$ - -where $ g $ is a differentiable (and typically convex) function, and $ f $ is a (possibly non-differentiable) convex -function that imposes certain structure or sparsity in the solution. The proximal gradient method updates the -solution iteratively through a two-step process: - -1. **Gradient Step on $ g $**: Take a step towards the direction of the negative gradient of $ g $ at the current -estimate $ x_k $, with a step size $ \alpha_k $, leading to an intermediate estimate $ y_k $: - $$ - y_k = x_k - \alpha_k \nabla g(x_k) - $$ -2. **Proximal Step on $ f $**: Apply the proximal operator of $ f $ to the intermediate -estimate $ y_k $ to obtain the new estimate $ x_{k+1} $: - - $$ - x_{k+1} = \text{prox}_{ f}(y_k) = \arg\min_x \left( f(x) + \frac{1}{2\alpha_k}\Vert x - y_k \Vert_2 ^2 \right) - $$ - -The gradient step aims to reduce the value of the smooth part of the objective $ g $, and the proximal step -takes care of the non-smooth part $ f $, often enforcing properties like sparsity due to regularization terms -such as the $ \ell_1 $ norm. - -By iteratively performing these two steps, the proximal gradient algorithm converges to a solution that -balances minimizing the differentiable part $ g $ while respecting the structure imposed by the non-differentiable -part $ f $. The proximal operator effectively "proximates" the solution at each iteration, -taking into account the influence of the non-smooth term $ f $, which would be otherwise challenging to -handle due to its potential non-differentiability. - -[^1]: Parikh, Neal, and Stephen Boyd. "Proximal Algorithms, ser. Foundations and Trends (r) in Optimization." (2013). diff --git a/mkdocs.yml b/mkdocs.yml index 660465a8..abf651ff 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -49,7 +49,5 @@ extra_css: nav: - Home: index.md # Link to the index.md file (home page) - Tutorials: generated/gallery # Link to the generated gallery as Tutorials - - Theory: - - Proximal Methods: theory/proximal_operators.md - For Developers: developers_notes/ # Link to the developers notes - Code References: reference/ # Link to the reference/ directory diff --git a/src/nemos/proximal_operator.py b/src/nemos/proximal_operator.py index 6404f1f2..76607183 100644 --- a/src/nemos/proximal_operator.py +++ b/src/nemos/proximal_operator.py @@ -1,7 +1,27 @@ r"""Collection of proximal operators. -See the theory note on "Proximal Methods" in the package documentation for an introduction to proximal -operators and the Proximal Gradient algorithm. +Proximal operators are a mathematical tools used to solve non-differentiable optimization +problems or to simplify complex ones. + +A classical use-case for proximal operator is that of minimizing a penalized loss function where the +penalization is non-differentiable (Lasso, group Lasso etc.). In proximal gradient algorithms, proximal +operators are used to find the parameters that balance the minimization of the penalty term with + the proximity to the gradient descent update of the un-penalized loss. + +More formally, proximal operators solve the minimization problem, + +$$ +\\text{prox}\_f(\bm{v}) = \arg\min\_{\bm{x}} \left( f(\bm{x}) + \frac{1}{2}\Vert \bm{x} - \bm{v}\Vert_2 ^2 \right) +$$ + + +Where $f$ is usually the non-differentiable penalization term, and $\bm{v}$ is the parameter update of the +un-penalized loss function. The first term controls the penalization magnitude, the second the proximity +with the gradient based update. + +References +---------- +[1] Parikh, Neal, and Stephen Boyd. *"Proximal Algorithms, ser. Foundations and Trends (r) in Optimization."* (2013). """ from typing import Tuple From 8c61fd1f1d608f24abc888411f9e988ef8c659e6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 29 Nov 2023 18:36:30 -0500 Subject: [PATCH 207/250] removed get_runner and use super().instantiate_solver instead --- docs/examples/coupled_neurons_params.json | 10704 +------------------- src/nemos/regularizer.py | 42 +- 2 files changed, 18 insertions(+), 10728 deletions(-) diff --git a/docs/examples/coupled_neurons_params.json b/docs/examples/coupled_neurons_params.json index a0f73586..296bcf24 100644 --- a/docs/examples/coupled_neurons_params.json +++ b/docs/examples/coupled_neurons_params.json @@ -1,10703 +1 @@ -{ - "intercept_": [ - -4.0, - -4.0 - ], - "coef_": [ - [ - -0.004372, - -0.02786, - -0.04582, - -0.0588, - -0.06539, - -0.06396, - -0.05328, - -0.03192, - 0.0002296, - 0.04143, - 0.08794, - 0.1483, - 0.2053, - 0.2483, - 0.2892, - 0.3093, - 0.2917, - 0.2225, - 0.07357, - -0.2711, - -0.006235, - -0.01047, - 0.02189, - 0.058, - 0.09002, - 0.1118, - 0.1209, - 0.1167, - 0.09909, - 0.07044, - 0.03448, - -0.01565, - -0.06823, - -0.1128, - -0.1655, - -0.2176, - -0.2621, - -0.2982, - -0.3255, - -0.3449, - 0.5, - 0.5 - ], - [ - -0.004637, - 0.02223, - 0.07071, - 0.09572, - 0.1012, - 0.08923, - 0.06464, - 0.03076, - -0.007911, - -0.04737, - -0.08429, - -0.1249, - -0.1582, - -0.1827, - -0.2081, - -0.23, - -0.2473, - -0.2616, - -0.2741, - -0.287, - 0.01127, - 0.04864, - 0.0544, - 0.05082, - 0.03975, - 0.02393, - 0.004725, - -0.01763, - -0.04202, - -0.06744, - -0.09269, - -0.1231, - -0.1522, - -0.1763, - -0.2051, - -0.2348, - -0.2629, - -0.2896, - -0.3149, - -0.3389, - 0.5, - 0.5 - ] - ], - "coupling_basis": [ - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0024979173609873673, - 0.9975020826390129 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.11451325277931029, - 0.8854867472206909, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.25013898844998006, - 0.7498610115500185, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.3122501403134024, - 0.687749859686596, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.28176761370807446, - 0.7182323862919272, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.17383844924397923, - 0.8261615507560222, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.04364762794083282, - 0.9563523720591665, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.9912618171282106, - 0.008738182871789013, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.7892946476427273, - 0.21070535235727128, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.3531647741677867, - 0.6468352258322151, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.011883820048045501, - 0.9881161799519544, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.7841665801263835, - 0.21583341987361648, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.17688067665784446, - 0.8231193233421555, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.9253003862638604, - 0.0746996137361397, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.2549435480705588, - 0.7450564519294413, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.9205258993369989, - 0.07947410066300109, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.16827351931758228, - 0.8317264806824178, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.7835282009408713, - 0.21647179905912872, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.019118847416525586, - 0.9808811525834744, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.4372031242218587, - 0.5627968757781414, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.9120243919870162, - 0.08797560801298382, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.044222034278324274, - 0.9557779657216758, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.40793669708774605, - 0.5920633029122541, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.8283923698925478, - 0.17160763010745222, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.0, - 0.9999802058373224, - 1.9794162677666538e-05, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.1458111022283093, - 0.8541888977716907, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.4778824971400245, - 0.5221175028599756, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.803486827077907, - 0.19651317292209308, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.0, - 0.9824675828481839, - 0.017532417151816082, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.029720664099906924, - 0.9702793359000932, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.19724020774947038, - 0.8027597922505296, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.44389603578613035, - 0.5561039642138698, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.6909694421867117, - 0.30903055781328825, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.8804498633788072, - 0.1195501366211929, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.0, - 0.9828262050955638, - 0.017173794904436157, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.005816278861877466, - 0.9941837211381226, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.07171948190677246, - 0.9282805180932275, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.19211081158089233, - 0.8078891884191077, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.3422365913893123, - 0.6577634086106878, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.49997219806462273, - 0.5000278019353773, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.6481581380891199, - 0.3518418619108801, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.775227808426499, - 0.22477219157350103, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.8747644272334134, - 0.12523557276658664, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.9445228823471115, - 0.05547711765288865, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.9852942394771702, - 0.014705760522829736, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.0, - 0.9998405276097415, - 0.00015947239025848603, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.00798856965539202, - 0.9920114303446079, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.03392307742054024, - 0.9660769225794598, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.07373523476821137, - 0.9262647652317886, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.12352988337197751, - 0.8764701166280225, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.17990211564285485, - 0.8200978843571451, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.2399997347398921, - 0.7600002652601079, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.3015222924967669, - 0.6984777075032332, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.36268149196393995, - 0.63731850803606, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.42214108290743424, - 0.5778589170925659, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.47894873221112266, - 0.5210512677888774, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.5324679173051469, - 0.46753208269485313, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.5823146093533313, - 0.4176853906466687, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.6283012081735033, - 0.3716987918264968, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.6703886551778314, - 0.32961134482216864, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.7086466881407022, - 0.2913533118592979, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.7432216468423799, - 0.25677835315762026, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.7743109612271127, - 0.22568903877288732, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.802143356101582, - 0.197856643898418, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.82696381862707, - 0.17303618137292998, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.8490224486822571, - 0.15097755131774288, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.8685664156253453, - 0.13143358437465474, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.8858343578296817, - 0.11416564217031833, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9010526715389762, - 0.09894732846102389, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9144332365128198, - 0.08556676348718023, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9261722145965264, - 0.07382778540347357, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9364496329422705, - 0.06355036705772948, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9454295266061546, - 0.05457047339384541, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9532604668007324, - 0.04673953319926766, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9600763426393057, - 0.039923657360694254, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9659972972699125, - 0.03400270273008754, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.971130745291511, - 0.028869254708488945, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.975572418558468, - 0.024427581441531954, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9794074030288873, - 0.020592596971112653, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9827111411428311, - 0.017288858857168965, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9855503831123861, - 0.014449616887613925, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9879840771076767, - 0.012015922892323394, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9900641931482845, - 0.009935806851715523, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9918364789707291, - 0.008163521029270815, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9933411485659462, - 0.006658851434053759, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9946135057219054, - 0.005386494278094567, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9956845059646938, - 0.004315494035306178, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9965812609202838, - 0.0034187390797163486, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.997327489436671, - 0.002672510563328956, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9979439199017871, - 0.002056080098212898, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9984486481342357, - 0.0015513518657642722, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9988574550621354, - 0.0011425449378646424, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9991840881776304, - 0.0008159118223696749, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.999440510488429, - 0.0005594895115710874, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9996371204027914, - 0.00036287959720865404, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.999782945694725, - 0.00021705430527496627, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9998858144113889, - 0.00011418558861114869, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.9999525053112863, - 4.7494688713622946e-05, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 0.99998888016377, - 1.1119836230089053e-05, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ], - [ - 1.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0 - ] - ], - "feedforward_input": [ - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 2.5 - ], - [ - 2.5 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ], - [ - [ - 0.0 - ], - [ - 0.0 - ] - ] - ], - "init_spikes": [ - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ], - [ - 0.0, - 0.0 - ] - ], - "n_neurons": 2 -} \ No newline at end of file +{"intercept_":[-4.0,-4.0],"coef_":[[-0.004372,-0.02786,-0.04582,-0.0588,-0.06539,-0.06396,-0.05328,-0.03192,0.0002296,0.04143,0.08794,0.1483,0.2053,0.2483,0.2892,0.3093,0.2917,0.2225,0.07357,-0.2711,-0.006235,-0.01047,0.02189,0.058,0.09002,0.1118,0.1209,0.1167,0.09909,0.07044,0.03448,-0.01565,-0.06823,-0.1128,-0.1655,-0.2176,-0.2621,-0.2982,-0.3255,-0.3449,0.5,0.5],[-0.004637,0.02223,0.07071,0.09572,0.1012,0.08923,0.06464,0.03076,-0.007911,-0.04737,-0.08429,-0.1249,-0.1582,-0.1827,-0.2081,-0.23,-0.2473,-0.2616,-0.2741,-0.287,0.01127,0.04864,0.0544,0.05082,0.03975,0.02393,0.004725,-0.01763,-0.04202,-0.06744,-0.09269,-0.1231,-0.1522,-0.1763,-0.2051,-0.2348,-0.2629,-0.2896,-0.3149,-0.3389,0.5,0.5]],"coupling_basis":[[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0024979173609873673,0.9975020826390129],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.11451325277931029,0.8854867472206909,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25013898844998006,0.7498610115500185,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.3122501403134024,0.687749859686596,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.28176761370807446,0.7182323862919272,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17383844924397923,0.8261615507560222,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.04364762794083282,0.9563523720591665,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9912618171282106,0.008738182871789013,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7892946476427273,0.21070535235727128,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.3531647741677867,0.6468352258322151,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.011883820048045501,0.9881161799519544,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7841665801263835,0.21583341987361648,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17688067665784446,0.8231193233421555,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9253003862638604,0.0746996137361397,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.2549435480705588,0.7450564519294413,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9205258993369989,0.07947410066300109,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.16827351931758228,0.8317264806824178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.7835282009408713,0.21647179905912872,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.019118847416525586,0.9808811525834744,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.4372031242218587,0.5627968757781414,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.9120243919870162,0.08797560801298382,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.044222034278324274,0.9557779657216758,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.40793669708774605,0.5920633029122541,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.8283923698925478,0.17160763010745222,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.9999802058373224,1.9794162677666538e-05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.1458111022283093,0.8541888977716907,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.4778824971400245,0.5221175028599756,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.803486827077907,0.19651317292209308,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.9824675828481839,0.017532417151816082,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.029720664099906924,0.9702793359000932,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.19724020774947038,0.8027597922505296,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.44389603578613035,0.5561039642138698,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.6909694421867117,0.30903055781328825,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.8804498633788072,0.1195501366211929,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.9828262050955638,0.017173794904436157,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.005816278861877466,0.9941837211381226,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.07171948190677246,0.9282805180932275,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.19211081158089233,0.8078891884191077,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.3422365913893123,0.6577634086106878,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.49997219806462273,0.5000278019353773,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.6481581380891199,0.3518418619108801,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.775227808426499,0.22477219157350103,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.8747644272334134,0.12523557276658664,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.9445228823471115,0.05547711765288865,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.9852942394771702,0.014705760522829736,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.9998405276097415,0.00015947239025848603,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.00798856965539202,0.9920114303446079,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.03392307742054024,0.9660769225794598,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.07373523476821137,0.9262647652317886,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.12352988337197751,0.8764701166280225,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.17990211564285485,0.8200978843571451,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.2399997347398921,0.7600002652601079,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.3015222924967669,0.6984777075032332,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.36268149196393995,0.63731850803606,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.42214108290743424,0.5778589170925659,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.47894873221112266,0.5210512677888774,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.5324679173051469,0.46753208269485313,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.5823146093533313,0.4176853906466687,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.6283012081735033,0.3716987918264968,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.6703886551778314,0.32961134482216864,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.7086466881407022,0.2913533118592979,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.7432216468423799,0.25677835315762026,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.7743109612271127,0.22568903877288732,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.802143356101582,0.197856643898418,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.82696381862707,0.17303618137292998,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.8490224486822571,0.15097755131774288,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.8685664156253453,0.13143358437465474,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.8858343578296817,0.11416564217031833,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9010526715389762,0.09894732846102389,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9144332365128198,0.08556676348718023,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9261722145965264,0.07382778540347357,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9364496329422705,0.06355036705772948,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9454295266061546,0.05457047339384541,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9532604668007324,0.04673953319926766,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9600763426393057,0.039923657360694254,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9659972972699125,0.03400270273008754,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.971130745291511,0.028869254708488945,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.975572418558468,0.024427581441531954,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9794074030288873,0.020592596971112653,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9827111411428311,0.017288858857168965,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9855503831123861,0.014449616887613925,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9879840771076767,0.012015922892323394,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9900641931482845,0.009935806851715523,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9918364789707291,0.008163521029270815,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9933411485659462,0.006658851434053759,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9946135057219054,0.005386494278094567,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9956845059646938,0.004315494035306178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9965812609202838,0.0034187390797163486,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.997327489436671,0.002672510563328956,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9979439199017871,0.002056080098212898,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9984486481342357,0.0015513518657642722,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9988574550621354,0.0011425449378646424,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9991840881776304,0.0008159118223696749,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.999440510488429,0.0005594895115710874,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9996371204027914,0.00036287959720865404,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.999782945694725,0.00021705430527496627,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9998858144113889,0.00011418558861114869,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9999525053112863,4.7494688713622946e-05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.99998888016377,1.1119836230089053e-05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]],"feedforward_input":[[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]]],"init_spikes":[[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0]],"n_neurons":2} \ No newline at end of file diff --git a/src/nemos/regularizer.py b/src/nemos/regularizer.py index 1a51a2dc..f20f681e 100644 --- a/src/nemos/regularizer.py +++ b/src/nemos/regularizer.py @@ -146,48 +146,40 @@ def _check_solver_kwargs(solver_name, solver_kwargs): def instantiate_solver( self, loss: Callable, *args: Any, **kwargs: Any - ) -> SolverRunner: - """Abstract method to instantiate a solver with a given loss function.""" - # check that the loss is Callable - utils.assert_is_callable(loss, "loss") - return self.get_runner(loss, *args, **kwargs) - - def get_runner( - self, - loss: Callable, - *run_args: Any, - **run_kwargs: dict, ) -> SolverRunner: """ - Get the solver runner with provided arguments. + Instantiate the solver with the provided loss function. + Parameters ---------- loss : - The loss function. - run_kwargs : - Additional keyword arguments for the solver run. + The loss function to be optimized. + + *args: + Positional arguments for the jaxopt `solver.run` method, e.g. the regularizing + strength for proximal gradient methods. + + *kwargs: + Keyword arguments for the jaxopt `solver.run` method. Returns ------- : - The solver runner. - - Raises - ------ - TypeError - If the loss function is not a callable. + A function that runs the solver with the provided loss and proximal operator. """ + # check that the loss is Callable + utils.assert_is_callable(loss, "loss") + # get the solver with given arguments. # The "fun" argument is not always the first one, but it is always KEYWORD # see jaxopt.EqualityConstrainedQP for example. The most general way is to pass it as keyword. solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) def solver_run( - init_params: Tuple[jnp.ndarray, jnp.ndarray], *args: jnp.ndarray + init_params: Tuple[jnp.ndarray, jnp.ndarray], *run_args: jnp.ndarray ) -> jaxopt.OptStep: - args = (*run_args, *args) - return solver.run(init_params, *args, **run_kwargs) + return solver.run(init_params, *args, *run_args, **kwargs) return solver_run @@ -302,7 +294,7 @@ def instantiate_solver( def penalized_loss(params, X, y): return loss(params, X, y) + self._penalization(params) - return self.get_runner(penalized_loss) + return super().instantiate_solver(penalized_loss) class ProxGradientRegularizer(Regularizer, abc.ABC): From 46c2a58774ff485b212e5584ceee607df41421b0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 29 Nov 2023 18:38:57 -0500 Subject: [PATCH 208/250] edited note --- docs/developers_notes/04-regularizer.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/developers_notes/04-regularizer.md b/docs/developers_notes/04-regularizer.md index 3097fd0b..cd077849 100644 --- a/docs/developers_notes/04-regularizer.md +++ b/docs/developers_notes/04-regularizer.md @@ -34,15 +34,9 @@ The abstract class `Regularizer` enforces the implementation of the `instantiate Additionally, the class provides auxiliary methods for checking that the solver and loss function specifications are valid. -### Abstract Methods - -`Regularizer` objects define the following abstract method: - -- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function. The loss function must be of type `Callable`. In particular, this method prepares the arguments, calls, and returns the output of the `get_runner` public method, see **Public Methods** below. - ### Public Methods -- **`get_runner`**: Configure and return a `solver_run` callable. The method accepts two dictionary arguments, `solver_kwargs` and `run_kwargs`, which are meant to hold additional keyword arguments for the instantiation and execution of the solver, respectively. These keyword arguments are prepared by the concrete implementation of `instantiate_solver`, which is solver-type specific. +- **`instantiate_solver`**: Instantiate a solver runner for a provided loss function, configure and return a `solver_run` callable. The loss function must be of type `Callable`. ### Auxiliary Methods From 3d1c11397802fdbcfe442614ed37b4e2e59c61ca Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:17:22 -0500 Subject: [PATCH 209/250] switched to fixture --- tests/test_observation_models.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/tests/test_observation_models.py b/tests/test_observation_models.py index 502a8be8..f0994e07 100644 --- a/tests/test_observation_models.py +++ b/tests/test_observation_models.py @@ -8,33 +8,37 @@ import nemos as nmo +@pytest.fixture() +def poisson_observations(): + return nmo.observation_models.PoissonObservations + + class TestPoissonObservations: - cls = nmo.observation_models.PoissonObservations @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) - def test_initialization_link_is_callable(self, link_function): + def test_initialization_link_is_callable(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" raise_exception = not callable(link_function) if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): - self.cls(link_function) + poisson_observations(link_function) else: - self.cls(link_function) + poisson_observations(link_function) @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x:x, sm.families.links.log()]) - def test_initialization_link_is_jax(self, link_function): + def test_initialization_link_is_jax(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray"): - self.cls(link_function) + poisson_observations(link_function) else: - self.cls(link_function) + poisson_observations(link_function) @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) - def test_initialization_link_is_callable_set_params(self, link_function): + def test_initialization_link_is_callable_set_params(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" - observation_model = self.cls() + observation_model = poisson_observations() raise_exception = not callable(link_function) if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): @@ -43,10 +47,10 @@ def test_initialization_link_is_callable_set_params(self, link_function): observation_model.set_params(inverse_link_function=link_function) @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log()]) - def test_initialization_link_is_jax_set_params(self, link_function): + def test_initialization_link_is_jax_set_params(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) - observation_model = self.cls() + observation_model = poisson_observations() if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray!"): observation_model.set_params(inverse_link_function=link_function) @@ -57,10 +61,10 @@ def test_initialization_link_is_jax_set_params(self, link_function): jnp.exp, lambda x: jnp.exp(x) if isinstance(x, jnp.ndarray) else "not a number" ]) - def test_initialization_link_returns_scalar(self, link_function): + def test_initialization_link_returns_scalar(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" raise_exception = not isinstance(link_function(1.), (jnp.ndarray, float)) - observation_model = self.cls() + observation_model = poisson_observations() if raise_exception: with pytest.raises(TypeError, match="The `inverse_link_function` must handle scalar inputs correctly"): observation_model.set_params(inverse_link_function=link_function) From eb0a677b3f507e7003d353a9e8f5bab775908315 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:39:51 -0500 Subject: [PATCH 210/250] started refractoring tests --- tests/test_glm.py | 72 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 60c67698..8b990b03 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -4,6 +4,7 @@ import pytest import statsmodels.api as sm from sklearn.model_selection import GridSearchCV +from contextlib import nullcontext as does_not_raise import nemos as nmo @@ -23,6 +24,9 @@ def _test_class_method(cls, method_name, args, kwargs, error, match_str): else: getattr(cls, method_name)(*args, **kwargs) +@pytest.fixture() +def glm_class(): + return nmo.glm.GLM class TestGLM: """ @@ -34,45 +38,47 @@ class TestGLM: # Test model.__init__ ####################### @pytest.mark.parametrize( - "regularizer, error, match_str", + "regularizer, expectation", [ - (nmo.regularizer.Ridge("BFGS"), None, None), - (None, AttributeError, "The provided `solver` doesn't implement "), - (nmo.regularizer.Ridge, TypeError, "The provided `solver` cannot be instantiated") + (nmo.regularizer.Ridge("BFGS"), does_not_raise()), + (None, pytest.raises(AttributeError, match="The provided `solver` doesn't implement ")), + (nmo.regularizer.Ridge, pytest.raises(TypeError, match="The provided `solver` cannot be instantiated")) ] ) - def test_solver_type(self, regularizer, error, match_str, poissonGLM_model_instantiation): + def test_solver_type(self, regularizer, expectation, glm_class): """ Test that an error is raised if a non-compatible solver is passed. """ - _test_class_initialization(self.cls, {'regularizer': regularizer}, error, match_str) + with expectation: + glm_class(regularizer=regularizer) @pytest.mark.parametrize( - "observation, error, match_str", + "observation, expectation", [ - (nmo.observation_models.PoissonObservations(), None, None), - (nmo.regularizer.Regularizer, AttributeError, "The provided object does not have the required"), - (1, AttributeError, "The provided object does not have the required") + (nmo.observation_models.PoissonObservations(), does_not_raise()), + (nmo.regularizer.Regularizer, pytest.raises(AttributeError, match="The provided object does not have the required")), + (1, pytest.raises(AttributeError, match="The provided object does not have the required")) ] ) - def test_init_observation_type(self, observation, error, match_str, ridge_regularizer): + def test_init_observation_type(self, observation, expectation, glm_class, ridge_regularizer): """ Test initialization with different regularizer names. Check if an appropriate exception is raised when the regularizer name is not present in jaxopt. """ - _test_class_initialization(self.cls, {'regularizer': ridge_regularizer, 'observation_model': observation}, error, match_str) + with expectation: + glm_class(regularizer=ridge_regularizer, observation_model=observation) ####################### # Test model.fit ####################### - @pytest.mark.parametrize("n_params, error, match_str", [ - (0, ValueError, "Params needs to be array-like of length two."), - (1, ValueError, "Params needs to be array-like of length two."), - (2, None, None), - (3, ValueError, "Params needs to be array-like of length two."), + @pytest.mark.parametrize("n_params, expectation", [ + (0, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), + (1, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), + (2, does_not_raise()), + (3, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), ]) - def test_fit_param_length(self, n_params, error, match_str, poissonGLM_model_instantiation): + def test_fit_param_length(self, n_params, expectation, poissonGLM_model_instantiation): """ Test the `fit` method with different numbers of initial parameters. Check for correct number of parameters. @@ -84,20 +90,18 @@ def test_fit_param_length(self, n_params, error, match_str, poissonGLM_model_ins init_params = (true_params[0],) else: init_params = true_params + (true_params[0],) * (n_params - 2) - _test_class_method(model, "fit", - [X, y], - {"init_params": init_params}, - error, match_str) - - @pytest.mark.parametrize("add_entry, add_to, error, match_str", [ - (0, "X", None, None), - (np.nan, "X", ValueError, "Input array .+ contains"), - (np.inf, "X", ValueError, "Input array .+ contains"), - (0, "y", None, None), - (np.nan, "y", ValueError, "Input array .+ contains"), - (np.inf, "y", ValueError, "Input array .+ contains"), + with expectation: + model.fit(X, y, init_params=init_params) + + @pytest.mark.parametrize("add_entry, add_to, expectation", [ + (0, "X", does_not_raise()), + (np.nan, "X", pytest.raises(ValueError, match="Input array .+ contains")), + (np.inf, "X", pytest.raises(ValueError, match="Input array .+ contains")), + (0, "y", does_not_raise()), + (np.nan, "y", pytest.raises(ValueError, match="Input array .+ contains")), + (np.inf, "y", pytest.raises(ValueError, match="Input array .+ contains")), ]) - def test_fit_param_values(self, add_entry, add_to, error, match_str, poissonGLM_model_instantiation): + def test_fit_param_values(self, add_entry, add_to, expectation, poissonGLM_model_instantiation): """ Test the `fit` method with altered X or y values. Ensure the method raises exceptions for NaN or Inf values. """ @@ -110,10 +114,8 @@ def test_fit_param_values(self, add_entry, add_to, error, match_str, poissonGLM_ idx = np.unravel_index(np.random.choice(y.size), y.shape) y = np.asarray(y, dtype=np.float32) y[idx] = add_entry - _test_class_method(model, "fit", - [X, y], - {"init_params": true_params}, - error, match_str) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("dim_weights, error, match_str", [ (0, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)"), From c9d6f0a4cbf1e9f3e2a3c93806139d813b947e1e Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:41:38 -0500 Subject: [PATCH 211/250] refractored test_fit_weights_dimensionality --- tests/test_glm.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 8b990b03..f89c22c4 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -117,17 +117,13 @@ def test_fit_param_values(self, add_entry, add_to, expectation, poissonGLM_model with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("dim_weights, error, match_str", [ - (0, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)"), - (1, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)"), - (2, None, None), - (3, ValueError, r"params\[0\] must be of shape \(n_neurons, n_features\)") + @pytest.mark.parametrize("dim_weights, expectation", [ + (0, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), + (1, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), + (2, does_not_raise()), + (3, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")) ]) - def test_fit_weights_dimensionality(self, dim_weights, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method with weight matrices of different dimensionalities. - Check for correct dimensionality. - """ + def test_fit_weights_dimensionality(self, dim_weights, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape if dim_weights == 0: @@ -138,7 +134,8 @@ def test_fit_weights_dimensionality(self, dim_weights, error, match_str, poisson init_w = jnp.zeros((n_neurons, n_features)) else: init_w = jnp.zeros((n_neurons, n_features) + (1,) * (dim_weights - 2)) - _test_class_method(model, "fit", [X, y], {"init_params": (init_w, true_params[1])}, error, match_str) + with expectation: + model.fit(X, y, init_params=(init_w, true_params[1])) @pytest.mark.parametrize("dim_intercepts, error, match_str", [ (0, ValueError, r"params\[1\] must be of shape"), From 42e8a2ee8ec109d91d5c48dcebf86e5f58029b06 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:42:31 -0500 Subject: [PATCH 212/250] refractored test_fit_intercepts_dimensionality --- tests/test_glm.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index f89c22c4..4a351e3f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -137,22 +137,19 @@ def test_fit_weights_dimensionality(self, dim_weights, expectation, poissonGLM_m with expectation: model.fit(X, y, init_params=(init_w, true_params[1])) - @pytest.mark.parametrize("dim_intercepts, error, match_str", [ - (0, ValueError, r"params\[1\] must be of shape"), - (1, None, None), - (2, ValueError, r"params\[1\] must be of shape"), - (3, ValueError, r"params\[1\] must be of shape") + @pytest.mark.parametrize("dim_intercepts, expectation", [ + (0, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), + (1, does_not_raise()), + (2, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), + (3, pytest.raises(ValueError, match=r"params\[1\] must be of shape")) ]) - def test_fit_intercepts_dimensionality(self, dim_intercepts, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method with intercepts of different dimensionalities. Check for correct dimensionality. - """ + def test_fit_intercepts_dimensionality(self, dim_intercepts, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape - init_b = jnp.zeros((n_neurons,) * dim_intercepts) init_w = jnp.zeros((n_neurons, n_features)) - _test_class_method(model, "fit", [X, y], {"init_params": (init_w, init_b)}, error, match_str) + with expectation: + model.fit(X, y, init_params=(init_w, init_b)) @pytest.mark.parametrize( "init_params, error, match_str", From c3e7b29915a40fa74d152c94fe9b29ef728db3e2 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:43:00 -0500 Subject: [PATCH 213/250] refractored test_fit_init_params_type --- tests/test_glm.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 4a351e3f..b8d576fb 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -151,26 +151,20 @@ def test_fit_intercepts_dimensionality(self, dim_intercepts, expectation, poisso with expectation: model.fit(X, y, init_params=(init_w, init_b)) - @pytest.mark.parametrize( - "init_params, error, match_str", - [ - ([jnp.zeros((1, 5)), jnp.zeros((1,))], None, None), - (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), None, None), - (dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), TypeError, "Initial parameters must be array-like"), - (0, TypeError, "Initial parameters must be array-like"), - ({0, 1}, ValueError, r"params\[0\] must be of shape"), - ([jnp.zeros((1, 5)), ""], TypeError, "Initial parameters must be array-like"), - (["", jnp.zeros((1,))], TypeError, "Initial parameters must be array-like") - ] - ) - def test_fit_init_params_type(self, init_params, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method with various types of initial parameters. Ensure that the provided initial parameters - are array-like. - """ + @pytest.mark.parametrize("init_params, expectation", [ + ([jnp.zeros((1, 5)), jnp.zeros((1,))], does_not_raise()), + (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), does_not_raise()), + (dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), + pytest.raises(TypeError, match="Initial parameters must be array-like")), + (0, pytest.raises(TypeError, match="Initial parameters must be array-like")), + ({0, 1}, pytest.raises(ValueError, match=r"params\[0\] must be of shape")), + ([jnp.zeros((1, 5)), ""], pytest.raises(TypeError, match="Initial parameters must be array-like")), + (["", jnp.zeros((1,))], pytest.raises(TypeError, match="Initial parameters must be array-like")) + ]) + def test_fit_init_params_type(self, init_params, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - _test_class_method(model, "fit", [X, y], {"init_params": init_params}, error, match_str) - + with expectation: + model.fit(X, y, init_params=init_params) @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ From 332a7ebf11147f957d48ff9e277939fcdfb4fc89 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:44:40 -0500 Subject: [PATCH 214/250] refractored test_fit_n_neuron_match_weights --- tests/test_glm.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index b8d576fb..0fc7df1f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -166,21 +166,17 @@ def test_fit_init_params_type(self, init_params, expectation, poissonGLM_model_i with expectation: model.fit(X, y, init_params=init_params) - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "Model parameters have inconsistent shapes"), - (0, None, None), - (1, ValueError, "Model parameters have inconsistent shapes") - ]) - def test_fit_n_neuron_match_weights(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method ensuring The number of neurons in the weights matches the expected number. - """ - raise_exception = delta_n_neuron != 0 + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")) + ]) + def test_fit_n_neuron_match_weights(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape init_w = jnp.zeros((n_neurons + delta_n_neuron, n_features)) - _test_class_method(model, "fit", [X, y], {"init_params": (init_w, true_params[1])}, error, match_str) + with expectation: + model.fit(X, y, init_params=(init_w, true_params[1])) @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ From 90bbc87785c2d7ed870000a6eff0660b9d00de81 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:45:39 -0500 Subject: [PATCH 215/250] refractored test_fit_n_neuron_match_baseline_rate --- tests/test_glm.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 0fc7df1f..f13f2ccd 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -171,12 +171,12 @@ def test_fit_init_params_type(self, init_params, expectation, poissonGLM_model_i (0, does_not_raise()), (1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")) ]) - def test_fit_n_neuron_match_weights(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape - init_w = jnp.zeros((n_neurons + delta_n_neuron, n_features)) + init_b = jnp.zeros((n_neurons + delta_n_neuron,)) with expectation: - model.fit(X, y, init_params=(init_w, true_params[1])) + model.fit(X, y, init_params=(true_params[0], init_b)) @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ From 6e49aa6dfc772287ad93566476c5ff7193fdae8d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:46:11 -0500 Subject: [PATCH 216/250] refractored test_fit_n_neuron_match_baseline_rate --- tests/test_glm.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index f13f2ccd..d6bccf5f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -178,21 +178,17 @@ def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, expectation, poi with expectation: model.fit(X, y, init_params=(true_params[0], init_b)) - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "Model parameters have inconsistent shapes"), - (0, None, None), - (1, ValueError, "Model parameters have inconsistent shapes") - ]) - def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method ensuring The number of neurons in the baseline rate matches the expected number. - """ - raise_exception = delta_n_neuron != 0 + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")) + ]) + def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape init_b = jnp.zeros((n_neurons + delta_n_neuron,)) - _test_class_method(model, "fit", [X, y], {"init_params": (true_params[0], init_b)}, error, match_str) + with expectation: + model.fit(X, y, init_params=(true_params[0], init_b)) @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ From 2a043f0c0abc9442961ce0e1da12d6121bce2649 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:46:43 -0500 Subject: [PATCH 217/250] refractored test_fit_n_neuron_match_x --- tests/test_glm.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index d6bccf5f..f38b49e9 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -190,22 +190,17 @@ def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, expectation, poi with expectation: model.fit(X, y, init_params=(true_params[0], init_b)) - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_fit_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method ensuring The number of neurons in X matches The number of neurons in the model. - """ - raise_exception = delta_n_neuron != 0 + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_fit_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ From 2468b737cad1a8b47a10400cc8ba6b9615de4239 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:47:14 -0500 Subject: [PATCH 218/250] refractored test_fit_n_neuron_match_y --- tests/test_glm.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index f38b49e9..db29b22b 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -202,21 +202,17 @@ def test_fit_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_mode with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_fit_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method ensuring The number of neurons in y matches The number of neurons in the model. - """ + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_fit_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("delta_dim, error, match_str", [ From 4b6565b3848fffd7eacc3b7462f9a89378db6a33 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:49:29 -0500 Subject: [PATCH 219/250] refractored test_fit_x_dimensionality --- tests/test_glm.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index db29b22b..8a412086 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -214,28 +214,19 @@ def test_fit_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_mode with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_dim, error, match_str", - [ - (-1, ValueError, "X must be three-dimensional"), - (0, None, None), - (1, ValueError, "X must be three-dimensional") - ] - ) - def test_fit_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method with X input data of different dimensionalities. Ensure correct dimensionality for X. - """ + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")) + ]) + def test_fit_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - if delta_dim == -1: - # remove a dimension - X = np.zeros((n_samples, n_neurons)) + X = np.zeros((X.shape[0], X.shape[1])) elif delta_dim == 1: - # add a dimension - X = np.zeros((n_samples, n_neurons, n_features, 1)) - - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + X = np.zeros((X.shape[0], X.shape[1], X.shape[2], 1)) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("delta_dim, error, match_str", [ From 051354041f574e638555f7b22be6f216e6ade097 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:50:05 -0500 Subject: [PATCH 220/250] refractored test_fit_y_dimensionality --- tests/test_glm.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 8a412086..257a611f 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -228,28 +228,19 @@ def test_fit_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_ins with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_dim, error, match_str", - [ - (-1, ValueError, "y must be two-dimensional"), - (0, None, None), - (1, ValueError, "y must be two-dimensional") - ] - ) - def test_fit_y_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method with y target data of different dimensionalities. Ensure correct dimensionality for y. - """ + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="y must be two-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="y must be two-dimensional")) + ]) + def test_fit_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - if delta_dim == -1: - # remove a dimension - y = np.zeros((n_samples, )) + y = np.zeros(X.shape[0]) elif delta_dim == 1: - # add a dimension - y = np.zeros((n_samples, n_neurons, 1)) - - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + y = np.zeros((X.shape[0], X.shape[1], 1)) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("delta_n_features, error, match_str", [ From fb862da6741b13d1629a8976f5493f7e62b3f58b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:50:42 -0500 Subject: [PATCH 221/250] refractored test_fit_n_feature_consistency_weights --- tests/test_glm.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 257a611f..1fa2276e 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -242,24 +242,17 @@ def test_fit_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_ins with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_n_features, error, match_str", - [ - (-1, ValueError, "Inconsistent number of features"), - (0, None, None), - (1, ValueError, "Inconsistent number of features") - ] - ) - def test_fit_n_feature_consistency_weights(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method for inconsistencies between data features and initial weights provided. - Ensure the number of features align. - """ + @pytest.mark.parametrize("delta_n_features, expectation", [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")) + ]) + def test_fit_n_feature_consistency_weights(self, delta_n_features, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - # add/remove a feature from weights - init_w = jnp.zeros((n_neurons, n_features + delta_n_features)) - init_b = jnp.zeros((n_neurons,)) - _test_class_method(model, "fit", [X, y], {"init_params": (init_w,init_b)}, error, match_str) + init_w = jnp.zeros((X.shape[1], X.shape[2] + delta_n_features)) + init_b = jnp.zeros(X.shape[1]) + with expectation: + model.fit(X, y, init_params=(init_w, init_b)) @pytest.mark.parametrize("delta_n_features, error, match_str", [ From 88fe8c85ffd3e789859df2d13e95632440d578c1 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:51:16 -0500 Subject: [PATCH 222/250] refractored test_fit_n_feature_consistency_x --- tests/test_glm.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 1fa2276e..2927f746 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -254,28 +254,19 @@ def test_fit_n_feature_consistency_weights(self, delta_n_features, expectation, with expectation: model.fit(X, y, init_params=(init_w, init_b)) - @pytest.mark.parametrize("delta_n_features, error, match_str", - [ - (-1, ValueError, "Inconsistent number of features"), - (0, None, None), - (1, ValueError, "Inconsistent number of features") - ] - ) - def test_fit_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method for inconsistencies between data features and model's expectations. - Ensure the number of features in X aligns. - """ + @pytest.mark.parametrize("delta_n_features, expectation", [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")) + ]) + def test_fit_n_feature_consistency_x(self, delta_n_features, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - if delta_n_features == 1: - # add a feature - X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + X = jnp.concatenate((X, jnp.zeros((X.shape[0], X.shape[1], 1))), axis=2) elif delta_n_features == -1: - # remove a feature X = X[..., :-1] - - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("delta_tp, error, match_str", [ From 34b9b840e45f816b3c3947d82e92382df1dd1ff7 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:51:43 -0500 Subject: [PATCH 223/250] refractored test_fit_time_points_x --- tests/test_glm.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 2927f746..c9953e63 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -268,20 +268,16 @@ def test_fit_n_feature_consistency_x(self, delta_n_features, expectation, poisso with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_tp, error, match_str", - [ - (-1, ValueError, "The number of time-points in X and y"), - (0, None, None), - (1, ValueError, "The number of time-points in X and y") - ] - ) - def test_fit_time_points_x(self, delta_tp, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method for inconsistencies in time-points in data X. Ensure the correct number of time-points. - """ + @pytest.mark.parametrize("delta_tp, expectation", [ + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")) + ]) + def test_fit_time_points_x(self, delta_tp, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - X = jnp.zeros((X.shape[0] + delta_tp, ) + X.shape[1:]) - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) + with expectation: + model.fit(X, y, init_params=true_params) @pytest.mark.parametrize("delta_tp, error, match_str", [ From 1fc86b285cebb04692050ee3b3c044cbbab37750 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:52:19 -0500 Subject: [PATCH 224/250] test_fit_time_points_y --- tests/test_glm.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index c9953e63..6f355332 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -279,21 +279,16 @@ def test_fit_time_points_x(self, delta_tp, expectation, poissonGLM_model_instant with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_tp, error, match_str", - [ - (-1, ValueError, "The number of time-points in X and y"), - (0, None, None), - (1, ValueError, "The number of time-points in X and y") - ] - ) - def test_fit_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_instantiation): - """ - Test the `fit` method for inconsistencies in time-points in y. Ensure the correct number of time-points. - """ + @pytest.mark.parametrize("delta_tp, expectation", [ + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")) + ]) + def test_fit_time_points_y(self, delta_tp, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) - _test_class_method(model, "fit", [X, y], {"init_params": true_params}, error, match_str) + with expectation: + model.fit(X, y, init_params=true_params) def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation): """Test that the group lasso fit goes through""" From 0071c0750646bf707390bacc12027fdb8671179a Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:53:51 -0500 Subject: [PATCH 225/250] test_score_n_neuron_match_x --- tests/test_glm.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 6f355332..26506dbc 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -299,24 +299,18 @@ def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation) ####################### # Test model.score ####################### - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_score_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method when The number of neurons in X differs. Ensure the correct number of neurons. - """ + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_score_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_neurons = X.shape[1] - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] - X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) - _test_class_method(model, "score", [X, y], {}, error, match_str) + X = jnp.repeat(X, X.shape[1] + delta_n_neuron, axis=1) + with expectation: + model.score(X, y) @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ From 71e6976df10ab5be82f81c04cebd2baa3c5087e0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:54:17 -0500 Subject: [PATCH 226/250] test_score_n_neuron_match_y --- tests/test_glm.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 26506dbc..2c2ed9f3 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -312,25 +312,18 @@ def test_score_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_mo with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_score_n_neuron_match_y(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method when The number of neurons in y differs. Ensure the correct number of neurons. - """ - raise_exception = delta_n_neuron != 0 + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_score_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_neurons = X.shape[1] - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] - y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) - _test_class_method(model, "score", [X, y], {}, error, match_str) + y = jnp.repeat(y, y.shape[1] + delta_n_neuron, axis=1) + with expectation: + model.score(X, y) @pytest.mark.parametrize("delta_dim, error, match_str", [ From 56e86b0ccdf1706244cea2b62be80d63f8b0ea7c Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 09:56:00 -0500 Subject: [PATCH 227/250] test_score_x_dimensionality --- tests/test_glm.py | 30 +++++++++++------------------- 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index 2c2ed9f3..f7f3dda3 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -28,6 +28,7 @@ def _test_class_method(cls, method_name, args, kwargs, error, match_str): def glm_class(): return nmo.glm.GLM + class TestGLM: """ Unit tests for the PoissonGLM class. @@ -325,30 +326,21 @@ def test_score_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_mo with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_dim, error, match_str", - [ - (-1, ValueError, "X must be three-dimensional"), - (0, None, None), - (1, ValueError, "X must be three-dimensional") - ] - ) - def test_score_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method with X input data of different dimensionalities. Ensure correct dimensionality for X. - """ + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")) + ]) + def test_score_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] - if delta_dim == -1: - # remove a dimension - X = np.zeros((n_samples, n_neurons)) + X = np.zeros((X.shape[0], X.shape[1])) elif delta_dim == 1: - # add a dimension - X = np.zeros((n_samples, n_neurons, n_features, 1)) - _test_class_method(model, "score", [X, y], {}, error, match_str) + X = np.zeros((X.shape[0], X.shape[1], X.shape[2], 1)) + with expectation: + model.score(X, y) @pytest.mark.parametrize("delta_dim, error, match_str", [ From 1b2215fecebf6444a4ed8b573170c239bd9c93e6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 10:18:43 -0500 Subject: [PATCH 228/250] completed refractoring --- tests/test_glm.py | 587 +++++++++++++++------------------------------- 1 file changed, 195 insertions(+), 392 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index f7f3dda3..c7172570 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -342,132 +342,99 @@ def test_score_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_i with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_dim, error, match_str", - [ - (-1, ValueError, "y must be two-dimensional, with shape"), - (0, None, None), - (1, ValueError, "y must be two-dimensional, with shape") - ] - ) - def test_score_y_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="y must be two-dimensional, with shape")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="y must be two-dimensional, with shape")) + ]) + def test_score_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): """ Test the `score` method with y of different dimensionalities. Ensure correct dimensionality for y. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, _ = X.shape - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] - if delta_dim == -1: - # remove a dimension - y = np.zeros((n_samples,)) + y = np.zeros((X.shape[0],)) elif delta_dim == 1: - # add a dimension - y = np.zeros((n_samples, n_neurons, 1)) - - _test_class_method(model, "score", [X, y], {}, error, match_str) + y = np.zeros((X.shape[0], X.shape[1], 1)) + with expectation: + model.score(X, y) - @pytest.mark.parametrize("delta_n_features, error, match_str", - [ - (-1, ValueError, "Inconsistent number of features"), - (0, None, None), - (1, ValueError, "Inconsistent number of features") - ] - ) - def test_score_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): + @pytest.mark.parametrize("delta_n_features, expectation", [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")) + ]) + def test_score_n_feature_consistency_x(self, delta_n_features, expectation, poissonGLM_model_instantiation): """ Test the `score` method for inconsistencies in features of X. Ensure the number of features in X aligns with the model params. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] if delta_n_features == 1: - # add a feature - X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + X = jnp.concatenate((X, jnp.zeros((X.shape[0], X.shape[1], 1))), axis=2) elif delta_n_features == -1: - # remove a feature X = X[..., :-1] + with expectation: + model.score(X, y) - _test_class_method(model, "score", [X, y], {}, error, match_str) - - @pytest.mark.parametrize("is_fit, error, match_str", - [ - (True, None, None), - (False, ValueError, "This GLM instance is not fitted yet") - ] - ) - def test_score_is_fit(self, is_fit, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method on models based on their fit status. - Ensure scoring is only possible on fitted models. - """ - raise_exception = not is_fit - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - if is_fit: - model.fit(X, y) - _test_class_method(model, "score", [X, y], {}, error, match_str) - - @pytest.mark.parametrize("delta_tp, error, match_str", - [ - (-1, ValueError, "The number of time-points in X and y"), - (0, None, None), - (1, ValueError, "The number of time-points in X and y") + @pytest.mark.parametrize("is_fit, expectation", [ + (True, does_not_raise()), + (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")) + ]) + def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): + """ + Test the `score` method on models based on their fit status. + Ensure scoring is only possible on fitted models. + """ + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + if is_fit: + model.fit(X, y) + with expectation: + model.predict(X) - ] - ) - def test_score_time_points_x(self, delta_tp, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method for inconsistencies in time-points in X. - Ensure that the number of time-points in X and y matches. - """ + @pytest.mark.parametrize("delta_tp, expectation", [ + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")) + ]) + def test_score_time_points_x(self, delta_tp, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] - X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) - _test_class_method(model, "score", [X, y], {}, error, match_str) - - @pytest.mark.parametrize("delta_tp, error, match_str", - [ - (-1, ValueError, "The number of time-points in X and y"), - (0, None, None), - (1, ValueError, "The number of time-points in X and y") + with expectation: + model.score(X, y) - ] - ) - def test_score_time_points_y(self, delta_tp, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method for inconsistencies in time-points in y. - Ensure that the number of time-points in X and y matches. - """ + @pytest.mark.parametrize("delta_tp, expectation", [ + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")) + ]) + def test_score_time_points_y(self, delta_tp, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] - y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) - _test_class_method(model, "score", [X, y], {}, error, match_str) - - @pytest.mark.parametrize("score_type, error, match_str", [ - ("pseudo-r2-McFadden", None, None), - ("pseudo-r2-Choen", None, None), - ("log-likelihood", None, None), - ("not-implemented", NotImplementedError, "Scoring method %s not implemented") - ] - ) - def test_score_type_r2(self, score_type, error, match_str, poissonGLM_model_instantiation): - """ - Test the `score` method for unsupported scoring types. - Ensure only valid score types are used. - """ + with expectation: + model.score(X, y) + + @pytest.mark.parametrize("score_type, expectation", [ + ("pseudo-r2-McFadden", does_not_raise()), + ("pseudo-r2-Choen", does_not_raise()), + ("log-likelihood", does_not_raise()), + ("not-implemented", pytest.raises(NotImplementedError, match="Scoring method not-implemented not implemented")) + ]) + def test_score_type_r2(self, score_type, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - match_str = match_str % score_type if type(match_str) is str else None model.coef_ = true_params[0] model.intercept_ = true_params[1] - _test_class_method(model, "score", [X, y], {"score_type": score_type}, error, match_str) + with expectation: + model.score(X, y, score_type=score_type) def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation): """ @@ -490,337 +457,198 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) ####################### # Test model.predict ####################### - @pytest.mark.parametrize("delta_n_neuron, error, match_str", [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_predict_n_neuron_match_x(self, delta_n_neuron, error, match_str, poissonGLM_model_instantiation): - """ - Test the `predict` method when The number of neurons in X differs. - Ensure that The number of neurons in X, y and params matches. - """ - raise_exception = delta_n_neuron != 0 + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_predict_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): X, _, model, true_params, _ = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] - X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) - _test_class_method(model, "predict", [X], {}, error, match_str) + X = jnp.repeat(X, X.shape[1] + delta_n_neuron, axis=1) + with expectation: + model.predict(X) - @pytest.mark.parametrize("delta_dim, error, match_str", [ - (-1, ValueError, "X must be three-dimensional"), - (0, None, None), - (1, ValueError, "X must be three-dimensional") - ] - ) - def test_predict_x_dimensionality(self, delta_dim, error, match_str, poissonGLM_model_instantiation): - """ - Test the `predict` method with x input data of different dimensionalities. - Ensure correct dimensionality for x. - """ + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")) + ]) + def test_predict_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] if delta_dim == -1: - # remove a dimension - X = np.zeros((n_samples, n_neurons)) + X = np.zeros((X.shape[0], X.shape[1])) elif delta_dim == 1: - # add a dimension - X = np.zeros((n_samples, n_neurons, n_features, 1)) - _test_class_method(model, "predict", [X], {}, error, match_str) - - @pytest.mark.parametrize("delta_n_features, error, match_str", [ - (-1, ValueError, "Inconsistent number of features"), - (0, None, None), - (1, ValueError, "Inconsistent number of features") - ] - ) - def test_predict_n_feature_consistency_x(self, delta_n_features, error, match_str, poissonGLM_model_instantiation): - """ - Test the `predict` method ensuring the number of features in x input data - is consistent with the model's `model.coef_`. - """ + X = np.zeros((X.shape[0], X.shape[1], X.shape[2], 1)) + with expectation: + model.predict(X) + + @pytest.mark.parametrize("delta_n_features, expectation", [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")) + ]) + def test_predict_n_feature_consistency_x(self, delta_n_features, expectation, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - # set model coeff model.coef_ = true_params[0] model.intercept_ = true_params[1] if delta_n_features == 1: - # add a feature - X = jnp.concatenate((X, jnp.zeros((100, 1, 1))), axis=2) + X = jnp.concatenate((X, jnp.zeros((X.shape[0], X.shape[1], 1))), axis=2) elif delta_n_features == -1: - # remove a feature X = X[..., :-1] - _test_class_method(model, "predict", [X], {}, error, match_str) + with expectation: + model.predict(X) - @pytest.mark.parametrize("is_fit, error, match_str", - [ - (True, None, None), - (False, ValueError, "This GLM instance is not fitted yet") - ] - ) - def test_predict_is_fit(self, is_fit, error, match_str, poissonGLM_model_instantiation): + @pytest.mark.parametrize("is_fit, expectation", [ + (True, does_not_raise()), + (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")) + ]) + def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): """ - Test if the model raises a ValueError when trying to score before it's fitted. + Test the `score` method on models based on their fit status. + Ensure scoring is only possible on fitted models. """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if is_fit: model.fit(X, y) - _test_class_method(model, "predict", [X], {}, error, match_str) + with expectation: + model.predict(X) ####################### # Test model.simulate ####################### - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_simulate_n_neuron_match_input(self, delta_n_neuron, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_simulate_n_neuron_match_input(self, delta_n_neuron, expectation, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method to ensure that The number of neurons in the input matches the model's parameters. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - n_neurons, n_features = model.coef_.shape - n_time_points, _, n_basis_input = feedforward_input.shape + model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate if delta_n_neuron != 0: - feedforward_input = np.zeros((n_time_points, n_neurons+delta_n_neuron, n_basis_input)) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) + feedforward_input = np.zeros( + (feedforward_input.shape[0], feedforward_input.shape[1] + delta_n_neuron, feedforward_input.shape[2])) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - @pytest.mark.parametrize("delta_dim, error, match_str", - [ - (-1, ValueError, "X must be three-dimensional"), - (0, None, None), - (1, ValueError, "X must be three-dimensional") - ] - ) - def test_simulate_input_dimensionality(self, delta_dim, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")) + ]) + def test_simulate_input_dimensionality(self, delta_dim, expectation, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method with input data of different dimensionalities. Ensure correct dimensionality for input. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate + model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate if delta_dim == -1: - # remove a dimension feedforward_input = np.zeros(feedforward_input.shape[:2]) elif delta_dim == 1: - # add a dimension feedforward_input = np.zeros(feedforward_input.shape + (1,)) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - - }, - error, - match_str - ) - - @pytest.mark.parametrize("delta_dim, error, match_str", - [ - (-1, ValueError, "y must be two-dimensional"), - (0, None, None), - (1, ValueError, "y must be two-dimensional") - ] - ) - def test_simulate_y_dimensionality(self, delta_dim, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_dim, expectation", [ + (-1, pytest.raises(ValueError, match="y must be two-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="y must be two-dimensional")) + ]) + def test_simulate_y_dimensionality(self, delta_dim, expectation, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method with init_spikes of different dimensionalities. Ensure correct dimensionality for init_spikes. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - n_samples, n_neurons = feedforward_input.shape[:2] + model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate if delta_dim == -1: - # remove a dimension - init_spikes = np.zeros((n_samples,)) + init_spikes = np.zeros((feedforward_input.shape[0],)) elif delta_dim == 1: - # add a dimension - init_spikes = np.zeros((n_samples, n_neurons, 1)) - - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input, - }, - error, - match_str - ) + init_spikes = np.zeros((feedforward_input.shape[0], feedforward_input.shape[1], 1)) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - @pytest.mark.parametrize("delta_n_neuron, error, match_str", - [ - (-1, ValueError, "The number of neurons in the model parameters"), - (0, None, None), - (1, ValueError, "The number of neurons in the model parameters") - ] - ) - def test_simulate_n_neuron_match_y(self, delta_n_neuron, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_n_neuron, expectation", [ + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) + ]) + def test_simulate_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method to ensure that The number of neurons in init_spikes matches the model's parameters. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - n_samples, n_neurons = feedforward_input.shape[:2] - - init_spikes = jnp.zeros((init_spikes.shape[0], n_neurons + delta_n_neuron)) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) + model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate + init_spikes = jnp.zeros((init_spikes.shape[0], feedforward_input.shape[1] + delta_n_neuron)) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - @pytest.mark.parametrize("is_fit, error, match_str", - [ - (True, None, None), - (False, ValueError, "This GLM instance is not fitted yet") - ] - ) - def test_simulate_is_fit(self, is_fit, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("is_fit, expectation", [ + (True, does_not_raise()), + (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")) + ]) + def test_simulate_is_fit(self, is_fit, expectation, poissonGLM_coupled_model_config_simulate): """ Test if the model raises a ValueError when trying to simulate before it's fitted. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - + model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate if not is_fit: model.intercept_ = None - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) - - @pytest.mark.parametrize("delta_tp, error, match_str", - [ - (-1, ValueError, "`init_y` and `coupling_basis_matrix`"), - (0, None, None), - (1, ValueError, "`init_y` and `coupling_basis_matrix`") + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - ] - ) - def test_simulate_time_point_match_y(self, delta_tp, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_tp, expectation", [ + (-1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")) + ]) + def test_simulate_time_point_match_y(self, delta_tp, expectation, poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method to ensure that the time points in init_y are consistent with the coupling_basis window size (they must be equal). """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate + init_spikes = jnp.zeros((init_spikes.shape[0] + delta_tp, init_spikes.shape[1])) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - init_spikes = jnp.zeros((init_spikes.shape[0] + delta_tp, - init_spikes.shape[1])) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) - - @pytest.mark.parametrize("delta_tp, error, match_str", - [ - (-1, ValueError, "`init_y` and `coupling_basis_matrix`"), - (0, None, None), - (1, ValueError, "`init_y` and `coupling_basis_matrix`") - - ] - ) - def test_simulate_time_point_match_coupling_basis(self, delta_tp, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_tp, expectation", [ + (-1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")) + ]) + def test_simulate_time_point_match_coupling_basis(self, delta_tp, expectation, + poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method to ensure that the window size in coupling_basis is consistent with the time-points in init_spikes (they must be equal). """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate + coupling_basis = jnp.zeros((coupling_basis.shape[0] + delta_tp,) + coupling_basis.shape[1:]) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - coupling_basis = jnp.zeros((coupling_basis.shape[0] + delta_tp,) + - coupling_basis.shape[1:]) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) - - - @pytest.mark.parametrize("delta_features, error, match_str", - [ - (-1, ValueError, "Inconsistent number of features. spike basis coefficients has"), - (0, None, None), - (1, ValueError, "Inconsistent number of features. spike basis coefficients has") - - ] - ) - def test_simulate_feature_consistency_input(self, delta_features, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_features, expectation", [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features. spike basis coefficients has")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features. spike basis coefficients has")) + ]) + def test_simulate_feature_consistency_input(self, delta_features, expectation, + poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method ensuring the number of features in `feedforward_input` is consistent with the model's expected number of features. @@ -832,33 +660,19 @@ def test_simulate_feature_consistency_input(self, delta_features, error, match_s """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate - feedforward_input = jnp.zeros((feedforward_input.shape[0], - feedforward_input.shape[1], - feedforward_input.shape[2] + delta_features)) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) - - @pytest.mark.parametrize("delta_features, error, match_str", - [ - (-1, ValueError, "Inconsistent number of features"), - (0, None, None), - (1, ValueError, "Inconsistent number of features") + feedforward_input = jnp.zeros( + (feedforward_input.shape[0], feedforward_input.shape[1], feedforward_input.shape[2] + delta_features)) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) - ] - ) - def test_simulate_feature_consistency_coupling_basis(self, delta_features, error, match_str, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize("delta_features, expectation", [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")) + ]) + def test_simulate_feature_consistency_coupling_basis(self, delta_features, expectation, + poissonGLM_coupled_model_config_simulate): """ Test the `simulate` method ensuring the number of features in `coupling_basis` is consistent with the model's expected number of features. @@ -870,21 +684,10 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, error """ model, coupling_basis, feedforward_input, init_spikes, random_key = \ poissonGLM_coupled_model_config_simulate - coupling_basis = jnp.zeros((coupling_basis.shape[0], - coupling_basis.shape[1] + delta_features)) - _test_class_method( - model, - "simulate_recurrent", - [], - { - "random_key": random_key, - "init_y": init_spikes, - "coupling_basis_matrix": coupling_basis, - "feedforward_input": feedforward_input - }, - error, - match_str - ) + coupling_basis = jnp.zeros((coupling_basis.shape[0], coupling_basis.shape[1] + delta_features)) + with expectation: + model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input) def test_simulate_feedforward_GLM_not_fit(self, poissonGLM_model_instantiation): X, y, model, params, rate = poissonGLM_model_instantiation From 2786607b7e5489790e370d671010f5ac66cb3283 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 10:37:52 -0500 Subject: [PATCH 229/250] linted everything --- src/nemos/__init__.py | 10 +- src/nemos/base_class.py | 3 +- src/nemos/glm.py | 5 +- src/nemos/observation_models.py | 23 +- src/nemos/regularizer.py | 4 +- src/nemos/utils.py | 3 +- tests/conftest.py | 47 +- tests/test_basis.py | 9 +- tests/test_convolution_1d.py | 7 +- tests/test_glm.py | 1239 +++++++++++++++++++++--------- tests/test_observation_models.py | 91 ++- tests/test_proximal_operator.py | 8 +- tests/test_regularizer.py | 308 +++++--- 13 files changed, 1249 insertions(+), 508 deletions(-) diff --git a/src/nemos/__init__.py b/src/nemos/__init__.py index ef1fe63a..25c573b3 100644 --- a/src/nemos/__init__.py +++ b/src/nemos/__init__.py @@ -1,3 +1,11 @@ #!/usr/bin/env python3 -from . import basis, glm, observation_models, sample_points, regularizer, utils +from . import ( + basis, + exceptions, + glm, + observation_models, + regularizer, + sample_points, + utils, +) diff --git a/src/nemos/base_class.py b/src/nemos/base_class.py index e9938e1d..4ee3ada6 100644 --- a/src/nemos/base_class.py +++ b/src/nemos/base_class.py @@ -4,7 +4,7 @@ import inspect import warnings from collections import defaultdict -from typing import Any, Literal, Optional, Tuple, Union +from typing import Any, Optional, Tuple, Union import jax import jax.numpy as jnp @@ -177,6 +177,7 @@ class BaseRegressor(Base, abc.ABC): - [`GLM`](../glm/#nemos.glm.GLM): A feed-forward GLM implementation. - [`GLMRecurrent`](../glm/#nemos.glm.GLMRecurrent): A recurrent GLM implementation. """ + @abc.abstractmethod def fit(self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray]): """Fit the model to neural activity.""" diff --git a/src/nemos/glm.py b/src/nemos/glm.py index f57eb7ca..bab31121 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -65,10 +65,12 @@ def __init__( @property def regularizer(self): + """Getter for the regularizer attribute.""" return self._regularizer @regularizer.setter def regularizer(self, regularizer: slv.Regularizer): + """Setter for the regularizer attribute.""" if not hasattr(regularizer, "instantiate_solver"): raise AttributeError( "The provided `solver` doesn't implement the `instantiate_solver` method." @@ -85,6 +87,7 @@ def regularizer(self, regularizer: slv.Regularizer): @property def observation_model(self): + """Getter for the observation_model attribute.""" return self._observation_model @observation_model.setter @@ -215,7 +218,7 @@ def score( score_type: Literal["log-likelihood", "pseudo-r2"] = "pseudo-r2-McFadden", pseudo_r2_type: Literal["McFadden", "Cox"] = "McFadden", ) -> jnp.ndarray: - r"""Evaluates the goodness-of-fit of the model to the observed neural data. + r"""Evaluate the goodness-of-fit of the model to the observed neural data. This method computes the goodness-of-fit score, which can either be the mean log-likelihood or the pseudo-R^2. The scoring process includes validation of diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 199a9eb0..e0359ffe 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -87,7 +87,6 @@ def check_inverse_link_function(inverse_link_function: Callable): If the function is not callable, does not return a jax.numpy.ndarray, or is not differentiable. """ - # check that it's callable if not callable(inverse_link_function): raise TypeError("The `inverse_link_function` function must be a Callable!") @@ -206,7 +205,9 @@ def pseudo_r2( self, predicted_rate: jnp.ndarray, y: jnp.ndarray, - score_type: Literal["pseudo-r2-McFadden", "pseudo-r2-Choen"] = "pseudo-r2-McFadden", + score_type: Literal[ + "pseudo-r2-McFadden", "pseudo-r2-Choen" + ] = "pseudo-r2-McFadden", ) -> jnp.ndarray: r"""Pseudo-$R^2$ calculation for a GLM. @@ -253,12 +254,13 @@ def pseudo_r2( References ---------- - 1. McFadden D (1979). Quantitative methods for analysing travel behavior of individuals: Some recent developments. In D. A. Hensher & P. R. Stopher (Eds.), *Behavioural travel modelling* (pp. 279-318). London: Croom Helm. + 1. McFadden D (1979). Quantitative methods for analysing travel behavior of individuals: Some recent + developments. In D. A. Hensher & P. R. Stopher (Eds.), *Behavioural travel modelling* (pp. 279-318). + London: Croom Helm. 2. Jacob Cohen, Patricia Cohen, Steven G. West, Leona S. Aiken. *Applied Multiple Regression/Correlation Analysis for the Behavioral Sciences*. 3rd edition. Routledge, 2002. p.502. ISBN 978-0-8058-2223-6. (May 2012) """ - if score_type == "pseudo-r2-McFadden": pseudo_r2 = self._pseudo_r2_mcfadden(predicted_rate, y) elif score_type == "pseudo-r2-Choen": @@ -267,7 +269,9 @@ def pseudo_r2( raise NotImplementedError(f"Score {score_type} not implemented!") return pseudo_r2 - def _pseudo_r2_choen(self, predicted_rate: jnp.ndarray, y: jnp.ndarray) -> jnp.ndarray: + def _pseudo_r2_choen( + self, predicted_rate: jnp.ndarray, y: jnp.ndarray + ) -> jnp.ndarray: r"""Choen's pseudo-$R^2$. Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002). See @@ -367,7 +371,6 @@ def negative_log_likelihood( Notes ----- - The formula for the Poisson mean log-likelihood is the following, $$ @@ -386,7 +389,9 @@ def negative_log_likelihood( The $\log({y\_{tn}!})$ term is not a function of the parameters and can be disregarded when computing the loss-function. This is why we incorporated it into the `const` term. """ - predicted_rate = jnp.clip(predicted_rate, a_min=jnp.finfo(predicted_rate.dtype).eps) + predicted_rate = jnp.clip( + predicted_rate, a_min=jnp.finfo(predicted_rate.dtype).eps + ) x = y * jnp.log(predicted_rate) # see above for derivation of this. return jnp.mean(predicted_rate - x) @@ -448,7 +453,9 @@ def deviance( log-likelihood. Lower values of deviance indicate a better fit. """ # this takes care of 0s in the log - ratio = jnp.clip(spike_counts / predicted_rate, jnp.finfo(predicted_rate.dtype).eps, jnp.inf) + ratio = jnp.clip( + spike_counts / predicted_rate, jnp.finfo(predicted_rate.dtype).eps, jnp.inf + ) deviance = 2 * (spike_counts * jnp.log(ratio) - (spike_counts - predicted_rate)) return deviance diff --git a/src/nemos/regularizer.py b/src/nemos/regularizer.py index f20f681e..678e9445 100644 --- a/src/nemos/regularizer.py +++ b/src/nemos/regularizer.py @@ -150,7 +150,6 @@ def instantiate_solver( """ Instantiate the solver with the provided loss function. - Parameters ---------- loss : @@ -177,7 +176,7 @@ def instantiate_solver( solver = getattr(jaxopt, self.solver_name)(fun=loss, **self.solver_kwargs) def solver_run( - init_params: Tuple[jnp.ndarray, jnp.ndarray], *run_args: jnp.ndarray + init_params: Tuple[jnp.ndarray, jnp.ndarray], *run_args: jnp.ndarray ) -> jaxopt.OptStep: return solver.run(init_params, *args, *run_args, **kwargs) @@ -429,6 +428,7 @@ def __init__( @property def mask(self): + """Getter for the mask attribute.""" return self._mask @mask.setter diff --git a/src/nemos/utils.py b/src/nemos/utils.py index cb5f86c5..6d52cb9b 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -2,9 +2,8 @@ # required to get ArrayLike to render correctly, unnecessary as of python 3.10 from __future__ import annotations -import inspect from functools import partial -from typing import Any, Callable, Iterable, List, Literal, Optional, Tuple, Union +from typing import Any, Callable, Iterable, List, Literal, Optional, Union import jax import jax.numpy as jnp diff --git a/tests/conftest.py b/tests/conftest.py index 16544d0d..c39dcd4d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,10 +38,10 @@ def poissonGLM_model_instantiation(): """ np.random.seed(123) X = np.random.normal(size=(100, 1, 5)) - b_true = np.zeros((1, )) + b_true = np.zeros((1,)) w_true = np.random.normal(size=(1, 5)) observation_model = nmo.observation_models.PoissonObservations(jnp.exp) - regularizer = nmo.regularizer.UnRegularized('GradientDescent', {}) + regularizer = nmo.regularizer.UnRegularized("GradientDescent", {}) model = nmo.glm.GLM(observation_model, regularizer) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate @@ -64,20 +64,31 @@ def poissonGLM_coupled_model_config_simulate(): """ current_file = inspect.getfile(inspect.currentframe()) test_dir = os.path.dirname(os.path.abspath(current_file)) - with open(os.path.join(test_dir, - "simulate_coupled_neurons_params.json"), "r") as fh: + with open( + os.path.join(test_dir, "simulate_coupled_neurons_params.json"), "r" + ) as fh: config_dict = json.load(fh) observations = nmo.observation_models.PoissonObservations(jnp.exp) regularizer = nmo.regularizer.Ridge("BFGS", regularizer_strength=0.1) - model = nmo.glm.GLMRecurrent(observation_model=observations, regularizer=regularizer) + model = nmo.glm.GLMRecurrent( + observation_model=observations, regularizer=regularizer + ) model.coef_ = jnp.asarray(config_dict["coef_"]) model.intercept_ = jnp.asarray(config_dict["intercept_"]) coupling_basis = jnp.asarray(config_dict["coupling_basis"]) feedforward_input = jnp.asarray(config_dict["feedforward_input"]) init_spikes = jnp.asarray(config_dict["init_spikes"]) - return model, coupling_basis, feedforward_input, init_spikes, jax.random.PRNGKey(123) + return ( + model, + coupling_basis, + feedforward_input, + init_spikes, + jax.random.PRNGKey(123), + ) + + @pytest.fixture def jaxopt_solvers(): return [ @@ -88,7 +99,7 @@ def jaxopt_solvers(): "NonlinearCG", "ScipyBoundedMinimize", "LBFGSB", - "ProximalGradient" + "ProximalGradient", ] @@ -110,14 +121,14 @@ def group_sparse_poisson_glm_model_instantiation(): """ np.random.seed(123) X = np.random.normal(size=(100, 1, 5)) - b_true = np.zeros((1, )) + b_true = np.zeros((1,)) w_true = np.random.normal(size=(1, 5)) - w_true[0, 1:4] = 0. + w_true[0, 1:4] = 0.0 mask = np.zeros((2, 5)) mask[0, 1:4] = 1 - mask[1, [0,4]] = 1 + mask[1, [0, 4]] = 1 observation_model = nmo.observation_models.PoissonObservations(jnp.exp) - regularizer = nmo.regularizer.UnRegularized('GradientDescent', {}) + regularizer = nmo.regularizer.UnRegularized("GradientDescent", {}) model = nmo.glm.GLM(observation_model, regularizer) rate = jax.numpy.exp(jax.numpy.einsum("ik,tik->ti", w_true, X) + b_true[None, :]) return X, np.random.poisson(rate), model, (w_true, b_true), rate, mask @@ -135,6 +146,7 @@ def example_data_prox_operator(): return params, regularizer_strength, mask, scaling + @pytest.fixture def poisson_observation_model(): return nmo.observation_models.PoissonObservations(jnp.exp) @@ -147,7 +159,9 @@ def ridge_regularizer(): @pytest.fixture def lasso_regularizer(): - return nmo.regularizer.Lasso(solver_name="ProximalGradient", regularizer_strength=0.1) + return nmo.regularizer.Lasso( + solver_name="ProximalGradient", regularizer_strength=0.1 + ) @pytest.fixture @@ -155,9 +169,16 @@ def group_lasso_2groups_5features_regularizer(): mask = np.zeros((2, 5)) mask[0, :2] = 1 mask[1, 2:] = 1 - return nmo.regularizer.GroupLasso(solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1) + return nmo.regularizer.GroupLasso( + solver_name="ProximalGradient", mask=mask, regularizer_strength=0.1 + ) @pytest.fixture def mock_data(): return jnp.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]]), jnp.array([[1, 2], [3, 4]]) + + +@pytest.fixture() +def glm_class(): + return nmo.glm.GLM diff --git a/tests/test_basis.py b/tests/test_basis.py index a8ee3941..ed5f38e0 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -295,8 +295,11 @@ def test_minimum_number_of_basis_required_is_matched(self, n_basis_funcs): """ raise_exception = n_basis_funcs < 1 if raise_exception: - with pytest.raises(ValueError, match=f"Object class {self.cls.__name__} " - r"requires >= 1 basis elements\."): + with pytest.raises( + ValueError, + match=f"Object class {self.cls.__name__} " + r"requires >= 1 basis elements\.", + ): self.cls(n_basis_funcs=n_basis_funcs) else: self.cls(n_basis_funcs=n_basis_funcs) @@ -565,7 +568,7 @@ def test_non_empty_samples(self, samples): self.cls(5, decay_rates=np.arange(1, 6)).evaluate(samples) @pytest.mark.parametrize( - "arraylike", [0, [0]*6, (0,)*6, np.array([0]*6), jax.numpy.array([0]*6)] + "arraylike", [0, [0] * 6, (0,) * 6, np.array([0] * 6), jax.numpy.array([0] * 6)] ) def test_input_to_evaluate_is_arraylike(self, arraylike): """ diff --git a/tests/test_convolution_1d.py b/tests/test_convolution_1d.py index fa66a2a3..09959f94 100644 --- a/tests/test_convolution_1d.py +++ b/tests/test_convolution_1d.py @@ -11,8 +11,11 @@ def test_basis_matrix_type(self, basis_matrix, trial_count_shape: tuple[int]): vec = np.ones(trial_count_shape) raise_exception = any(k == 0 for k in basis_matrix.shape) if raise_exception: - with pytest.raises(ValueError, match=r"Empty basis_matrix provided\. " - r"The shape of basis_matrix is \(0, 0\)!"): + with pytest.raises( + ValueError, + match=r"Empty basis_matrix provided\. " + r"The shape of basis_matrix is \(0, 0\)!", + ): utils.convolve_1d_trials(basis_matrix, vec) else: conv = utils.convolve_1d_trials(basis_matrix, vec) diff --git a/tests/test_glm.py b/tests/test_glm.py index c7172570..cec40c3b 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -1,39 +1,19 @@ +from contextlib import nullcontext as does_not_raise + import jax import jax.numpy as jnp import numpy as np import pytest import statsmodels.api as sm from sklearn.model_selection import GridSearchCV -from contextlib import nullcontext as does_not_raise import nemos as nmo -def _test_class_initialization(cls, kwargs, error, match_str): - if error: - with pytest.raises(error, match=match_str): - cls(**kwargs) - else: - cls(**kwargs) - - -def _test_class_method(cls, method_name, args, kwargs, error, match_str): - if error: - with pytest.raises(error, match=match_str): - getattr(cls, method_name)(*args, **kwargs) - else: - getattr(cls, method_name)(*args, **kwargs) - -@pytest.fixture() -def glm_class(): - return nmo.glm.GLM - - class TestGLM: """ Unit tests for the PoissonGLM class. """ - cls = nmo.glm.GLM ####################### # Test model.__init__ @@ -42,9 +22,19 @@ class TestGLM: "regularizer, expectation", [ (nmo.regularizer.Ridge("BFGS"), does_not_raise()), - (None, pytest.raises(AttributeError, match="The provided `solver` doesn't implement ")), - (nmo.regularizer.Ridge, pytest.raises(TypeError, match="The provided `solver` cannot be instantiated")) - ] + ( + None, + pytest.raises( + AttributeError, match="The provided `solver` doesn't implement " + ), + ), + ( + nmo.regularizer.Ridge, + pytest.raises( + TypeError, match="The provided `solver` cannot be instantiated" + ), + ), + ], ) def test_solver_type(self, regularizer, expectation, glm_class): """ @@ -53,16 +43,29 @@ def test_solver_type(self, regularizer, expectation, glm_class): with expectation: glm_class(regularizer=regularizer) - @pytest.mark.parametrize( "observation, expectation", [ (nmo.observation_models.PoissonObservations(), does_not_raise()), - (nmo.regularizer.Regularizer, pytest.raises(AttributeError, match="The provided object does not have the required")), - (1, pytest.raises(AttributeError, match="The provided object does not have the required")) - ] + ( + nmo.regularizer.Regularizer, + pytest.raises( + AttributeError, + match="The provided object does not have the required", + ), + ), + ( + 1, + pytest.raises( + AttributeError, + match="The provided object does not have the required", + ), + ), + ], ) - def test_init_observation_type(self, observation, expectation, glm_class, ridge_regularizer): + def test_init_observation_type( + self, observation, expectation, glm_class, ridge_regularizer + ): """ Test initialization with different regularizer names. Check if an appropriate exception is raised when the regularizer name is not present in jaxopt. @@ -73,13 +76,33 @@ def test_init_observation_type(self, observation, expectation, glm_class, ridge_ ####################### # Test model.fit ####################### - @pytest.mark.parametrize("n_params, expectation", [ - (0, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), - (1, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), - (2, does_not_raise()), - (3, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), - ]) - def test_fit_param_length(self, n_params, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "n_params, expectation", + [ + ( + 0, + pytest.raises( + ValueError, match="Params needs to be array-like of length two." + ), + ), + ( + 1, + pytest.raises( + ValueError, match="Params needs to be array-like of length two." + ), + ), + (2, does_not_raise()), + ( + 3, + pytest.raises( + ValueError, match="Params needs to be array-like of length two." + ), + ), + ], + ) + def test_fit_param_length( + self, n_params, expectation, poissonGLM_model_instantiation + ): """ Test the `fit` method with different numbers of initial parameters. Check for correct number of parameters. @@ -94,15 +117,20 @@ def test_fit_param_length(self, n_params, expectation, poissonGLM_model_instanti with expectation: model.fit(X, y, init_params=init_params) - @pytest.mark.parametrize("add_entry, add_to, expectation", [ - (0, "X", does_not_raise()), - (np.nan, "X", pytest.raises(ValueError, match="Input array .+ contains")), - (np.inf, "X", pytest.raises(ValueError, match="Input array .+ contains")), - (0, "y", does_not_raise()), - (np.nan, "y", pytest.raises(ValueError, match="Input array .+ contains")), - (np.inf, "y", pytest.raises(ValueError, match="Input array .+ contains")), - ]) - def test_fit_param_values(self, add_entry, add_to, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "add_entry, add_to, expectation", + [ + (0, "X", does_not_raise()), + (np.nan, "X", pytest.raises(ValueError, match="Input array .+ contains")), + (np.inf, "X", pytest.raises(ValueError, match="Input array .+ contains")), + (0, "y", does_not_raise()), + (np.nan, "y", pytest.raises(ValueError, match="Input array .+ contains")), + (np.inf, "y", pytest.raises(ValueError, match="Input array .+ contains")), + ], + ) + def test_fit_param_values( + self, add_entry, add_to, expectation, poissonGLM_model_instantiation + ): """ Test the `fit` method with altered X or y values. Ensure the method raises exceptions for NaN or Inf values. """ @@ -118,13 +146,40 @@ def test_fit_param_values(self, add_entry, add_to, expectation, poissonGLM_model with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("dim_weights, expectation", [ - (0, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), - (1, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), - (2, does_not_raise()), - (3, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")) - ]) - def test_fit_weights_dimensionality(self, dim_weights, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "dim_weights, expectation", + [ + ( + 0, + pytest.raises( + ValueError, + match=r"params\[0\] must be of shape \(n_neurons, n_features\)", + ), + ), + ( + 1, + pytest.raises( + ValueError, + match=r"params\[0\] must be of shape \(n_neurons, n_features\)", + ), + ), + (2, does_not_raise()), + ( + 3, + pytest.raises( + ValueError, + match=r"params\[0\] must be of shape \(n_neurons, n_features\)", + ), + ), + ], + ) + def test_fit_weights_dimensionality( + self, dim_weights, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method with weight matrices of different dimensionalities. + Check for correct dimensionality. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape if dim_weights == 0: @@ -138,13 +193,21 @@ def test_fit_weights_dimensionality(self, dim_weights, expectation, poissonGLM_m with expectation: model.fit(X, y, init_params=(init_w, true_params[1])) - @pytest.mark.parametrize("dim_intercepts, expectation", [ - (0, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), - (1, does_not_raise()), - (2, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), - (3, pytest.raises(ValueError, match=r"params\[1\] must be of shape")) - ]) - def test_fit_intercepts_dimensionality(self, dim_intercepts, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "dim_intercepts, expectation", + [ + (0, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), + (1, does_not_raise()), + (2, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), + (3, pytest.raises(ValueError, match=r"params\[1\] must be of shape")), + ], + ) + def test_fit_intercepts_dimensionality( + self, dim_intercepts, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method with intercepts of different dimensionalities. Check for correct dimensionality. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape init_b = jnp.zeros((n_neurons,) * dim_intercepts) @@ -152,75 +215,145 @@ def test_fit_intercepts_dimensionality(self, dim_intercepts, expectation, poisso with expectation: model.fit(X, y, init_params=(init_w, init_b)) - @pytest.mark.parametrize("init_params, expectation", [ - ([jnp.zeros((1, 5)), jnp.zeros((1,))], does_not_raise()), - (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), does_not_raise()), - (dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), - pytest.raises(TypeError, match="Initial parameters must be array-like")), - (0, pytest.raises(TypeError, match="Initial parameters must be array-like")), - ({0, 1}, pytest.raises(ValueError, match=r"params\[0\] must be of shape")), - ([jnp.zeros((1, 5)), ""], pytest.raises(TypeError, match="Initial parameters must be array-like")), - (["", jnp.zeros((1,))], pytest.raises(TypeError, match="Initial parameters must be array-like")) - ]) - def test_fit_init_params_type(self, init_params, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "init_params, expectation", + [ + ([jnp.zeros((1, 5)), jnp.zeros((1,))], does_not_raise()), + (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), does_not_raise()), + ( + dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), + pytest.raises(TypeError, match="Initial parameters must be array-like"), + ), + ( + 0, + pytest.raises(TypeError, match="Initial parameters must be array-like"), + ), + ({0, 1}, pytest.raises(ValueError, match=r"params\[0\] must be of shape")), + ( + [jnp.zeros((1, 5)), ""], + pytest.raises(TypeError, match="Initial parameters must be array-like"), + ), + ( + ["", jnp.zeros((1,))], + pytest.raises(TypeError, match="Initial parameters must be array-like"), + ), + ], + ) + def test_fit_init_params_type( + self, init_params, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method with various types of initial parameters. Ensure that the provided initial parameters + are array-like. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation with expectation: model.fit(X, y, init_params=init_params) - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")) - ]) - def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - n_samples, n_neurons, n_features = X.shape - init_b = jnp.zeros((n_neurons + delta_n_neuron,)) - with expectation: - model.fit(X, y, init_params=(true_params[0], init_b)) - - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")) - ]) - def test_fit_n_neuron_match_baseline_rate(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="Model parameters have inconsistent shapes" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="Model parameters have inconsistent shapes" + ), + ), + ], + ) + def test_fit_n_neuron_match_baseline_rate( + self, delta_n_neuron, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method ensuring The number of neurons in the baseline rate matches the expected number. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_samples, n_neurons, n_features = X.shape init_b = jnp.zeros((n_neurons + delta_n_neuron,)) with expectation: model.fit(X, y, init_params=(true_params[0], init_b)) - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_fit_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_fit_n_neuron_match_x( + self, delta_n_neuron, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method ensuring The number of neurons in X matches The number of neurons in the model. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] X = jnp.repeat(X, n_neurons + delta_n_neuron, axis=1) with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_fit_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_fit_n_neuron_match_y( + self, delta_n_neuron, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method ensuring The number of neurons in y matches The number of neurons in the model. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation n_neurons = X.shape[1] y = jnp.repeat(y, n_neurons + delta_n_neuron, axis=1) with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="X must be three-dimensional")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="X must be three-dimensional")) - ]) - def test_fit_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")), + ], + ) + def test_fit_x_dimensionality( + self, delta_dim, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method with X input data of different dimensionalities. Ensure correct dimensionality for X. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if delta_dim == -1: X = np.zeros((X.shape[0], X.shape[1])) @@ -229,12 +362,20 @@ def test_fit_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_ins with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="y must be two-dimensional")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="y must be two-dimensional")) - ]) - def test_fit_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + (-1, pytest.raises(ValueError, match="y must be two-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="y must be two-dimensional")), + ], + ) + def test_fit_y_dimensionality( + self, delta_dim, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method with y target data of different dimensionalities. Ensure correct dimensionality for y. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if delta_dim == -1: y = np.zeros(X.shape[0]) @@ -243,24 +384,42 @@ def test_fit_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_ins with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_n_features, expectation", [ - (-1, pytest.raises(ValueError, match="Inconsistent number of features")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Inconsistent number of features")) - ]) - def test_fit_n_feature_consistency_weights(self, delta_n_features, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_features, expectation", + [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")), + ], + ) + def test_fit_n_feature_consistency_weights( + self, delta_n_features, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method for inconsistencies between data features and initial weights provided. + Ensure the number of features align. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation init_w = jnp.zeros((X.shape[1], X.shape[2] + delta_n_features)) init_b = jnp.zeros(X.shape[1]) with expectation: model.fit(X, y, init_params=(init_w, init_b)) - @pytest.mark.parametrize("delta_n_features, expectation", [ - (-1, pytest.raises(ValueError, match="Inconsistent number of features")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Inconsistent number of features")) - ]) - def test_fit_n_feature_consistency_x(self, delta_n_features, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_features, expectation", + [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")), + ], + ) + def test_fit_n_feature_consistency_x( + self, delta_n_features, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method for inconsistencies between data features and model's expectations. + Ensure the number of features in X aligns. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation if delta_n_features == 1: X = jnp.concatenate((X, jnp.zeros((X.shape[0], X.shape[1], 1))), axis=2) @@ -269,23 +428,51 @@ def test_fit_n_feature_consistency_x(self, delta_n_features, expectation, poisso with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_tp, expectation", [ - (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of time-points in X and y")) - ]) - def test_fit_time_points_x(self, delta_tp, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_tp, expectation", + [ + ( + -1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + ], + ) + def test_fit_time_points_x( + self, delta_tp, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method for inconsistencies in time-points in data X. Ensure the correct number of time-points. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation X = jnp.zeros((X.shape[0] + delta_tp,) + X.shape[1:]) with expectation: model.fit(X, y, init_params=true_params) - @pytest.mark.parametrize("delta_tp, expectation", [ - (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of time-points in X and y")) - ]) - def test_fit_time_points_y(self, delta_tp, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_tp, expectation", + [ + ( + -1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + ], + ) + def test_fit_time_points_y( + self, delta_tp, expectation, poissonGLM_model_instantiation + ): + """ + Test the `fit` method for inconsistencies in time-points in y. Ensure the correct number of time-points. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation y = jnp.zeros((y.shape[0] + delta_tp,) + y.shape[1:]) with expectation: @@ -294,18 +481,40 @@ def test_fit_time_points_y(self, delta_tp, expectation, poissonGLM_model_instant def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation): """Test that the group lasso fit goes through""" X, y, model, params, rate, mask = group_sparse_poisson_glm_model_instantiation - model.set_params(regularizer=nmo.regularizer.GroupLasso(solver_name="ProximalGradient", mask=mask)) + model.set_params( + regularizer=nmo.regularizer.GroupLasso( + solver_name="ProximalGradient", mask=mask + ) + ) model.fit(X, y) ####################### # Test model.score ####################### - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_score_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_score_n_neuron_match_x( + self, delta_n_neuron, expectation, poissonGLM_model_instantiation + ): + """ + Test the `score` method when The number of neurons in X differs. Ensure the correct number of neurons. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -313,12 +522,30 @@ def test_score_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_mo with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_score_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_score_n_neuron_match_y( + self, delta_n_neuron, expectation, poissonGLM_model_instantiation + ): + """ + Test the `score` method when The number of neurons in y differs. Ensure the correct number of neurons. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -326,12 +553,20 @@ def test_score_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_mo with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="X must be three-dimensional")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="X must be three-dimensional")) - ]) - def test_score_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")), + ], + ) + def test_score_x_dimensionality( + self, delta_dim, expectation, poissonGLM_model_instantiation + ): + """ + Test the `score` method with X input data of different dimensionalities. Ensure correct dimensionality for X. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -342,12 +577,27 @@ def test_score_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_i with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="y must be two-dimensional, with shape")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="y must be two-dimensional, with shape")) - ]) - def test_score_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="y must be two-dimensional, with shape" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="y must be two-dimensional, with shape" + ), + ), + ], + ) + def test_score_y_dimensionality( + self, delta_dim, expectation, poissonGLM_model_instantiation + ): """ Test the `score` method with y of different dimensionalities. Ensure correct dimensionality for y. @@ -362,12 +612,17 @@ def test_score_y_dimensionality(self, delta_dim, expectation, poissonGLM_model_i with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_n_features, expectation", [ - (-1, pytest.raises(ValueError, match="Inconsistent number of features")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Inconsistent number of features")) - ]) - def test_score_n_feature_consistency_x(self, delta_n_features, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_features, expectation", + [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")), + ], + ) + def test_score_n_feature_consistency_x( + self, delta_n_features, expectation, poissonGLM_model_instantiation + ): """ Test the `score` method for inconsistencies in features of X. Ensure the number of features in X aligns with the model params. @@ -382,27 +637,48 @@ def test_score_n_feature_consistency_x(self, delta_n_features, expectation, pois with expectation: model.score(X, y) - @pytest.mark.parametrize("is_fit, expectation", [ + @pytest.mark.parametrize( + "is_fit, expectation", + [ (True, does_not_raise()), - (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")) - ]) - def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): - """ - Test the `score` method on models based on their fit status. - Ensure scoring is only possible on fitted models. - """ - X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - if is_fit: - model.fit(X, y) - with expectation: - model.predict(X) - - @pytest.mark.parametrize("delta_tp, expectation", [ - (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of time-points in X and y")) - ]) - def test_score_time_points_x(self, delta_tp, expectation, poissonGLM_model_instantiation): + ( + False, + pytest.raises(ValueError, match="This GLM instance is not fitted yet"), + ), + ], + ) + def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): + """ + Test the `score` method on models based on their fit status. + Ensure scoring is only possible on fitted models. + """ + X, y, model, true_params, firing_rate = poissonGLM_model_instantiation + if is_fit: + model.fit(X, y) + with expectation: + model.predict(X) + + @pytest.mark.parametrize( + "delta_tp, expectation", + [ + ( + -1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + ], + ) + def test_score_time_points_x( + self, delta_tp, expectation, poissonGLM_model_instantiation + ): + """ + Test the `score` method for inconsistencies in time-points in X. + Ensure that the number of time-points in X and y matches. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -410,12 +686,27 @@ def test_score_time_points_x(self, delta_tp, expectation, poissonGLM_model_insta with expectation: model.score(X, y) - @pytest.mark.parametrize("delta_tp, expectation", [ - (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of time-points in X and y")) - ]) - def test_score_time_points_y(self, delta_tp, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_tp, expectation", + [ + ( + -1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises(ValueError, match="The number of time-points in X and y"), + ), + ], + ) + def test_score_time_points_y( + self, delta_tp, expectation, poissonGLM_model_instantiation + ): + """ + Test the `score` method for inconsistencies in time-points in y. + Ensure that the number of time-points in X and y matches. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -423,13 +714,28 @@ def test_score_time_points_y(self, delta_tp, expectation, poissonGLM_model_insta with expectation: model.score(X, y) - @pytest.mark.parametrize("score_type, expectation", [ - ("pseudo-r2-McFadden", does_not_raise()), - ("pseudo-r2-Choen", does_not_raise()), - ("log-likelihood", does_not_raise()), - ("not-implemented", pytest.raises(NotImplementedError, match="Scoring method not-implemented not implemented")) - ]) - def test_score_type_r2(self, score_type, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "score_type, expectation", + [ + ("pseudo-r2-McFadden", does_not_raise()), + ("pseudo-r2-Choen", does_not_raise()), + ("log-likelihood", does_not_raise()), + ( + "not-implemented", + pytest.raises( + NotImplementedError, + match="Scoring method not-implemented not implemented", + ), + ), + ], + ) + def test_score_type_r2( + self, score_type, expectation, poissonGLM_model_instantiation + ): + """ + Test the `score` method for unsupported scoring types. + Ensure only valid score types are used. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -451,18 +757,38 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) mean_ll_jax = jax.scipy.stats.poisson.logpmf(y, mean_firing).mean() model_ll = model.score(X, y, score_type="log-likelihood") if not np.allclose(mean_ll_jax, model_ll): - raise ValueError("Log-likelihood of PoissonModel does not match" - "that of jax.scipy!") + raise ValueError( + "Log-likelihood of PoissonModel does not match" "that of jax.scipy!" + ) ####################### # Test model.predict ####################### - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_predict_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_predict_n_neuron_match_x( + self, delta_n_neuron, expectation, poissonGLM_model_instantiation + ): + """ + Test the `predict` method when The number of neurons in X differs. + Ensure that The number of neurons in X, y and params matches. + """ X, _, model, true_params, _ = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -470,12 +796,21 @@ def test_predict_n_neuron_match_x(self, delta_n_neuron, expectation, poissonGLM_ with expectation: model.predict(X) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="X must be three-dimensional")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="X must be three-dimensional")) - ]) - def test_predict_x_dimensionality(self, delta_dim, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")), + ], + ) + def test_predict_x_dimensionality( + self, delta_dim, expectation, poissonGLM_model_instantiation + ): + """ + Test the `predict` method with x input data of different dimensionalities. + Ensure correct dimensionality for x. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -486,12 +821,21 @@ def test_predict_x_dimensionality(self, delta_dim, expectation, poissonGLM_model with expectation: model.predict(X) - @pytest.mark.parametrize("delta_n_features, expectation", [ - (-1, pytest.raises(ValueError, match="Inconsistent number of features")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Inconsistent number of features")) - ]) - def test_predict_n_feature_consistency_x(self, delta_n_features, expectation, poissonGLM_model_instantiation): + @pytest.mark.parametrize( + "delta_n_features, expectation", + [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")), + ], + ) + def test_predict_n_feature_consistency_x( + self, delta_n_features, expectation, poissonGLM_model_instantiation + ): + """ + Test the `predict` method ensuring the number of features in x input data + is consistent with the model's `model.coef_`. + """ X, y, model, true_params, firing_rate = poissonGLM_model_instantiation model.coef_ = true_params[0] model.intercept_ = true_params[1] @@ -502,10 +846,16 @@ def test_predict_n_feature_consistency_x(self, delta_n_features, expectation, po with expectation: model.predict(X) - @pytest.mark.parametrize("is_fit, expectation", [ - (True, does_not_raise()), - (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")) - ]) + @pytest.mark.parametrize( + "is_fit, expectation", + [ + (True, does_not_raise()), + ( + False, + pytest.raises(ValueError, match="This GLM instance is not fitted yet"), + ), + ], + ) def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): """ Test the `score` method on models based on their fit status. @@ -520,135 +870,299 @@ def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiatio ####################### # Test model.simulate ####################### - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_simulate_n_neuron_match_input(self, delta_n_neuron, expectation, poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_simulate_n_neuron_match_input( + self, delta_n_neuron, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method to ensure that The number of neurons in the input matches the model's parameters. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate if delta_n_neuron != 0: feedforward_input = np.zeros( - (feedforward_input.shape[0], feedforward_input.shape[1] + delta_n_neuron, feedforward_input.shape[2])) + ( + feedforward_input.shape[0], + feedforward_input.shape[1] + delta_n_neuron, + feedforward_input.shape[2], + ) + ) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="X must be three-dimensional")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="X must be three-dimensional")) - ]) - def test_simulate_input_dimensionality(self, delta_dim, expectation, poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + (-1, pytest.raises(ValueError, match="X must be three-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="X must be three-dimensional")), + ], + ) + def test_simulate_input_dimensionality( + self, delta_dim, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method with input data of different dimensionalities. Ensure correct dimensionality for input. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate if delta_dim == -1: feedforward_input = np.zeros(feedforward_input.shape[:2]) elif delta_dim == 1: feedforward_input = np.zeros(feedforward_input.shape + (1,)) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_dim, expectation", [ - (-1, pytest.raises(ValueError, match="y must be two-dimensional")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="y must be two-dimensional")) - ]) - def test_simulate_y_dimensionality(self, delta_dim, expectation, poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_dim, expectation", + [ + (-1, pytest.raises(ValueError, match="y must be two-dimensional")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="y must be two-dimensional")), + ], + ) + def test_simulate_y_dimensionality( + self, delta_dim, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method with init_spikes of different dimensionalities. Ensure correct dimensionality for init_spikes. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate if delta_dim == -1: init_spikes = np.zeros((feedforward_input.shape[0],)) elif delta_dim == 1: - init_spikes = np.zeros((feedforward_input.shape[0], feedforward_input.shape[1], 1)) + init_spikes = np.zeros( + (feedforward_input.shape[0], feedforward_input.shape[1], 1) + ) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_n_neuron, expectation", [ - (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")) - ]) - def test_simulate_n_neuron_match_y(self, delta_n_neuron, expectation, poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_n_neuron, expectation", + [ + ( + -1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, match="The number of neurons in the model parameters" + ), + ), + ], + ) + def test_simulate_n_neuron_match_y( + self, delta_n_neuron, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method to ensure that The number of neurons in init_spikes matches the model's parameters. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate - init_spikes = jnp.zeros((init_spikes.shape[0], feedforward_input.shape[1] + delta_n_neuron)) + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate + init_spikes = jnp.zeros( + (init_spikes.shape[0], feedforward_input.shape[1] + delta_n_neuron) + ) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("is_fit, expectation", [ - (True, does_not_raise()), - (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")) - ]) - def test_simulate_is_fit(self, is_fit, expectation, poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "is_fit, expectation", + [ + (True, does_not_raise()), + ( + False, + pytest.raises(ValueError, match="This GLM instance is not fitted yet"), + ), + ], + ) + def test_simulate_is_fit( + self, is_fit, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test if the model raises a ValueError when trying to simulate before it's fitted. """ - model, coupling_basis, feedforward_input, init_spikes, random_key = poissonGLM_coupled_model_config_simulate + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate if not is_fit: model.intercept_ = None with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_tp, expectation", [ - (-1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")) - ]) - def test_simulate_time_point_match_y(self, delta_tp, expectation, poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_tp, expectation", + [ + ( + -1, + pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), + ), + ], + ) + def test_simulate_time_point_match_y( + self, delta_tp, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method to ensure that the time points in init_y are consistent with the coupling_basis window size (they must be equal). """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate init_spikes = jnp.zeros((init_spikes.shape[0] + delta_tp, init_spikes.shape[1])) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_tp, expectation", [ - (-1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")) - ]) - def test_simulate_time_point_match_coupling_basis(self, delta_tp, expectation, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_tp, expectation", + [ + ( + -1, + pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), + ), + ], + ) + def test_simulate_time_point_match_coupling_basis( + self, delta_tp, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method to ensure that the window size in coupling_basis is consistent with the time-points in init_spikes (they must be equal). """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - coupling_basis = jnp.zeros((coupling_basis.shape[0] + delta_tp,) + coupling_basis.shape[1:]) + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate + coupling_basis = jnp.zeros( + (coupling_basis.shape[0] + delta_tp,) + coupling_basis.shape[1:] + ) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_features, expectation", [ - (-1, pytest.raises(ValueError, match="Inconsistent number of features. spike basis coefficients has")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Inconsistent number of features. spike basis coefficients has")) - ]) - def test_simulate_feature_consistency_input(self, delta_features, expectation, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_features, expectation", + [ + ( + -1, + pytest.raises( + ValueError, + match="Inconsistent number of features. spike basis coefficients has", + ), + ), + (0, does_not_raise()), + ( + 1, + pytest.raises( + ValueError, + match="Inconsistent number of features. spike basis coefficients has", + ), + ), + ], + ) + def test_simulate_feature_consistency_input( + self, delta_features, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method ensuring the number of features in `feedforward_input` is consistent with the model's expected number of features. @@ -658,21 +1172,39 @@ def test_simulate_feature_consistency_input(self, delta_features, expectation, The total feature number `model.coef_.shape[1]` must be equal to `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate feedforward_input = jnp.zeros( - (feedforward_input.shape[0], feedforward_input.shape[1], feedforward_input.shape[2] + delta_features)) + ( + feedforward_input.shape[0], + feedforward_input.shape[1], + feedforward_input.shape[2] + delta_features, + ) + ) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) - @pytest.mark.parametrize("delta_features, expectation", [ - (-1, pytest.raises(ValueError, match="Inconsistent number of features")), - (0, does_not_raise()), - (1, pytest.raises(ValueError, match="Inconsistent number of features")) - ]) - def test_simulate_feature_consistency_coupling_basis(self, delta_features, expectation, - poissonGLM_coupled_model_config_simulate): + @pytest.mark.parametrize( + "delta_features, expectation", + [ + (-1, pytest.raises(ValueError, match="Inconsistent number of features")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match="Inconsistent number of features")), + ], + ) + def test_simulate_feature_consistency_coupling_basis( + self, delta_features, expectation, poissonGLM_coupled_model_config_simulate + ): """ Test the `simulate` method ensuring the number of features in `coupling_basis` is consistent with the model's expected number of features. @@ -682,17 +1214,29 @@ def test_simulate_feature_consistency_coupling_basis(self, delta_features, expec The total feature number `model.coef_.shape[1]` must be equal to `feedforward_input.shape[2] + coupling_basis.shape[1]*n_neurons` """ - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate - coupling_basis = jnp.zeros((coupling_basis.shape[0], coupling_basis.shape[1] + delta_features)) + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate + coupling_basis = jnp.zeros( + (coupling_basis.shape[0], coupling_basis.shape[1] + delta_features) + ) with expectation: - model.simulate_recurrent(random_key=random_key, init_y=init_spikes, coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) def test_simulate_feedforward_GLM_not_fit(self, poissonGLM_model_instantiation): X, y, model, params, rate = poissonGLM_model_instantiation - with pytest.raises(nmo.exceptions.NotFittedError, - match="This GLM instance is not fitted yet"): + with pytest.raises( + nmo.exceptions.NotFittedError, match="This GLM instance is not fitted yet" + ): model.simulate(jax.random.PRNGKey(123), X) def test_simulate_feedforward_GLM(self, poissonGLM_model_instantiation): @@ -730,58 +1274,63 @@ def test_deviance_against_statsmodels(self, poissonGLM_model_instantiation): if not np.allclose(dev, dev_model): raise ValueError("Deviance doesn't match statsmodels!") - def test_compatibility_with_sklearn_cv(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation param_grid = {"regularizer__solver_name": ["BFGS", "GradientDescent"]} GridSearchCV(model, param_grid).fit(X, y) - def test_end_to_end_fit_and_simulate(self, - poissonGLM_coupled_model_config_simulate): - model, coupling_basis, feedforward_input, init_spikes, random_key = \ - poissonGLM_coupled_model_config_simulate + def test_end_to_end_fit_and_simulate( + self, poissonGLM_coupled_model_config_simulate + ): + ( + model, + coupling_basis, + feedforward_input, + init_spikes, + random_key, + ) = poissonGLM_coupled_model_config_simulate window_size = coupling_basis.shape[0] n_neurons = init_spikes.shape[1] n_trials = 1 n_timepoints = feedforward_input.shape[0] # generate spike trains - spikes, _ = model.simulate_recurrent(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) + spikes, _ = model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) # convolve basis and spikes # (n_trials, n_timepoints - ws + 1, n_neurons, n_coupling_basis) conv_spikes = jnp.asarray( - nmo.utils.convolve_1d_trials(coupling_basis, [spikes]), - dtype=jnp.float32 + nmo.utils.convolve_1d_trials(coupling_basis, [spikes]), dtype=jnp.float32 ) # create an individual neuron predictor by stacking the # two convolved spike trains in a single feature vector # and concatenate the trials. - conv_spikes = conv_spikes.reshape(n_trials * (n_timepoints - window_size + 1), -1) + conv_spikes = conv_spikes.reshape( + n_trials * (n_timepoints - window_size + 1), -1 + ) # replicate for each neuron, # (n_trials * (n_timepoints - ws + 1), n_neurons, n_neurons * n_coupling_basis) - conv_spikes = jnp.tile(conv_spikes, n_neurons).reshape(conv_spikes.shape[0], - n_neurons, - conv_spikes.shape[1]) + conv_spikes = jnp.tile(conv_spikes, n_neurons).reshape( + conv_spikes.shape[0], n_neurons, conv_spikes.shape[1] + ) # add the feed-forward input to the predictors - X = jnp.concatenate((conv_spikes[1:], - feedforward_input[:-window_size]), - axis=2) + X = jnp.concatenate((conv_spikes[1:], feedforward_input[:-window_size]), axis=2) # fit the model model.fit(X, spikes[:-window_size]) # simulate - model.simulate_recurrent(random_key=random_key, - init_y=init_spikes, - coupling_basis_matrix=coupling_basis, - feedforward_input=feedforward_input) - - - + model.simulate_recurrent( + random_key=random_key, + init_y=init_spikes, + coupling_basis_matrix=coupling_basis, + feedforward_input=feedforward_input, + ) diff --git a/tests/test_observation_models.py b/tests/test_observation_models.py index f0994e07..8e9a9ff7 100644 --- a/tests/test_observation_models.py +++ b/tests/test_observation_models.py @@ -14,59 +14,90 @@ def poisson_observations(): class TestPoissonObservations: - @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) def test_initialization_link_is_callable(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" raise_exception = not callable(link_function) if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): + with pytest.raises( + TypeError, + match="The `inverse_link_function` function must be a Callable", + ): poisson_observations(link_function) else: poisson_observations(link_function) - @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x:x, sm.families.links.log()]) + @pytest.mark.parametrize( + "link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log()] + ) def test_initialization_link_is_jax(self, link_function, poisson_observations): """Check that the observation model initializes when a callable is passed.""" - raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) + raise_exception = isinstance(link_function, np.ufunc) | isinstance( + link_function, sm.families.links.Link + ) if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray"): + with pytest.raises( + TypeError, + match="The `inverse_link_function` must return a jax.numpy.ndarray", + ): poisson_observations(link_function) else: poisson_observations(link_function) @pytest.mark.parametrize("link_function", [jnp.exp, jax.nn.softplus, 1]) - def test_initialization_link_is_callable_set_params(self, link_function, poisson_observations): + def test_initialization_link_is_callable_set_params( + self, link_function, poisson_observations + ): """Check that the observation model initializes when a callable is passed.""" observation_model = poisson_observations() raise_exception = not callable(link_function) if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` function must be a Callable"): + with pytest.raises( + TypeError, + match="The `inverse_link_function` function must be a Callable", + ): observation_model.set_params(inverse_link_function=link_function) else: observation_model.set_params(inverse_link_function=link_function) - @pytest.mark.parametrize("link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log()]) - def test_initialization_link_is_jax_set_params(self, link_function, poisson_observations): + @pytest.mark.parametrize( + "link_function", [jnp.exp, np.exp, lambda x: x, sm.families.links.log()] + ) + def test_initialization_link_is_jax_set_params( + self, link_function, poisson_observations + ): """Check that the observation model initializes when a callable is passed.""" - raise_exception = isinstance(link_function, np.ufunc) | isinstance(link_function, sm.families.links.Link) + raise_exception = isinstance(link_function, np.ufunc) | isinstance( + link_function, sm.families.links.Link + ) observation_model = poisson_observations() if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` must return a jax.numpy.ndarray!"): + with pytest.raises( + TypeError, + match="The `inverse_link_function` must return a jax.numpy.ndarray!", + ): observation_model.set_params(inverse_link_function=link_function) else: observation_model.set_params(inverse_link_function=link_function) - @pytest.mark.parametrize("link_function", [ - jnp.exp, - lambda x: jnp.exp(x) if isinstance(x, jnp.ndarray) else "not a number" - ]) - def test_initialization_link_returns_scalar(self, link_function, poisson_observations): + @pytest.mark.parametrize( + "link_function", + [ + jnp.exp, + lambda x: jnp.exp(x) if isinstance(x, jnp.ndarray) else "not a number", + ], + ) + def test_initialization_link_returns_scalar( + self, link_function, poisson_observations + ): """Check that the observation model initializes when a callable is passed.""" - raise_exception = not isinstance(link_function(1.), (jnp.ndarray, float)) + raise_exception = not isinstance(link_function(1.0), (jnp.ndarray, float)) observation_model = poisson_observations() if raise_exception: - with pytest.raises(TypeError, match="The `inverse_link_function` must handle scalar inputs correctly"): + with pytest.raises( + TypeError, + match="The `inverse_link_function` must handle scalar inputs correctly", + ): observation_model.set_params(inverse_link_function=link_function) else: observation_model.set_params(inverse_link_function=link_function) @@ -88,11 +119,13 @@ def test_loglikelihood_against_scipy(self, poissonGLM_model_instantiation): Assesses if the model estimates are close to statsmodels' results. """ _, y, model, _, firing_rate = poissonGLM_model_instantiation - ll_model = - model.observation_model.negative_log_likelihood(firing_rate, y).sum()\ - - jax.scipy.special.gammaln(y + 1).mean() + ll_model = ( + -model.observation_model.negative_log_likelihood(firing_rate, y).sum() + - jax.scipy.special.gammaln(y + 1).mean() + ) ll_scipy = sts.poisson(firing_rate).logpmf(y).mean() if not np.allclose(ll_model, ll_scipy): - raise ValueError("Log-likelihood doesn't match scipy!") + raise ValueError("Log-likelihood doesn't match scipy!") @pytest.mark.parametrize("score_type", ["pseudo-r2-Choen", "pseudo-r2-McFadden"]) def test_pseudo_r2_range(self, score_type, poissonGLM_model_instantiation): @@ -100,7 +133,9 @@ def test_pseudo_r2_range(self, score_type, poissonGLM_model_instantiation): Compute the pseudo-r2 and check that is < 1. """ _, y, model, _, firing_rate = poissonGLM_model_instantiation - pseudo_r2 = model.observation_model.pseudo_r2(firing_rate, y, score_type=score_type) + pseudo_r2 = model.observation_model.pseudo_r2( + firing_rate, y, score_type=score_type + ) if (pseudo_r2 > 1) or (pseudo_r2 < 0): raise ValueError(f"pseudo-r2 of {pseudo_r2} outside the [0,1] range!") @@ -110,9 +145,13 @@ def test_pseudo_r2_mean(self, score_type, poissonGLM_model_instantiation): Check that the pseudo-r2 of the null model is 0. """ _, y, model, _, _ = poissonGLM_model_instantiation - pseudo_r2 = model.observation_model.pseudo_r2(y.mean(), y,score_type=score_type) + pseudo_r2 = model.observation_model.pseudo_r2( + y.mean(), y, score_type=score_type + ) if not np.allclose(pseudo_r2, 0): - raise ValueError(f"pseudo-r2 of {pseudo_r2} for the null model. Should be equal to 0!") + raise ValueError( + f"pseudo-r2 of {pseudo_r2} for the null model. Should be equal to 0!" + ) def test_emission_probability(selfself, poissonGLM_model_instantiation): """ @@ -124,4 +163,6 @@ def test_emission_probability(selfself, poissonGLM_model_instantiation): key_array = jax.random.PRNGKey(123) counts = model.observation_model.sample_generator(key_array, np.arange(1, 11)) if not jnp.all(counts == jax.random.poisson(key_array, np.arange(1, 11))): - raise ValueError("The emission probability should output the results of a call to jax.random.poisson.") + raise ValueError( + "The emission probability should output the results of a call to jax.random.poisson." + ) diff --git a/tests/test_proximal_operator.py b/tests/test_proximal_operator.py index 432db4c5..4addd1fa 100644 --- a/tests/test_proximal_operator.py +++ b/tests/test_proximal_operator.py @@ -9,38 +9,44 @@ def test_prox_group_lasso_returns_tuple(example_data_prox_operator): updated_params = prox_group_lasso(params, alpha, mask, scaling) assert isinstance(updated_params, tuple) + def test_prox_group_lasso_tuple_length(example_data_prox_operator): """Test whether the tuple returned by prox_group_lasso has a length of 2.""" params, alpha, mask, scaling = example_data_prox_operator updated_params = prox_group_lasso(params, alpha, mask, scaling) assert len(updated_params) == 2 + def test_prox_group_lasso_weights_shape(example_data_prox_operator): """Test whether the shape of the weights in prox_group_lasso is correct.""" params, alpha, mask, scaling = example_data_prox_operator updated_params = prox_group_lasso(params, alpha, mask, scaling) assert updated_params[0].shape == params[0].shape + def test_prox_group_lasso_intercepts_shape(example_data_prox_operator): """Test whether the shape of the intercepts in prox_group_lasso is correct.""" params, alpha, mask, scaling = example_data_prox_operator updated_params = prox_group_lasso(params, alpha, mask, scaling) assert updated_params[1].shape == params[1].shape + def test_vmap_norm2_masked_2_returns_array(example_data_prox_operator): """Test whether _vmap_norm2_masked_2 returns a NumPy array.""" params, _, mask, _ = example_data_prox_operator l2_norm = _vmap_norm2_masked_2(params[0], mask) assert isinstance(l2_norm, jnp.ndarray) + def test_vmap_norm2_masked_2_shape(example_data_prox_operator): """Test whether the shape of the result from _vmap_norm2_masked_2 is correct.""" params, _, mask, _ = example_data_prox_operator l2_norm = _vmap_norm2_masked_2(params[0], mask) assert l2_norm.shape == (params[0].shape[0], mask.shape[0]) + def test_vmap_norm2_masked_2_non_negative(example_data_prox_operator): """Test whether all elements of the result from _vmap_norm2_masked_2 are non-negative.""" params, _, mask, _ = example_data_prox_operator l2_norm = _vmap_norm2_masked_2(params[0], mask) - assert jnp.all(l2_norm >= 0) \ No newline at end of file + assert jnp.all(l2_norm >= 0) diff --git a/tests/test_regularizer.py b/tests/test_regularizer.py index c28785b8..c4c0a454 100644 --- a/tests/test_regularizer.py +++ b/tests/test_regularizer.py @@ -11,7 +11,10 @@ class TestUnRegularized: cls = nmo.regularizer.UnRegularized - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_init_solver_name(self, solver_name): """Test UnRegularized acceptable solvers.""" acceptable_solvers = [ @@ -21,16 +24,21 @@ def test_init_solver_name(self, solver_name): "ScipyMinimize", "NonlinearCG", "ScipyBoundedMinimize", - "LBFGSB" + "LBFGSB", ] raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): self.cls(solver_name) else: self.cls(solver_name) - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_set_solver_name_allowed(self, solver_name): """Test UnRegularized acceptable solvers.""" acceptable_solvers = [ @@ -40,12 +48,14 @@ def test_set_solver_name_allowed(self, solver_name): "ScipyMinimize", "NonlinearCG", "ScipyBoundedMinimize", - "LBFGSB" + "LBFGSB", ] regularizer = self.cls("GradientDescent") raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): regularizer.set_params(solver_name=solver_name) else: regularizer.set_params(solver_name=solver_name) @@ -57,7 +67,9 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): raise_exception = "tols" in list(solver_kwargs.keys()) if raise_exception: - with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + with pytest.raises( + NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg" + ): self.cls(solver_name, solver_kwargs=solver_kwargs) else: self.cls(solver_name, solver_kwargs=solver_kwargs) @@ -78,7 +90,7 @@ def test_run_solver(self, solver_name, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation runner = self.cls("GradientDescent").instantiate_solver(model._score) - runner((true_params[0]*0., true_params[1]), X, y) + runner((true_params[0] * 0.0, true_params[1]), X, y) def test_solver_output_match(self, poissonGLM_model_instantiation): """Test that different solvers converge to the same solution.""" @@ -86,19 +98,35 @@ def test_solver_output_match(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 - runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver(model._score) - runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver(model._score) - runner_scipy = self.cls("ScipyMinimize", {"method": "BFGS", "tol": 10**-12}).instantiate_solver(model._score) - weights_gd, intercepts_gd = runner_gd((true_params[0] * 0., true_params[1]), X, y)[0] - weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] - weights_scipy, intercepts_scipy = runner_scipy((true_params[0] * 0., true_params[1]), X, y)[0] - - match_weights = np.allclose(weights_gd, weights_bfgs) and \ - np.allclose(weights_gd, weights_scipy) - match_intercepts = np.allclose(intercepts_gd, intercepts_bfgs) and \ - np.allclose(intercepts_gd, intercepts_scipy) + runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver( + model._score + ) + runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver( + model._score + ) + runner_scipy = self.cls( + "ScipyMinimize", {"method": "BFGS", "tol": 10**-12} + ).instantiate_solver(model._score) + weights_gd, intercepts_gd = runner_gd( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + weights_bfgs, intercepts_bfgs = runner_bfgs( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + weights_scipy, intercepts_scipy = runner_scipy( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + + match_weights = np.allclose(weights_gd, weights_bfgs) and np.allclose( + weights_gd, weights_scipy + ) + match_intercepts = np.allclose(intercepts_gd, intercepts_bfgs) and np.allclose( + intercepts_gd, intercepts_scipy + ) if (not match_weights) or (not match_intercepts): - raise ValueError("Convex estimators should converge to the same numerical value.") + raise ValueError( + "Convex estimators should converge to the same numerical value." + ) def test_solver_match_sklearn(self, poissonGLM_model_instantiation): """Test that different solvers converge to the same solution.""" @@ -108,9 +136,11 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): model.data_type = jnp.float64 regularizer = self.cls("GradientDescent", {"tol": 10**-12}) runner_bfgs = regularizer.instantiate_solver(model._score) - weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] - model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=0.) - model_skl.fit(X[:,0], y[:, 0]) + weights_bfgs, intercepts_bfgs = runner_bfgs( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=0.0) + model_skl.fit(X[:, 0], y[:, 0]) match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) match_intercepts = np.allclose(model_skl.intercept_, intercepts_bfgs.flatten()) @@ -121,7 +151,10 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): class TestRidge: cls = nmo.regularizer.Ridge - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_init_solver_name(self, solver_name): """Test RidgeSolver acceptable solvers.""" acceptable_solvers = [ @@ -131,16 +164,21 @@ def test_init_solver_name(self, solver_name): "ScipyMinimize", "NonlinearCG", "ScipyBoundedMinimize", - "LBFGSB" + "LBFGSB", ] raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): self.cls(solver_name) else: self.cls(solver_name) - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_set_solver_name_allowed(self, solver_name): """Test RidgeSolver acceptable solvers.""" acceptable_solvers = [ @@ -150,12 +188,14 @@ def test_set_solver_name_allowed(self, solver_name): "ScipyMinimize", "NonlinearCG", "ScipyBoundedMinimize", - "LBFGSB" + "LBFGSB", ] regularizer = self.cls("GradientDescent") raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): regularizer.set_params(solver_name=solver_name) else: regularizer.set_params(solver_name=solver_name) @@ -167,7 +207,9 @@ def test_init_solver_kwargs(self, solver_name, solver_kwargs): raise_exception = "tols" in list(solver_kwargs.keys()) if raise_exception: - with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + with pytest.raises( + NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg" + ): self.cls(solver_name, solver_kwargs=solver_kwargs) else: self.cls(solver_name, solver_kwargs=solver_kwargs) @@ -188,7 +230,7 @@ def test_run_solver(self, solver_name, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation runner = self.cls("GradientDescent").instantiate_solver(model._score) - runner((true_params[0]*0., true_params[1]), X, y) + runner((true_params[0] * 0.0, true_params[1]), X, y) def test_solver_output_match(self, poissonGLM_model_instantiation): """Test that different solvers converge to the same solution.""" @@ -196,19 +238,35 @@ def test_solver_output_match(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 - runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver(model._score) - runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver(model._score) - runner_scipy = self.cls("ScipyMinimize", {"method": "BFGS", "tol": 10**-12}).instantiate_solver(model._score) - weights_gd, intercepts_gd = runner_gd((true_params[0] * 0., true_params[1]), X, y)[0] - weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] - weights_scipy, intercepts_scipy = runner_scipy((true_params[0] * 0., true_params[1]), X, y)[0] - - match_weights = np.allclose(weights_gd, weights_bfgs) and \ - np.allclose(weights_gd, weights_scipy) - match_intercepts = np.allclose(intercepts_gd, intercepts_bfgs) and \ - np.allclose(intercepts_gd, intercepts_scipy) + runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver( + model._score + ) + runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver( + model._score + ) + runner_scipy = self.cls( + "ScipyMinimize", {"method": "BFGS", "tol": 10**-12} + ).instantiate_solver(model._score) + weights_gd, intercepts_gd = runner_gd( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + weights_bfgs, intercepts_bfgs = runner_bfgs( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + weights_scipy, intercepts_scipy = runner_scipy( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + + match_weights = np.allclose(weights_gd, weights_bfgs) and np.allclose( + weights_gd, weights_scipy + ) + match_intercepts = np.allclose(intercepts_gd, intercepts_bfgs) and np.allclose( + intercepts_gd, intercepts_scipy + ) if (not match_weights) or (not match_intercepts): - raise ValueError("Convex estimators should converge to the same numerical value.") + raise ValueError( + "Convex estimators should converge to the same numerical value." + ) def test_solver_match_sklearn(self, poissonGLM_model_instantiation): """Test that different solvers converge to the same solution.""" @@ -218,9 +276,13 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): model.data_type = jnp.float64 regularizer = self.cls("GradientDescent", {"tol": 10**-12}) runner_bfgs = regularizer.instantiate_solver(model._score) - weights_bfgs, intercepts_bfgs = runner_bfgs((true_params[0] * 0., true_params[1]), X, y)[0] - model_skl = PoissonRegressor(fit_intercept=True, tol=10**-12, alpha=regularizer.regularizer_strength) - model_skl.fit(X[:,0], y[:, 0]) + weights_bfgs, intercepts_bfgs = runner_bfgs( + (true_params[0] * 0.0, true_params[1]), X, y + )[0] + model_skl = PoissonRegressor( + fit_intercept=True, tol=10**-12, alpha=regularizer.regularizer_strength + ) + model_skl.fit(X[:, 0], y[:, 0]) match_weights = np.allclose(model_skl.coef_, weights_bfgs.flatten()) match_intercepts = np.allclose(model_skl.intercept_, intercepts_bfgs.flatten()) @@ -231,29 +293,35 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): class TestLasso: cls = nmo.regularizer.Lasso - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_init_solver_name(self, solver_name): """Test Lasso acceptable solvers.""" - acceptable_solvers = [ - "ProximalGradient" - ] + acceptable_solvers = ["ProximalGradient"] raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): self.cls(solver_name) else: self.cls(solver_name) - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_set_solver_name_allowed(self, solver_name): """Test Lasso acceptable solvers.""" - acceptable_solvers = [ - "ProximalGradient" - ] + acceptable_solvers = ["ProximalGradient"] regularizer = self.cls("ProximalGradient") raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): regularizer.set_params(solver_name=solver_name) else: regularizer.set_params(solver_name=solver_name) @@ -263,7 +331,9 @@ def test_init_solver_kwargs(self, solver_kwargs): """Test LassoSolver acceptable kwargs.""" raise_exception = "tols" in list(solver_kwargs.keys()) if raise_exception: - with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + with pytest.raises( + NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg" + ): self.cls("ProximalGradient", solver_kwargs=solver_kwargs) else: self.cls("ProximalGradient", solver_kwargs=solver_kwargs) @@ -283,7 +353,7 @@ def test_run_solver(self, poissonGLM_model_instantiation): X, y, model, true_params, firing_rate = poissonGLM_model_instantiation runner = self.cls("ProximalGradient").instantiate_solver(model._score) - runner((true_params[0]*0., true_params[1]), X, y) + runner((true_params[0] * 0.0, true_params[1]), X, y) def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): """Test that different solvers converge to the same solution.""" @@ -293,21 +363,21 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): model.data_type = jnp.float64 regularizer = self.cls("ProximalGradient", {"tol": 10**-12}) runner = regularizer.instantiate_solver(model._score) - weights, intercepts = runner((true_params[0] * 0., true_params[1]), X, y)[0] + weights, intercepts = runner((true_params[0] * 0.0, true_params[1]), X, y)[0] # instantiate the glm with statsmodels - glm_sm = sm.GLM(endog=y[:, 0], - exog=sm.add_constant(X[:, 0]), - family=sm.families.Poisson()) + glm_sm = sm.GLM( + endog=y[:, 0], exog=sm.add_constant(X[:, 0]), family=sm.families.Poisson() + ) # regularize everything except intercept alpha_sm = np.ones(X.shape[2] + 1) * regularizer.regularizer_strength alpha_sm[0] = 0 # pure lasso = elastic net with L1 weight = 1 - res_sm = glm_sm.fit_regularized(method="elastic_net", - alpha=alpha_sm, - L1_wt=1., cnvrg_tol=10**-12) + res_sm = glm_sm.fit_regularized( + method="elastic_net", alpha=alpha_sm, L1_wt=1.0, cnvrg_tol=10**-12 + ) # compare params sm_params = res_sm.params glm_params = jnp.hstack((intercepts, weights.flatten())) @@ -319,12 +389,13 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): class TestGroupLasso: cls = nmo.regularizer.GroupLasso - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_init_solver_name(self, solver_name): """Test GroupLasso acceptable solvers.""" - acceptable_solvers = [ - "ProximalGradient" - ] + acceptable_solvers = ["ProximalGradient"] raise_exception = solver_name not in acceptable_solvers # create a valid mask @@ -334,17 +405,20 @@ def test_init_solver_name(self, solver_name): mask = jnp.asarray(mask) if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): self.cls(solver_name, mask) else: self.cls(solver_name, mask) - @pytest.mark.parametrize("solver_name", ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1]) + @pytest.mark.parametrize( + "solver_name", + ["GradientDescent", "BFGS", "ProximalGradient", "AGradientDescent", 1], + ) def test_set_solver_name_allowed(self, solver_name): """Test GroupLassoSolver acceptable solvers.""" - acceptable_solvers = [ - "ProximalGradient" - ] + acceptable_solvers = ["ProximalGradient"] # create a valid mask mask = np.zeros((2, 10)) mask[0, :5] = 1 @@ -353,7 +427,9 @@ def test_set_solver_name_allowed(self, solver_name): regularizer = self.cls("ProximalGradient", mask=mask) raise_exception = solver_name not in acceptable_solvers if raise_exception: - with pytest.raises(ValueError, match=f"Solver `{solver_name}` not allowed for "): + with pytest.raises( + ValueError, match=f"Solver `{solver_name}` not allowed for " + ): regularizer.set_params(solver_name=solver_name) else: regularizer.set_params(solver_name=solver_name) @@ -370,7 +446,9 @@ def test_init_solver_kwargs(self, solver_kwargs): mask = jnp.asarray(mask) if raise_exception: - with pytest.raises(NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg"): + with pytest.raises( + NameError, match="kwargs {'tols'} in solver_kwargs not a kwarg" + ): self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) else: self.cls("ProximalGradient", mask, solver_kwargs=solver_kwargs) @@ -404,15 +482,22 @@ def test_run_solver(self, poissonGLM_model_instantiation): mask = jnp.asarray(mask) runner = self.cls("ProximalGradient", mask).instantiate_solver(model._score) - runner((true_params[0]*0., true_params[1]), X, y) + runner((true_params[0] * 0.0, true_params[1]), X, y) @pytest.mark.parametrize("n_groups_assign", [0, 1, 2]) - def test_mask_validity_groups(self, - n_groups_assign, - group_sparse_poisson_glm_model_instantiation): + def test_mask_validity_groups( + self, n_groups_assign, group_sparse_poisson_glm_model_instantiation + ): """Test that mask assigns at most 1 group to each weight.""" raise_exception = n_groups_assign > 1 - X, y, model, true_params, firing_rate, _ = group_sparse_poisson_glm_model_instantiation + ( + X, + y, + model, + true_params, + firing_rate, + _, + ) = group_sparse_poisson_glm_model_instantiation # create a valid mask mask = np.zeros((2, X.shape[2])) @@ -428,14 +513,13 @@ def test_mask_validity_groups(self, mask = jnp.asarray(mask) if raise_exception: - with pytest.raises(ValueError, match="Incorrect group assignment. " - "Some of the features"): + with pytest.raises( + ValueError, match="Incorrect group assignment. " "Some of the features" + ): self.cls("ProximalGradient", mask).instantiate_solver(model._score) else: self.cls("ProximalGradient", mask).instantiate_solver(model._score) - - @pytest.mark.parametrize("set_entry", [0, 1, -1, 2, 2.5]) def test_mask_validity_entries(self, set_entry, poissonGLM_model_instantiation): """Test that mask is composed of 0s and 1s.""" @@ -473,7 +557,7 @@ def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): mask[0, :2] = 1 mask[1, 2:] = 1 else: - mask = np.zeros((2, X.shape[2]) + (1, ) * (n_dim-2)) + mask = np.zeros((2, X.shape[2]) + (1,) * (n_dim - 2)) mask[0, :2] = 1 mask[1, 2:] = 1 @@ -494,9 +578,9 @@ def test_mask_n_groups(self, n_groups, poissonGLM_model_instantiation): # create a mask mask = np.zeros((n_groups, X.shape[2])) if n_groups > 0: - for i in range(n_groups-1): - mask[i, i: i+1] = 1 - mask[-1, n_groups-1:] = 1 + for i in range(n_groups - 1): + mask[i, i : i + 1] = 1 + mask[-1, n_groups - 1 :] = 1 mask = jnp.asarray(mask, dtype=jnp.float32) @@ -506,9 +590,18 @@ def test_mask_n_groups(self, n_groups, poissonGLM_model_instantiation): else: self.cls("ProximalGradient", mask).instantiate_solver(model._score) - def test_group_sparsity_enforcement(self, group_sparse_poisson_glm_model_instantiation): + def test_group_sparsity_enforcement( + self, group_sparse_poisson_glm_model_instantiation + ): """Test that group lasso works on a simple dataset.""" - X, y, model, true_params, firing_rate, _ = group_sparse_poisson_glm_model_instantiation + ( + X, + y, + model, + true_params, + firing_rate, + _, + ) = group_sparse_poisson_glm_model_instantiation zeros_true = true_params[0].flatten() == 0 mask = np.zeros((2, X.shape[2])) mask[0, zeros_true] = 1 @@ -516,23 +609,29 @@ def test_group_sparsity_enforcement(self, group_sparse_poisson_glm_model_instant mask = jnp.asarray(mask, dtype=jnp.float32) runner = self.cls("ProximalGradient", mask).instantiate_solver(model._score) - params, _ = runner((true_params[0]*0., true_params[1]), X, y) + params, _ = runner((true_params[0] * 0.0, true_params[1]), X, y) zeros_est = params[0] == 0 if not np.all(zeros_est == zeros_true): raise ValueError("GroupLasso failed to zero-out the parameter group!") - ########### # Test mask from set_params ########### @pytest.mark.parametrize("n_groups_assign", [0, 1, 2]) - def test_mask_validity_groups_set_params(self, - n_groups_assign, - group_sparse_poisson_glm_model_instantiation): + def test_mask_validity_groups_set_params( + self, n_groups_assign, group_sparse_poisson_glm_model_instantiation + ): """Test that mask assigns at most 1 group to each weight.""" raise_exception = n_groups_assign > 1 - X, y, model, true_params, firing_rate, _ = group_sparse_poisson_glm_model_instantiation + ( + X, + y, + model, + true_params, + firing_rate, + _, + ) = group_sparse_poisson_glm_model_instantiation # create a valid mask mask = np.zeros((2, X.shape[2])) @@ -549,14 +648,17 @@ def test_mask_validity_groups_set_params(self, mask = jnp.asarray(mask) if raise_exception: - with pytest.raises(ValueError, match="Incorrect group assignment. " - "Some of the features"): + with pytest.raises( + ValueError, match="Incorrect group assignment. " "Some of the features" + ): regularizer.set_params(mask=mask) else: regularizer.set_params(mask=mask) @pytest.mark.parametrize("set_entry", [0, 1, -1, 2, 2.5]) - def test_mask_validity_entries_set_params(self, set_entry, poissonGLM_model_instantiation): + def test_mask_validity_entries_set_params( + self, set_entry, poissonGLM_model_instantiation + ): """Test that mask is composed of 0s and 1s.""" raise_exception = set_entry not in {0, 1} X, y, model, true_params, firing_rate = poissonGLM_model_instantiation @@ -625,8 +727,8 @@ def test_mask_n_groups_set_params(self, n_groups, poissonGLM_model_instantiation mask = np.zeros((n_groups, X.shape[2])) if n_groups > 0: for i in range(n_groups - 1): - mask[i, i: i + 1] = 1 - mask[-1, n_groups - 1:] = 1 + mask[i, i : i + 1] = 1 + mask[-1, n_groups - 1 :] = 1 mask = jnp.asarray(mask, dtype=jnp.float32) @@ -635,5 +737,3 @@ def test_mask_n_groups_set_params(self, n_groups, poissonGLM_model_instantiation regularizer.set_params(mask=mask) else: regularizer.set_params(mask=mask) - - From 85de28f0ce4eb0058bb58dd71a2042c9ea5c69ec Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 11:10:13 -0500 Subject: [PATCH 230/250] fixed naming --- src/nemos/glm.py | 13 +++++-------- src/nemos/observation_models.py | 10 +++++----- tests/test_glm.py | 2 +- tests/test_observation_models.py | 4 ++-- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index bab31121..367a7877 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -6,7 +6,7 @@ from numpy.typing import NDArray from . import observation_models as obs -from . import regularizer as slv +from . import regularizer as reg from . import utils from .base_class import BaseRegressor from .exceptions import NotFittedError @@ -51,7 +51,7 @@ class GLM(BaseRegressor): def __init__( self, observation_model: obs.Observations = obs.PoissonObservations(), - regularizer: slv.Regularizer = slv.Ridge("GradientDescent"), + regularizer: reg.Regularizer = reg.Ridge("GradientDescent"), ): super().__init__() @@ -69,7 +69,7 @@ def regularizer(self): return self._regularizer @regularizer.setter - def regularizer(self, regularizer: slv.Regularizer): + def regularizer(self, regularizer: reg.Regularizer): """Setter for the regularizer attribute.""" if not hasattr(regularizer, "instantiate_solver"): raise AttributeError( @@ -215,8 +215,7 @@ def score( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], - score_type: Literal["log-likelihood", "pseudo-r2"] = "pseudo-r2-McFadden", - pseudo_r2_type: Literal["McFadden", "Cox"] = "McFadden", + score_type: Literal["log-likelihood", "pseudo-r2-McFadden", "pseudo-r2-Cohen"] = "pseudo-r2-McFadden", ) -> jnp.ndarray: r"""Evaluate the goodness-of-fit of the model to the observed neural data. @@ -236,8 +235,6 @@ def score( during the fitting of this GLM instance. Shape (n_time_bins, n_neurons). score_type : Type of scoring: either log-likelihood or pseudo-r2. - pseudo_r2_type : - Type of pseudo-r2 to be reported. Returns ------- @@ -461,7 +458,7 @@ class GLMRecurrent(GLM): def __init__( self, observation_model: obs.Observations = obs.PoissonObservations(), - regularizer: slv.Regularizer = slv.Ridge(), + regularizer: reg.Regularizer = reg.Ridge(), ): super().__init__(observation_model=observation_model, regularizer=regularizer) diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index e0359ffe..3085f883 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -206,13 +206,13 @@ def pseudo_r2( predicted_rate: jnp.ndarray, y: jnp.ndarray, score_type: Literal[ - "pseudo-r2-McFadden", "pseudo-r2-Choen" + "pseudo-r2-McFadden", "pseudo-r2-Cohen" ] = "pseudo-r2-McFadden", ) -> jnp.ndarray: r"""Pseudo-$R^2$ calculation for a GLM. Compute the pseudo-$R^2$ metric for the GLM, as defined by McFadden et al.[$^1$](#references) - or by Choen et al.[$^2$](#references). + or by Cohen et al.[$^2$](#references). This metric evaluates the goodness-of-fit of the model relative to a null (baseline) model that assumes a constant mean for the observations. While the pseudo-$R^2$ is bounded between 0 and 1 for the training set, @@ -239,10 +239,10 @@ def pseudo_r2( $$ R^2_{\text{mcf}} = 1 - \frac{\log(L_{M})}{\log(L_0)} $$ - - The Choen pseudo-$R^2$ is given by: + - The Cohen pseudo-$R^2$ is given by: $$ \begin{aligned} - R^2_{\text{Choen}} &= \frac{D_0 - D_M}{D_0} \\\ + R^2_{\text{Cohen}} &= \frac{D_0 - D_M}{D_0} \\\ &= 1 - \frac{\log(L_s) - \log(L_M)}{\log(L_s)-\log(L_0)}, \end{aligned} $$ @@ -263,7 +263,7 @@ def pseudo_r2( """ if score_type == "pseudo-r2-McFadden": pseudo_r2 = self._pseudo_r2_mcfadden(predicted_rate, y) - elif score_type == "pseudo-r2-Choen": + elif score_type == "pseudo-r2-Cohen": pseudo_r2 = self._pseudo_r2_choen(predicted_rate, y) else: raise NotImplementedError(f"Score {score_type} not implemented!") diff --git a/tests/test_glm.py b/tests/test_glm.py index cec40c3b..b71824e7 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -718,7 +718,7 @@ def test_score_time_points_y( "score_type, expectation", [ ("pseudo-r2-McFadden", does_not_raise()), - ("pseudo-r2-Choen", does_not_raise()), + ("pseudo-r2-Cohen", does_not_raise()), ("log-likelihood", does_not_raise()), ( "not-implemented", diff --git a/tests/test_observation_models.py b/tests/test_observation_models.py index 8e9a9ff7..803b49fb 100644 --- a/tests/test_observation_models.py +++ b/tests/test_observation_models.py @@ -127,7 +127,7 @@ def test_loglikelihood_against_scipy(self, poissonGLM_model_instantiation): if not np.allclose(ll_model, ll_scipy): raise ValueError("Log-likelihood doesn't match scipy!") - @pytest.mark.parametrize("score_type", ["pseudo-r2-Choen", "pseudo-r2-McFadden"]) + @pytest.mark.parametrize("score_type", ["pseudo-r2-Cohen", "pseudo-r2-McFadden"]) def test_pseudo_r2_range(self, score_type, poissonGLM_model_instantiation): """ Compute the pseudo-r2 and check that is < 1. @@ -139,7 +139,7 @@ def test_pseudo_r2_range(self, score_type, poissonGLM_model_instantiation): if (pseudo_r2 > 1) or (pseudo_r2 < 0): raise ValueError(f"pseudo-r2 of {pseudo_r2} outside the [0,1] range!") - @pytest.mark.parametrize("score_type", ["pseudo-r2-Choen", "pseudo-r2-McFadden"]) + @pytest.mark.parametrize("score_type", ["pseudo-r2-Cohen", "pseudo-r2-McFadden"]) def test_pseudo_r2_mean(self, score_type, poissonGLM_model_instantiation): """ Check that the pseudo-r2 of the null model is 0. From 41f3771c2c83a40771f6f40a440c5722854c7fa3 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 11:11:04 -0500 Subject: [PATCH 231/250] linted glm --- src/nemos/glm.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 367a7877..80866f7b 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -215,7 +215,9 @@ def score( self, X: Union[NDArray, jnp.ndarray], y: Union[NDArray, jnp.ndarray], - score_type: Literal["log-likelihood", "pseudo-r2-McFadden", "pseudo-r2-Cohen"] = "pseudo-r2-McFadden", + score_type: Literal[ + "log-likelihood", "pseudo-r2-McFadden", "pseudo-r2-Cohen" + ] = "pseudo-r2-McFadden", ) -> jnp.ndarray: r"""Evaluate the goodness-of-fit of the model to the observed neural data. From 29aab9773faca3471f21e1bbdfd6902c3496d61f Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 30 Nov 2023 11:14:05 -0500 Subject: [PATCH 232/250] removed sklearn dep --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5885c24b..778b0916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,6 @@ dependencies = [ 'jaxopt>=0.6', # Optimization library built on JAX 'matplotlib>=3.7', # Plotting library 'numpy>1.20', # Numerical computing library - 'scikit-learn>=1.2', # Machine learning library 'scipy>=1.10', # Scientific computing library 'typing_extensions>=4.6' # Typing extensions for Python ] From 979b3b4334367a9d528f4b0fd23d3e3c47cf69a6 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Dec 2023 12:42:39 -0500 Subject: [PATCH 233/250] removed unused method --- src/nemos/utils.py | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/nemos/utils.py b/src/nemos/utils.py index 6d52cb9b..bd8a3d72 100644 --- a/src/nemos/utils.py +++ b/src/nemos/utils.py @@ -459,32 +459,3 @@ def assert_scalar_func(func: Callable, inputs: List[jnp.ndarray], func_name: str f"The `{func_name}` should return a scalar! " f"Array of shape {array_out.shape} returned instead!" ) - - -def multi_array_device_put( - *args: jnp.ndarray, device: Literal["cpu", "tpu", "gpu"] -) -> Union[Any, jnp.ndarray]: - """Send arrays to device. - - This function sends the arrays to the target device, if the arrays are - not already there. - - Parameters - ---------- - *args: - NDArray - device: - A target device between "cpu", "tpu", "gpu". - - Returns - ------- - : - The arrays on the desired device. - """ - device_obj = jax.devices(device)[0] - return tuple( - jax.device_put(arg, device_obj) - if arg.device_buffer.device() != device_obj - else arg - for arg in args - ) From 899ca987c0b831572f9fd1a3078334ab01113e06 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Dec 2023 13:17:10 -0500 Subject: [PATCH 234/250] refractored names --- docs/developers_notes/04-regularizer.md | 10 +++--- docs/examples/plot_glm_demo.py | 4 +-- src/nemos/glm.py | 19 ++++++----- src/nemos/regularizer.py | 8 ++--- tests/test_base_class.py | 3 +- tests/test_regularizer.py | 44 ++++++++++++------------- 6 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/developers_notes/04-regularizer.md b/docs/developers_notes/04-regularizer.md index cd077849..3026c162 100644 --- a/docs/developers_notes/04-regularizer.md +++ b/docs/developers_notes/04-regularizer.md @@ -40,7 +40,7 @@ Additionally, the class provides auxiliary methods for checking that the solver ### Auxiliary Methods -- **`_check_solver`**: This method ensures that the provided solver name is in the list of allowed optimizers for the specific `Regularizer` object. This is crucial for maintaining consistency and correctness in the solver's operation. +- **`_check_solver`**: This method ensures that the provided solver name is in the list of allowed solvers for the specific `Regularizer` object. This is crucial for maintaining consistency and correctness in the solver's operation. - **`_check_solver_kwargs`**: This method checks if the provided keyword arguments are valid for the specified solver. This helps in catching and preventing potential errors in solver configuration. @@ -50,7 +50,7 @@ The `UnRegularized` class extends the base `Regularizer` class and is designed s ### Attributes -- **`allowed_solvers`**: A list of string identifiers for the optimization algorithms that can be used with this solver class. The optimization methods listed here are specifically suitable for unregularized optimization problems. +- **`allowed_solvers`**: A list of string identifiers for the optimization solvers that can be used with this regularizer class. The optimization methods listed here are specifically suitable for unregularized optimization problems. ### Methods @@ -72,7 +72,7 @@ The `Ridge` class extends the `Regularizer` class to handle optimization problem ### Attributes -- **`allowed_solvers`**: A list containing string identifiers of optimization algorithms compatible with Ridge regularization. +- **`allowed_solvers`**: A list containing string identifiers of optimization solvers compatible with Ridge regularization. - **`regularizer_strength`**: A floating-point value determining the strength of the Ridge regularization. Higher values correspond to stronger regularization which tends to drive the model parameters towards zero. @@ -97,7 +97,7 @@ optim_results = runner(init_params, exog_vars, endog_vars) `ProxGradientRegularizer` class extends the `Regularizer` class to utilize the Proximal Gradient method for optimization. It leverages the `jaxopt` library's Proximal Gradient optimizer, introducing the functionality of a proximal operator. ### Attributes: -- **`allowed_solvers`**: A list containing string identifiers of optimization algorithms compatible with this solver, specifically the "ProximalGradient". +- **`allowed_solvers`**: A list containing string identifiers of optimization solvers compatible with this solver, specifically the "ProximalGradient". ### Methods: - **`__init__`**: The constructor method for the `ProxGradientRegularizer` class. It accepts the name of the solver algorithm (`solver_name`), an optional dictionary of additional keyword arguments (`solver_kwargs`) for the solver, the regularization strength (`regularizer_strength`), and an optional mask array (`mask`). @@ -149,7 +149,7 @@ When developing a functional (i.e., concrete) `Regularizer` class: - **Must** inherit from `Regularizer` or one of its derivatives. - **Must** implement the `instantiate_solver` method to tailor the solver instantiation based on the provided loss function. - For any Proximal Gradient method, **must** include a `get_prox_operator` method to define the proximal operator. -- **Must** possess an `allowed_solvers` attribute to list the optimizer names that are permissible to be used with this solver. +- **Must** possess an `allowed_solvers` attribute to list the solver names that are permissible to be used with this regularizer. - **May** embed additional attributes and methods such as `mask` and `_check_mask` if required by the specific Solver subclass for handling special optimization scenarios. - **May** include a `regularizer_strength` attribute to control the strength of the regularization in scenarios where regularization is applicable. - **May** rely on a custom solver implementation for specific optimization problems, but the implementation **must** adhere to the `jaxopt` API. diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 638451fa..d168c395 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -127,11 +127,11 @@ # %% # !!! warning -# Each `Regularizer` has an associated attribute `Regularizer.allowed_optimizers` +# Each `Regularizer` has an associated attribute `Regularizer.allowed_solvers` # which lists the optimizers that are suited for each optimization problem. # For example, a `Ridge` is differentiable and can be fit with `GradientDescent` # , `BFGS`, etc., while a `Lasso` should use the `ProximalGradient` method instead. -# If the provided `solver_name` is not listed in the `allowed_optimizers` this will raise an +# If the provided `solver_name` is not listed in the `allowed_solvers` this will raise an # exception. # %% diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 80866f7b..bfc579d5 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -77,7 +77,7 @@ def regularizer(self, regularizer: reg.Regularizer): ) # test solver instantiation on the GLM loss try: - regularizer.instantiate_solver(self._score) + regularizer.instantiate_solver(self._predict_and_compute_loss) except Exception: raise TypeError( "The provided `solver` cannot be instantiated on " @@ -181,13 +181,13 @@ def predict(self, X: Union[NDArray, jnp.ndarray]) -> jnp.ndarray: self._check_input_and_params_consistency((Ws, bs), X=X) return self._predict((Ws, bs), X) - def _score( + def _predict_and_compute_loss( self, params: Tuple[jnp.ndarray, jnp.ndarray], X: jnp.ndarray, y: jnp.ndarray, ) -> jnp.ndarray: - r"""Score the predicted rates against target neural activity. + r"""Predict the rate and compute the negative log-likelihood against neural activity. This method computes the negative log-likelihood up to a constant term. Unlike `score`, it does not conduct parameter checks prior to evaluation. Passed directly to the solver, @@ -222,10 +222,11 @@ def score( r"""Evaluate the goodness-of-fit of the model to the observed neural data. This method computes the goodness-of-fit score, which can either be the mean - log-likelihood or the pseudo-R^2. The scoring process includes validation of - input compatibility with the model's parameters, ensuring that the model - has been previously fitted and the input data are appropriate for scoring. A - higher score indicates a better fit of the model to the observed data. + log-likelihood or of two versions of the pseudo-R^2. + The scoring process includes validation of input compatibility with the model's + parameters, ensuring that the model has been previously fitted and the input data + are appropriate for scoring. A higher score indicates a better fit of the model + to the observed data. Parameters @@ -300,7 +301,7 @@ def score( if score_type == "log-likelihood": norm_constant = jax.scipy.special.gammaln(y + 1).mean() - score = -self._score((Ws, bs), X, y) - norm_constant + score = -self._predict_and_compute_loss((Ws, bs), X, y) - norm_constant elif score_type.startswith("pseudo-r2"): score = self._observation_model.pseudo_r2( self._predict((Ws, bs), X), y, score_type=score_type @@ -352,7 +353,7 @@ def fit( X, y, init_params = self._preprocess_fit(X, y, init_params) # Run optimization - runner = self.regularizer.instantiate_solver(self._score) + runner = self.regularizer.instantiate_solver(self._predict_and_compute_loss) params, state = runner(init_params, X, y) # estimate the GLM scale diff --git a/src/nemos/regularizer.py b/src/nemos/regularizer.py index 678e9445..c39203a6 100644 --- a/src/nemos/regularizer.py +++ b/src/nemos/regularizer.py @@ -57,7 +57,7 @@ class Regularizer(Base, abc.ABC): Attributes ---------- allowed_solvers : - List of optimizer names that are allowed for use with this solver. + List of solver names that are allowed for use with this regularizer. solver_name : Name of the solver being used. solver_kwargs : @@ -194,7 +194,7 @@ class are defined in the `allowed_solvers` attribute. Attributes ---------- allowed_solvers : list of str - List of optimizer names that are allowed for this solver class. + List of solver names that are allowed for this regularizer class. See Also -------- @@ -227,7 +227,7 @@ class Ridge(Regularizer): Attributes ---------- allowed_solvers : List[..., str] - A list of optimizer names that are allowed to be used with this solver. + A list of solver names that are allowed to be used with this regularizer. """ allowed_solvers = [ @@ -306,7 +306,7 @@ class ProxGradientRegularizer(Regularizer, abc.ABC): Attributes ---------- allowed_solvers : List[...,str] - A list of optimizer names that are allowed to be used with this solver. + A list of solver names that are allowed to be used with this regularizer. """ allowed_solvers = ["ProximalGradient"] diff --git a/tests/test_base_class.py b/tests/test_base_class.py index 7cd3114a..56d1b7dc 100644 --- a/tests/test_base_class.py +++ b/tests/test_base_class.py @@ -5,9 +5,8 @@ import pytest from numpy.typing import NDArray -import nemos as nmo from nemos.base_class import Base, BaseRegressor -from nemos.utils import check_invalid_entry, multi_array_device_put +from nemos.utils import check_invalid_entry @pytest.fixture diff --git a/tests/test_regularizer.py b/tests/test_regularizer.py index c4c0a454..b8a6fe1c 100644 --- a/tests/test_regularizer.py +++ b/tests/test_regularizer.py @@ -89,7 +89,7 @@ def test_run_solver(self, solver_name, poissonGLM_model_instantiation): """Test that the solver runs.""" X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - runner = self.cls("GradientDescent").instantiate_solver(model._score) + runner = self.cls("GradientDescent").instantiate_solver(model._predict_and_compute_loss) runner((true_params[0] * 0.0, true_params[1]), X, y) def test_solver_output_match(self, poissonGLM_model_instantiation): @@ -99,14 +99,14 @@ def test_solver_output_match(self, poissonGLM_model_instantiation): # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver( - model._score + model._predict_and_compute_loss ) runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver( - model._score + model._predict_and_compute_loss ) runner_scipy = self.cls( "ScipyMinimize", {"method": "BFGS", "tol": 10**-12} - ).instantiate_solver(model._score) + ).instantiate_solver(model._predict_and_compute_loss) weights_gd, intercepts_gd = runner_gd( (true_params[0] * 0.0, true_params[1]), X, y )[0] @@ -135,7 +135,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 regularizer = self.cls("GradientDescent", {"tol": 10**-12}) - runner_bfgs = regularizer.instantiate_solver(model._score) + runner_bfgs = regularizer.instantiate_solver(model._predict_and_compute_loss) weights_bfgs, intercepts_bfgs = runner_bfgs( (true_params[0] * 0.0, true_params[1]), X, y )[0] @@ -229,7 +229,7 @@ def test_run_solver(self, solver_name, poissonGLM_model_instantiation): """Test that the solver runs.""" X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - runner = self.cls("GradientDescent").instantiate_solver(model._score) + runner = self.cls("GradientDescent").instantiate_solver(model._predict_and_compute_loss) runner((true_params[0] * 0.0, true_params[1]), X, y) def test_solver_output_match(self, poissonGLM_model_instantiation): @@ -239,14 +239,14 @@ def test_solver_output_match(self, poissonGLM_model_instantiation): # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 runner_gd = self.cls("GradientDescent", {"tol": 10**-12}).instantiate_solver( - model._score + model._predict_and_compute_loss ) runner_bfgs = self.cls("BFGS", {"tol": 10**-12}).instantiate_solver( - model._score + model._predict_and_compute_loss ) runner_scipy = self.cls( "ScipyMinimize", {"method": "BFGS", "tol": 10**-12} - ).instantiate_solver(model._score) + ).instantiate_solver(model._predict_and_compute_loss) weights_gd, intercepts_gd = runner_gd( (true_params[0] * 0.0, true_params[1]), X, y )[0] @@ -275,7 +275,7 @@ def test_solver_match_sklearn(self, poissonGLM_model_instantiation): # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 regularizer = self.cls("GradientDescent", {"tol": 10**-12}) - runner_bfgs = regularizer.instantiate_solver(model._score) + runner_bfgs = regularizer.instantiate_solver(model._predict_and_compute_loss) weights_bfgs, intercepts_bfgs = runner_bfgs( (true_params[0] * 0.0, true_params[1]), X, y )[0] @@ -352,7 +352,7 @@ def test_run_solver(self, poissonGLM_model_instantiation): """Test that the solver runs.""" X, y, model, true_params, firing_rate = poissonGLM_model_instantiation - runner = self.cls("ProximalGradient").instantiate_solver(model._score) + runner = self.cls("ProximalGradient").instantiate_solver(model._predict_and_compute_loss) runner((true_params[0] * 0.0, true_params[1]), X, y) def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): @@ -362,7 +362,7 @@ def test_solver_match_statsmodels(self, poissonGLM_model_instantiation): # set precision to float64 for accurate matching of the results model.data_type = jnp.float64 regularizer = self.cls("ProximalGradient", {"tol": 10**-12}) - runner = regularizer.instantiate_solver(model._score) + runner = regularizer.instantiate_solver(model._predict_and_compute_loss) weights, intercepts = runner((true_params[0] * 0.0, true_params[1]), X, y)[0] # instantiate the glm with statsmodels @@ -481,7 +481,7 @@ def test_run_solver(self, poissonGLM_model_instantiation): mask[1, 2:] = 1 mask = jnp.asarray(mask) - runner = self.cls("ProximalGradient", mask).instantiate_solver(model._score) + runner = self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) runner((true_params[0] * 0.0, true_params[1]), X, y) @pytest.mark.parametrize("n_groups_assign", [0, 1, 2]) @@ -516,9 +516,9 @@ def test_mask_validity_groups( with pytest.raises( ValueError, match="Incorrect group assignment. " "Some of the features" ): - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) else: - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) @pytest.mark.parametrize("set_entry", [0, 1, -1, 2, 2.5]) def test_mask_validity_entries(self, set_entry, poissonGLM_model_instantiation): @@ -536,9 +536,9 @@ def test_mask_validity_entries(self, set_entry, poissonGLM_model_instantiation): if raise_exception: with pytest.raises(ValueError, match="Mask elements be 0s and 1s"): - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) else: - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) @pytest.mark.parametrize("n_dim", [0, 1, 2, 3]) def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): @@ -565,9 +565,9 @@ def test_mask_dimension(self, n_dim, poissonGLM_model_instantiation): if raise_exception: with pytest.raises(ValueError, match="`mask` must be 2-dimensional"): - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) else: - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) @pytest.mark.parametrize("n_groups", [0, 1, 2]) def test_mask_n_groups(self, n_groups, poissonGLM_model_instantiation): @@ -586,9 +586,9 @@ def test_mask_n_groups(self, n_groups, poissonGLM_model_instantiation): if raise_exception: with pytest.raises(ValueError, match=r"Empty mask provided! Mask has "): - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) else: - self.cls("ProximalGradient", mask).instantiate_solver(model._score) + self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) def test_group_sparsity_enforcement( self, group_sparse_poisson_glm_model_instantiation @@ -608,7 +608,7 @@ def test_group_sparsity_enforcement( mask[1, ~zeros_true] = 1 mask = jnp.asarray(mask, dtype=jnp.float32) - runner = self.cls("ProximalGradient", mask).instantiate_solver(model._score) + runner = self.cls("ProximalGradient", mask).instantiate_solver(model._predict_and_compute_loss) params, _ = runner((true_params[0] * 0.0, true_params[1]), X, y) zeros_est = params[0] == 0 From d5c220e1688bc1d6b08c91b68a932756b5e15a02 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Mon, 11 Dec 2023 15:28:04 -0500 Subject: [PATCH 235/250] undoes some unhelpful python black ormatting --- tests/test_glm.py | 320 ++++++++-------------------------------------- 1 file changed, 50 insertions(+), 270 deletions(-) diff --git a/tests/test_glm.py b/tests/test_glm.py index b71824e7..011a2b56 100644 --- a/tests/test_glm.py +++ b/tests/test_glm.py @@ -22,18 +22,8 @@ class TestGLM: "regularizer, expectation", [ (nmo.regularizer.Ridge("BFGS"), does_not_raise()), - ( - None, - pytest.raises( - AttributeError, match="The provided `solver` doesn't implement " - ), - ), - ( - nmo.regularizer.Ridge, - pytest.raises( - TypeError, match="The provided `solver` cannot be instantiated" - ), - ), + (None, pytest.raises(AttributeError, match="The provided `solver` doesn't implement ")), + (nmo.regularizer.Ridge, pytest.raises(TypeError, match="The provided `solver` cannot be instantiated")), ], ) def test_solver_type(self, regularizer, expectation, glm_class): @@ -47,20 +37,8 @@ def test_solver_type(self, regularizer, expectation, glm_class): "observation, expectation", [ (nmo.observation_models.PoissonObservations(), does_not_raise()), - ( - nmo.regularizer.Regularizer, - pytest.raises( - AttributeError, - match="The provided object does not have the required", - ), - ), - ( - 1, - pytest.raises( - AttributeError, - match="The provided object does not have the required", - ), - ), + (nmo.regularizer.Regularizer, pytest.raises(AttributeError, match="The provided object does not have the required")), + (1, pytest.raises(AttributeError, match="The provided object does not have the required")), ], ) def test_init_observation_type( @@ -79,25 +57,10 @@ def test_init_observation_type( @pytest.mark.parametrize( "n_params, expectation", [ - ( - 0, - pytest.raises( - ValueError, match="Params needs to be array-like of length two." - ), - ), - ( - 1, - pytest.raises( - ValueError, match="Params needs to be array-like of length two." - ), - ), + (0, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), + (1, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), (2, does_not_raise()), - ( - 3, - pytest.raises( - ValueError, match="Params needs to be array-like of length two." - ), - ), + (3, pytest.raises(ValueError, match="Params needs to be array-like of length two.")), ], ) def test_fit_param_length( @@ -149,28 +112,10 @@ def test_fit_param_values( @pytest.mark.parametrize( "dim_weights, expectation", [ - ( - 0, - pytest.raises( - ValueError, - match=r"params\[0\] must be of shape \(n_neurons, n_features\)", - ), - ), - ( - 1, - pytest.raises( - ValueError, - match=r"params\[0\] must be of shape \(n_neurons, n_features\)", - ), - ), + (0, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), + (1, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), (2, does_not_raise()), - ( - 3, - pytest.raises( - ValueError, - match=r"params\[0\] must be of shape \(n_neurons, n_features\)", - ), - ), + (3, pytest.raises(ValueError, match=r"params\[0\] must be of shape \(n_neurons, n_features\)")), ], ) def test_fit_weights_dimensionality( @@ -220,23 +165,11 @@ def test_fit_intercepts_dimensionality( [ ([jnp.zeros((1, 5)), jnp.zeros((1,))], does_not_raise()), (iter([jnp.zeros((1, 5)), jnp.zeros((1,))]), does_not_raise()), - ( - dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), - pytest.raises(TypeError, match="Initial parameters must be array-like"), - ), - ( - 0, - pytest.raises(TypeError, match="Initial parameters must be array-like"), - ), + (dict(p1=jnp.zeros((1, 5)), p2=jnp.zeros((1,))), pytest.raises(TypeError, match="Initial parameters must be array-like")), + (0, pytest.raises(TypeError, match="Initial parameters must be array-like")), ({0, 1}, pytest.raises(ValueError, match=r"params\[0\] must be of shape")), - ( - [jnp.zeros((1, 5)), ""], - pytest.raises(TypeError, match="Initial parameters must be array-like"), - ), - ( - ["", jnp.zeros((1,))], - pytest.raises(TypeError, match="Initial parameters must be array-like"), - ), + ([jnp.zeros((1, 5)), ""], pytest.raises(TypeError, match="Initial parameters must be array-like")), + (["", jnp.zeros((1,))], pytest.raises(TypeError, match="Initial parameters must be array-like")), ], ) def test_fit_init_params_type( @@ -253,19 +186,9 @@ def test_fit_init_params_type( @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="Model parameters have inconsistent shapes" - ), - ), + (-1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="Model parameters have inconsistent shapes" - ), - ), + (1, pytest.raises(ValueError, match="Model parameters have inconsistent shapes")), ], ) def test_fit_n_neuron_match_baseline_rate( @@ -283,19 +206,9 @@ def test_fit_n_neuron_match_baseline_rate( @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_fit_n_neuron_match_x( @@ -313,19 +226,9 @@ def test_fit_n_neuron_match_x( @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_fit_n_neuron_match_y( @@ -431,15 +334,9 @@ def test_fit_n_feature_consistency_x( @pytest.mark.parametrize( "delta_tp, expectation", [ - ( - -1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), (0, does_not_raise()), - ( - 1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")), ], ) def test_fit_time_points_x( @@ -456,15 +353,9 @@ def test_fit_time_points_x( @pytest.mark.parametrize( "delta_tp, expectation", [ - ( - -1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), (0, does_not_raise()), - ( - 1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")), ], ) def test_fit_time_points_y( @@ -494,19 +385,9 @@ def test_fit_mask_grouplasso(self, group_sparse_poisson_glm_model_instantiation) @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_score_n_neuron_match_x( @@ -525,19 +406,9 @@ def test_score_n_neuron_match_x( @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_score_n_neuron_match_y( @@ -580,19 +451,9 @@ def test_score_x_dimensionality( @pytest.mark.parametrize( "delta_dim, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="y must be two-dimensional, with shape" - ), - ), + (-1, pytest.raises(ValueError, match="y must be two-dimensional, with shape")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="y must be two-dimensional, with shape" - ), - ), + (1, pytest.raises(ValueError, match="y must be two-dimensional, with shape")), ], ) def test_score_y_dimensionality( @@ -641,10 +502,7 @@ def test_score_n_feature_consistency_x( "is_fit, expectation", [ (True, does_not_raise()), - ( - False, - pytest.raises(ValueError, match="This GLM instance is not fitted yet"), - ), + (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")), ], ) def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): @@ -661,15 +519,9 @@ def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiatio @pytest.mark.parametrize( "delta_tp, expectation", [ - ( - -1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), (0, does_not_raise()), - ( - 1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")), ], ) def test_score_time_points_x( @@ -689,15 +541,9 @@ def test_score_time_points_x( @pytest.mark.parametrize( "delta_tp, expectation", [ - ( - -1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (-1, pytest.raises(ValueError, match="The number of time-points in X and y")), (0, does_not_raise()), - ( - 1, - pytest.raises(ValueError, match="The number of time-points in X and y"), - ), + (1, pytest.raises(ValueError, match="The number of time-points in X and y")), ], ) def test_score_time_points_y( @@ -720,13 +566,7 @@ def test_score_time_points_y( ("pseudo-r2-McFadden", does_not_raise()), ("pseudo-r2-Cohen", does_not_raise()), ("log-likelihood", does_not_raise()), - ( - "not-implemented", - pytest.raises( - NotImplementedError, - match="Scoring method not-implemented not implemented", - ), - ), + ("not-implemented", pytest.raises(NotImplementedError, match="Scoring method not-implemented not implemented")), ], ) def test_score_type_r2( @@ -767,19 +607,9 @@ def test_loglikelihood_against_scipy_stats(self, poissonGLM_model_instantiation) @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_predict_n_neuron_match_x( @@ -850,10 +680,7 @@ def test_predict_n_feature_consistency_x( "is_fit, expectation", [ (True, does_not_raise()), - ( - False, - pytest.raises(ValueError, match="This GLM instance is not fitted yet"), - ), + (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")), ], ) def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiation): @@ -873,19 +700,9 @@ def test_predict_is_fit(self, is_fit, expectation, poissonGLM_model_instantiatio @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_simulate_n_neuron_match_input( @@ -991,19 +808,9 @@ def test_simulate_y_dimensionality( @pytest.mark.parametrize( "delta_n_neuron, expectation", [ - ( - -1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (-1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, match="The number of neurons in the model parameters" - ), - ), + (1, pytest.raises(ValueError, match="The number of neurons in the model parameters")), ], ) def test_simulate_n_neuron_match_y( @@ -1035,10 +842,7 @@ def test_simulate_n_neuron_match_y( "is_fit, expectation", [ (True, does_not_raise()), - ( - False, - pytest.raises(ValueError, match="This GLM instance is not fitted yet"), - ), + (False, pytest.raises(ValueError, match="This GLM instance is not fitted yet")), ], ) def test_simulate_is_fit( @@ -1067,15 +871,9 @@ def test_simulate_is_fit( @pytest.mark.parametrize( "delta_tp, expectation", [ - ( - -1, - pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), - ), + (-1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), (0, does_not_raise()), - ( - 1, - pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), - ), + (1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), ], ) def test_simulate_time_point_match_y( @@ -1104,15 +902,9 @@ def test_simulate_time_point_match_y( @pytest.mark.parametrize( "delta_tp, expectation", [ - ( - -1, - pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), - ), + (-1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), (0, does_not_raise()), - ( - 1, - pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`"), - ), + (1, pytest.raises(ValueError, match="`init_y` and `coupling_basis_matrix`")), ], ) def test_simulate_time_point_match_coupling_basis( @@ -1143,21 +935,9 @@ def test_simulate_time_point_match_coupling_basis( @pytest.mark.parametrize( "delta_features, expectation", [ - ( - -1, - pytest.raises( - ValueError, - match="Inconsistent number of features. spike basis coefficients has", - ), - ), + (-1, pytest.raises(ValueError, match="Inconsistent number of features. spike basis coefficients has")), (0, does_not_raise()), - ( - 1, - pytest.raises( - ValueError, - match="Inconsistent number of features. spike basis coefficients has", - ), - ), + (1, pytest.raises(ValueError, match="Inconsistent number of features. spike basis coefficients has")), ], ) def test_simulate_feature_consistency_input( From 2e2c071bb4df8ee8bf0a13c160c08a64a6d2235b Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Dec 2023 15:31:49 -0500 Subject: [PATCH 236/250] fixed docstrings --- src/nemos/glm.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/nemos/glm.py b/src/nemos/glm.py index bfc579d5..1ab85170 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -44,8 +44,7 @@ class GLM(BaseRegressor): Raises ------ TypeError - If provided `regularizer` or `observation_model` are not valid or implemented in `nemos.solver` and - `nemos.observation_models` respectively. + If provided `regularizer` or `observation_model` are not valid. """ def __init__( From 9645f9d6e274adc21a5c8ba925d036df00e1b0bc Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Mon, 11 Dec 2023 15:32:00 -0500 Subject: [PATCH 237/250] fix docstring --- src/nemos/regularizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/regularizer.py b/src/nemos/regularizer.py index 678e9445..ccd97aec 100644 --- a/src/nemos/regularizer.py +++ b/src/nemos/regularizer.py @@ -198,7 +198,7 @@ class are defined in the `allowed_solvers` attribute. See Also -------- - [Regularizer](./#nemos.solver.Regularizer) : Base solver class from which this class inherits. + [Regularizer](./#nemos.regularizer.Regularizer) : Base solver class from which this class inherits. """ allowed_solvers = [ From 1ee0de4882a9900afa9b181ff2d5204f2ad23fe0 Mon Sep 17 00:00:00 2001 From: "William F. Broderick" Date: Mon, 11 Dec 2023 15:34:16 -0500 Subject: [PATCH 238/250] choen -> cohen --- docs/developers_notes/03-observation_models.md | 2 +- src/nemos/glm.py | 2 +- src/nemos/observation_models.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/developers_notes/03-observation_models.md b/docs/developers_notes/03-observation_models.md index 190686fd..b7429bcd 100644 --- a/docs/developers_notes/03-observation_models.md +++ b/docs/developers_notes/03-observation_models.md @@ -24,7 +24,7 @@ For subclasses derived from `Observations` to function correctly, they must impl ### Public Methods -- **pseudo_r2**: Method for computing the pseudo-$R^2$ of the model based on the residual deviance. There is no consensus definition for the pseudo-$R^2$, what we used here is the definition by Choen at al. 2003[^2]. +- **pseudo_r2**: Method for computing the pseudo-$R^2$ of the model based on the residual deviance. There is no consensus definition for the pseudo-$R^2$, what we used here is the definition by Cohen at al. 2003[^2]. ### Auxiliary Methods diff --git a/src/nemos/glm.py b/src/nemos/glm.py index 80866f7b..e763064c 100644 --- a/src/nemos/glm.py +++ b/src/nemos/glm.py @@ -309,7 +309,7 @@ def score( raise NotImplementedError( f"Scoring method {score_type} not implemented! " "`score_type` must be either 'log-likelihood', 'pseudo-r2-McFadden', " - "or 'pseudo-r2-Choen'." + "or 'pseudo-r2-Cohen'." ) return score diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 3085f883..b6d8c166 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -264,15 +264,15 @@ def pseudo_r2( if score_type == "pseudo-r2-McFadden": pseudo_r2 = self._pseudo_r2_mcfadden(predicted_rate, y) elif score_type == "pseudo-r2-Cohen": - pseudo_r2 = self._pseudo_r2_choen(predicted_rate, y) + pseudo_r2 = self._pseudo_r2_cohen(predicted_rate, y) else: raise NotImplementedError(f"Score {score_type} not implemented!") return pseudo_r2 - def _pseudo_r2_choen( + def _pseudo_r2_cohen( self, predicted_rate: jnp.ndarray, y: jnp.ndarray ) -> jnp.ndarray: - r"""Choen's pseudo-$R^2$. + r"""Cohen's pseudo-$R^2$. Compute the pseudo-$R^2$ metric as defined by Cohen et al. (2002). See [`pseudo_r2`](#pseudo_r2) for additional information. From 4cc2d34e549765c2512ab5971e3771ad0fb8f4c0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Dec 2023 16:50:24 -0500 Subject: [PATCH 239/250] added statsmodels equivalence in docstrings --- src/nemos/observation_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 3085f883..51e488a6 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -237,8 +237,9 @@ def pseudo_r2( ----- - The McFadden pseudo-$R^2$ is given by: $$ - R^2_{\text{mcf}} = 1 - \frac{\log(L_{M})}{\log(L_0)} + R^2_{\text{mcf}} = 1 - \frac{\log(L_{M})}{\log(L_0)}. $$ + *Equivalent to statsmodels [`GLMResults.pseudo_rsquared(kind='mcf')`](https://www.statsmodels.org/dev/generated/statsmodels.genmod.generalized_linear_model.GLMResults.pseudo_rsquared.html).* - The Cohen pseudo-$R^2$ is given by: $$ \begin{aligned} From d016665e1fce6a4b9ec9a91d8c0dc58fa457fe0d Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Mon, 11 Dec 2023 18:15:55 -0500 Subject: [PATCH 240/250] linted flake8 --- src/nemos/observation_models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/nemos/observation_models.py b/src/nemos/observation_models.py index 3fbe4601..ce80bad7 100644 --- a/src/nemos/observation_models.py +++ b/src/nemos/observation_models.py @@ -239,7 +239,8 @@ def pseudo_r2( $$ R^2_{\text{mcf}} = 1 - \frac{\log(L_{M})}{\log(L_0)}. $$ - *Equivalent to statsmodels [`GLMResults.pseudo_rsquared(kind='mcf')`](https://www.statsmodels.org/dev/generated/statsmodels.genmod.generalized_linear_model.GLMResults.pseudo_rsquared.html).* + *Equivalent to statsmodels + [`GLMResults.pseudo_rsquared(kind='mcf')`](https://www.statsmodels.org/dev/generated/statsmodels.genmod.generalized_linear_model.GLMResults.pseudo_rsquared.html).* - The Cohen pseudo-$R^2$ is given by: $$ \begin{aligned} From e9bd25c4949a5137d1f419f932f7a1c2c9fae1bb Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 12 Dec 2023 11:41:34 -0500 Subject: [PATCH 241/250] generate parameters for simulaiton --- docs/examples/plot_glm_demo.py | 56 +++++++----- docs/examples/scripts/simulation_utils.py | 102 ++++++++++++++++++++++ 2 files changed, 137 insertions(+), 21 deletions(-) create mode 100644 docs/examples/scripts/simulation_utils.py diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index d168c395..f405a24f 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -29,8 +29,9 @@ import jax import matplotlib.pyplot as plt import numpy as np -import sklearn.model_selection as sklearn_model_selection from matplotlib.patches import Rectangle +from scripts import simulation_utils +from sklearn import model_selection import nemos as nmo @@ -162,7 +163,9 @@ # **Ridge** parameter_grid = {"regularizer__regularizer_strength": np.logspace(-1.5, 1.5, 6)} -cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +# in practice, you should use more folds than 2, but for the purposes of this +# demo, 2 is sufficient. +cls = model_selection.GridSearchCV(model, parameter_grid, cv=2) cls.fit(X, spikes) print("Ridge results ") @@ -176,7 +179,7 @@ # **Lasso** model.set_params(regularizer=nmo.regularizer.Lasso()) -cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls = model_selection.GridSearchCV(model, parameter_grid, cv=2) cls.fit(X, spikes) print("Lasso results ") @@ -194,7 +197,7 @@ regularizer = nmo.regularizer.GroupLasso("ProximalGradient", mask=mask) model.set_params(regularizer=regularizer) -cls = sklearn_model_selection.GridSearchCV(model, parameter_grid, cv=5) +cls = model_selection.GridSearchCV(model, parameter_grid, cv=2) cls.fit(X, spikes) print("\nGroup Lasso results") @@ -233,30 +236,41 @@ with open("coupled_neurons_params.json", "r") as fh: config_dict = json.load(fh) -# basis weights & intercept for the GLM (both coupling and feedforward) -# (the last coefficient is the weight of the feedforward input) -basis_coeff = np.asarray(config_dict["coef_"])[:, :-1] +# Neural population params +n_neurons = 2 +coupling_filter_duration = 100 +# basis weights & intercept for the GLM (coupling only) +coupling_basis, basis_coeff, intercept = simulation_utils.define_coupling_filters(n_neurons, coupling_filter_duration) -# Mask the weights so that only the first neuron receives the imput -basis_coeff[:, 40:] = np.abs(basis_coeff[:, 40:]) * np.array([[1.], [0.]]) +# define a squared current stimulus +simulation_duration = 1000 +stimulus_onset = 200 +stimulus_offset = 500 +stimulus_intensity = 1.5 +feedforward_input = np.zeros((simulation_duration, n_neurons, 1)) +# inject square input in the first neuron only +feedforward_input[stimulus_onset: stimulus_offset, 0] = stimulus_intensity -intercept = np.asarray(config_dict["intercept_"]) +# the input for the simulation will be the dot product +# of input_coeff with the `feedforward_input` +input_coeff = np.ones((n_neurons, 1)) -# basis function, inputs and initial spikes -coupling_basis = jax.numpy.asarray(config_dict["coupling_basis"]) -feedforward_input = jax.numpy.asarray(config_dict["feedforward_input"]) -init_spikes = jax.numpy.asarray(config_dict["init_spikes"]) +# Add the input coefficient to the basis +basis_coeff = np.hstack((basis_coeff, input_coeff)) + +# initialize spikes for the recurrent simulation +init_spikes = np.zeros((coupling_filter_duration, n_neurons)) # %% # We can explore visualize the coupling filters and the input. # plot coupling functions n_basis_coupling = coupling_basis.shape[1] -fig, axs = plt.subplots(2,2) +fig, axs = plt.subplots(n_neurons,n_neurons) plt.suptitle("Coupling filters") -for unit_i in range(2): - for unit_j in range(2): - axs[unit_i,unit_j].set_title(f"unit {unit_j} -> unit {unit_i}") +for unit_i in range(n_neurons): + for unit_j in range(n_neurons): + axs[unit_i, unit_j].set_title(f"unit {unit_j} -> unit {unit_i}") coeff = basis_coeff[unit_i, unit_j * n_basis_coupling: (unit_j + 1) * n_basis_coupling] axs[unit_i, unit_j].plot(np.dot(coupling_basis, coeff)) plt.tight_layout() @@ -277,7 +291,7 @@ # call simulate, with both the recurrent coupling # and the input spikes, rates = model.simulate_recurrent( - random_key, + jax.random.PRNGKey(123), feedforward_input=feedforward_input, coupling_basis_matrix=coupling_basis, init_y=init_spikes @@ -298,8 +312,8 @@ p0, = plt.plot(rates[:, 0]) p1, = plt.plot(rates[:, 1]) -plt.vlines(np.where(spikes[:, 0])[0], 0.00, 0.01, color=p0.get_color(), label="neu 0") -plt.vlines(np.where(spikes[:, 1])[0], -0.01, 0.00, color=p1.get_color(), label="neu 1") +plt.vlines(np.where(spikes[:, 0])[0], 0.00, 0.01, color=p0.get_color(), label="rate neuron 0") +plt.vlines(np.where(spikes[:, 1])[0], -0.01, 0.00, color=p1.get_color(), label="rate neuron 1") plt.plot(np.exp(basis_coeff[0, -1] * feedforward_input[:, 0, 0] + intercept[0]), color='k', lw=0.8, label="stimulus") ax.add_patch(patch) plt.ylim(-0.011, .13) diff --git a/docs/examples/scripts/simulation_utils.py b/docs/examples/scripts/simulation_utils.py new file mode 100644 index 00000000..71f15c5e --- /dev/null +++ b/docs/examples/scripts/simulation_utils.py @@ -0,0 +1,102 @@ +"""Utility functions for coupling filter definition.""" + +from typing import Tuple + +import numpy as np +import scipy.stats as sts +from numpy.typing import NDArray + +import nemos as nmo + + +def temporal_fitler(ws: int, inhib_a: float = 1., excit_a: float = 2., inhib_b: float = 2., excit_b: float = 2.)\ + -> NDArray: + """Generate coupling filter as Gamma pdf difference. + + Parameters + ---------- + ws: + The window size of the filter. + inhib_a: + The `a` constant for the gamma pdf of the inhibitory part of the filer. + excit_a: + The `a` constant for the gamma pdf of the excitatory part of the filer. + inhib_b: + The `b` constant for the gamma pdf of the inhibitory part of the filer. + excit_b: + The `a` constant for the gamma pdf of the excitatory part of the filer. + + Returns + ------- + filter: + The coupling filter. + """ + x = np.linspace(0, 5, ws) + gm_inhibition = sts.gamma(a=inhib_a, scale=1/inhib_b) + gm_excitation = sts.gamma(a=excit_a, scale=1/excit_b) + filter = gm_excitation.pdf(x) - gm_inhibition.pdf(x) + # impose a norm < 1 for the filter + filter = 0.8 * filter / np.linalg.norm(filter) + return filter + + +def regress_filter(coupling_filter_bank: NDArray, basis: nmo.basis.Basis) -> Tuple[NDArray, NDArray]: + """Approximate scipy.stats.gamma based filters with basis function. + + Find the ols weights for representing the filters in terms of basis functions. + This is done to re-use the nsl.glm.simulate method. + + Parameters + ---------- + coupling_filter_bank: + The coupling filters. Shape (n_neurons, n_neurons, window_size) + basis: + The basis function to instantiate. + + Returns + ------- + eval_basis: + The basis matrix, shape (window_size, n_basis_funcs) + weights: + The weights for each neuron. Shape (n_neurons, n_neurons, n_basis_funcs) + """ + n_neurons, _, ws = coupling_filter_bank.shape + eval_basis = basis.evaluate(np.linspace(0, 1, ws)) + + # Reshape the coupling_filter_bank for vectorized least squares + filters_reshaped = coupling_filter_bank.reshape(-1, ws) + + # Solve the least squares problem for all filters at once + # (vecotrizing the features) + weights = np.linalg.lstsq(eval_basis, filters_reshaped.T, rcond=None)[0] + + # Reshape back to the original dimensions + weights = weights.T.reshape(n_neurons, n_neurons, -1) + + return eval_basis, weights + + +def define_coupling_filters(n_neurons: int, window_size: int, n_basis_funcs: int = 20): + + np.random.seed(101) + # inhibition params + a_inhib = 1 + b_inhib = 1 + a_excit = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) + b_excit = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) + + # define 2x2 coupling filters of the specific width + coupling_filter_bank = np.zeros((n_neurons, n_neurons, window_size)) + for neu_i in range(n_neurons): + for neu_j in range(n_neurons): + coupling_filter_bank[neu_i, neu_j, :] = temporal_fitler(window_size, + inhib_a=a_inhib, + excit_a=a_excit[neu_i, neu_j], + inhib_b=b_inhib, + excit_b=b_excit[neu_i, neu_j] + ) + basis = nmo.basis.RaisedCosineBasisLog(n_basis_funcs) + coupling_basis, weights = regress_filter(coupling_filter_bank, basis) + weights = weights.reshape(n_neurons, -1) + intercept = -4 * np.ones(n_neurons) + return coupling_basis, weights, intercept From 4679dc2d95c52d9c74b3f5ccb9353c04d93f52a0 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Tue, 12 Dec 2023 11:42:52 -0500 Subject: [PATCH 242/250] removed json in docs --- docs/examples/coupled_neurons_params.json | 1 - docs/examples/plot_glm_demo.py | 4 ---- 2 files changed, 5 deletions(-) delete mode 100644 docs/examples/coupled_neurons_params.json diff --git a/docs/examples/coupled_neurons_params.json b/docs/examples/coupled_neurons_params.json deleted file mode 100644 index 296bcf24..00000000 --- a/docs/examples/coupled_neurons_params.json +++ /dev/null @@ -1 +0,0 @@ -{"intercept_":[-4.0,-4.0],"coef_":[[-0.004372,-0.02786,-0.04582,-0.0588,-0.06539,-0.06396,-0.05328,-0.03192,0.0002296,0.04143,0.08794,0.1483,0.2053,0.2483,0.2892,0.3093,0.2917,0.2225,0.07357,-0.2711,-0.006235,-0.01047,0.02189,0.058,0.09002,0.1118,0.1209,0.1167,0.09909,0.07044,0.03448,-0.01565,-0.06823,-0.1128,-0.1655,-0.2176,-0.2621,-0.2982,-0.3255,-0.3449,0.5,0.5],[-0.004637,0.02223,0.07071,0.09572,0.1012,0.08923,0.06464,0.03076,-0.007911,-0.04737,-0.08429,-0.1249,-0.1582,-0.1827,-0.2081,-0.23,-0.2473,-0.2616,-0.2741,-0.287,0.01127,0.04864,0.0544,0.05082,0.03975,0.02393,0.004725,-0.01763,-0.04202,-0.06744,-0.09269,-0.1231,-0.1522,-0.1763,-0.2051,-0.2348,-0.2629,-0.2896,-0.3149,-0.3389,0.5,0.5]],"coupling_basis":[[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0024979173609873673,0.9975020826390129],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.11451325277931029,0.8854867472206909,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25013898844998006,0.7498610115500185,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.3122501403134024,0.687749859686596,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.28176761370807446,0.7182323862919272,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17383844924397923,0.8261615507560222,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.04364762794083282,0.9563523720591665,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9912618171282106,0.008738182871789013,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7892946476427273,0.21070535235727128,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.3531647741677867,0.6468352258322151,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.011883820048045501,0.9881161799519544,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7841665801263835,0.21583341987361648,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.17688067665784446,0.8231193233421555,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9253003862638604,0.0746996137361397,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.2549435480705588,0.7450564519294413,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.9205258993369989,0.07947410066300109,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.16827351931758228,0.8317264806824178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.0,0.7835282009408713,0.21647179905912872,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.019118847416525586,0.9808811525834744,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.4372031242218587,0.5627968757781414,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.0,0.9120243919870162,0.08797560801298382,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.044222034278324274,0.9557779657216758,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.40793669708774605,0.5920633029122541,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.8283923698925478,0.17160763010745222,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.0,0.9999802058373224,1.9794162677666538e-05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.1458111022283093,0.8541888977716907,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.4778824971400245,0.5221175028599756,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.803486827077907,0.19651317292209308,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.0,0.9824675828481839,0.017532417151816082,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.029720664099906924,0.9702793359000932,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.19724020774947038,0.8027597922505296,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.44389603578613035,0.5561039642138698,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.6909694421867117,0.30903055781328825,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.8804498633788072,0.1195501366211929,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.0,0.9828262050955638,0.017173794904436157,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.005816278861877466,0.9941837211381226,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.07171948190677246,0.9282805180932275,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.19211081158089233,0.8078891884191077,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.3422365913893123,0.6577634086106878,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.49997219806462273,0.5000278019353773,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.6481581380891199,0.3518418619108801,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.775227808426499,0.22477219157350103,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.8747644272334134,0.12523557276658664,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.9445228823471115,0.05547711765288865,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.9852942394771702,0.014705760522829736,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.0,0.9998405276097415,0.00015947239025848603,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.00798856965539202,0.9920114303446079,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.03392307742054024,0.9660769225794598,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.07373523476821137,0.9262647652317886,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.12352988337197751,0.8764701166280225,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.17990211564285485,0.8200978843571451,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.2399997347398921,0.7600002652601079,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.3015222924967669,0.6984777075032332,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.36268149196393995,0.63731850803606,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.42214108290743424,0.5778589170925659,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.47894873221112266,0.5210512677888774,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.5324679173051469,0.46753208269485313,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.5823146093533313,0.4176853906466687,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.6283012081735033,0.3716987918264968,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.6703886551778314,0.32961134482216864,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.7086466881407022,0.2913533118592979,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.7432216468423799,0.25677835315762026,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.7743109612271127,0.22568903877288732,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.802143356101582,0.197856643898418,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.82696381862707,0.17303618137292998,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.8490224486822571,0.15097755131774288,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.8685664156253453,0.13143358437465474,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.8858343578296817,0.11416564217031833,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9010526715389762,0.09894732846102389,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9144332365128198,0.08556676348718023,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9261722145965264,0.07382778540347357,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9364496329422705,0.06355036705772948,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9454295266061546,0.05457047339384541,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9532604668007324,0.04673953319926766,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9600763426393057,0.039923657360694254,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9659972972699125,0.03400270273008754,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.971130745291511,0.028869254708488945,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.975572418558468,0.024427581441531954,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9794074030288873,0.020592596971112653,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9827111411428311,0.017288858857168965,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9855503831123861,0.014449616887613925,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9879840771076767,0.012015922892323394,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9900641931482845,0.009935806851715523,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9918364789707291,0.008163521029270815,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9933411485659462,0.006658851434053759,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9946135057219054,0.005386494278094567,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9956845059646938,0.004315494035306178,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9965812609202838,0.0034187390797163486,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.997327489436671,0.002672510563328956,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9979439199017871,0.002056080098212898,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9984486481342357,0.0015513518657642722,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9988574550621354,0.0011425449378646424,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9991840881776304,0.0008159118223696749,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.999440510488429,0.0005594895115710874,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9996371204027914,0.00036287959720865404,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.999782945694725,0.00021705430527496627,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9998858144113889,0.00011418558861114869,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.9999525053112863,4.7494688713622946e-05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[0.99998888016377,1.1119836230089053e-05,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0],[1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]],"feedforward_input":[[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[2.5],[2.5]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]],[[0.0],[0.0]]],"init_spikes":[[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0],[0.0,0.0]],"n_neurons":2} \ No newline at end of file diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index f405a24f..7a283b83 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -232,10 +232,6 @@ # For brevity, we will import the model parameters instead of generating # them on the fly. -# load parameters -with open("coupled_neurons_params.json", "r") as fh: - config_dict = json.load(fh) - # Neural population params n_neurons = 2 coupling_filter_duration = 100 From 5d32df94a15c8397febe1abc4107a23352c10f07 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Wed, 13 Dec 2023 10:34:04 -0500 Subject: [PATCH 243/250] modified the glm demo --- docs/examples/plot_glm_demo.py | 124 +++++++++++++++------- docs/examples/scripts/simulation_utils.py | 102 ------------------ src/nemos/__init__.py | 1 + src/nemos/simulation.py | 107 +++++++++++++++++++ tests/conftest.py | 27 ++--- 5 files changed, 207 insertions(+), 154 deletions(-) delete mode 100644 docs/examples/scripts/simulation_utils.py create mode 100644 src/nemos/simulation.py diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index 7a283b83..aeaa3f11 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -24,22 +24,21 @@ data. """ -import json import jax import matplotlib.pyplot as plt import numpy as np from matplotlib.patches import Rectangle -from scripts import simulation_utils from sklearn import model_selection import nemos as nmo +from nemos import simulation -# Enable float64 precision (optional) +# enable float64 precision (optional) jax.config.update("jax_enable_x64", True) np.random.seed(111) -# Random design tensor. Shape (n_time_points, n_neurons, n_features). +# random design tensor. Shape (n_time_points, n_neurons, n_features). X = 0.5*np.random.normal(size=(100, 1, 5)) # log-rates & weights, shape (n_neurons, ) and (n_neurons, n_features) respectively. @@ -79,12 +78,12 @@ # ### Model Configuration # One could visualize the model hyperparameters by calling `get_params` method. -# Get the glm model parameters only +# get the glm model parameters only print("\nGLM model parameters:") for key, value in model.get_params(deep=False).items(): print(f"\t- {key}: {value}") -# Get the glm model parameters, including the all the +# get the glm model parameters, including the all the # attributes print("\nNested parameters:") for key, value in model.get_params(deep=True).items(): @@ -142,7 +141,7 @@ # Additionally one may provide an initial parameter guess. # The same exact syntax works for any configuration. -# Fit a ridge regression Poisson GLM +# fit a ridge regression Poisson GLM model = nmo.glm.GLM() model.set_params(regularizer__regularizer_strength=0.1) model.fit(X, spikes) @@ -226,56 +225,109 @@ # %% # ## Recurrently Coupled GLM # Defining a recurrent model follows the same syntax. In this example -# we will simulate two coupled neurons. and we will inject a transient +# we will simulate two coupled neurons, and we will inject a transient # input driving the rate of one of the neurons. -# -# For brevity, we will import the model parameters instead of generating -# them on the fly. -# Neural population params + +# Neural population parameters n_neurons = 2 coupling_filter_duration = 100 -# basis weights & intercept for the GLM (coupling only) -coupling_basis, basis_coeff, intercept = simulation_utils.define_coupling_filters(n_neurons, coupling_filter_duration) -# define a squared current stimulus -simulation_duration = 1000 -stimulus_onset = 200 -stimulus_offset = 500 -stimulus_intensity = 1.5 -feedforward_input = np.zeros((simulation_duration, n_neurons, 1)) -# inject square input in the first neuron only -feedforward_input[stimulus_onset: stimulus_offset, 0] = stimulus_intensity +# %% +# We can now to define coupling filters that we will use to simulate +# the pairwise interactions between the neurons. We will model the +# filters as a difference of two Gamma probability density function. +# The negative component will capture inhibitory effects such as the +# refractory period of a neuron, while the positive component will +# describe excitation. -# the input for the simulation will be the dot product -# of input_coeff with the `feedforward_input` -input_coeff = np.ones((n_neurons, 1)) +np.random.seed(101) -# Add the input coefficient to the basis -basis_coeff = np.hstack((basis_coeff, input_coeff)) +# Gamma parameter for the inhibitory component of the fi;ter +inhib_a = 1 +inhib_b = 1 -# initialize spikes for the recurrent simulation -init_spikes = np.zeros((coupling_filter_duration, n_neurons)) +# Gamma parameters for the excitatory component of the filter +excit_a = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) +excit_b = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) + +# define 2x2 coupling filters of the specific with create_temporal_filter +coupling_filter_bank = np.zeros((n_neurons, n_neurons, coupling_filter_duration)) +for unit_i in range(n_neurons): + for unit_j in range(n_neurons): + coupling_filter_bank[unit_i, unit_j, :] = nmo.simulation.difference_of_gammas( + coupling_filter_duration, + inhib_a=inhib_a, + excit_a=excit_a[unit_i, unit_j], + inhib_b=inhib_b, + excit_b=excit_b[unit_i, unit_j], + ) + +# %% +# If we represent our filters in terms of basis functions, we can simulate our network by +# directly calling the `simulate` method of the `nmo.glm.GLMRecurrent` class. + +# define a basis function +n_basis_funcs = 20 +basis = nmo.basis.RaisedCosineBasisLog(n_basis_funcs) + +# approximate the coupling filters in terms of the basis function +coupling_basis, coupling_coeff = simulation.regress_filter(coupling_filter_bank, basis) +intercept = -4 * np.ones(n_neurons) # %% -# We can explore visualize the coupling filters and the input. +# We can check that our approximation worked by plotting the original filters +# and the basis expansion # plot coupling functions n_basis_coupling = coupling_basis.shape[1] -fig, axs = plt.subplots(n_neurons,n_neurons) +fig, axs = plt.subplots(n_neurons, n_neurons) plt.suptitle("Coupling filters") for unit_i in range(n_neurons): for unit_j in range(n_neurons): axs[unit_i, unit_j].set_title(f"unit {unit_j} -> unit {unit_i}") - coeff = basis_coeff[unit_i, unit_j * n_basis_coupling: (unit_j + 1) * n_basis_coupling] - axs[unit_i, unit_j].plot(np.dot(coupling_basis, coeff)) + coeff = coupling_coeff[unit_i, unit_j] + axs[unit_i, unit_j].plot(coupling_filter_bank[unit_i, unit_j], label="gamma difference") + axs[unit_i, unit_j].plot(np.dot(coupling_basis, coeff), ls="--", color="k", label="basis function") +axs[0, 0].legend() plt.tight_layout() -fig, axs = plt.subplots(1,1) -plt.title("Feedforward inputs") -plt.plot(feedforward_input[:, 0]) +# %% +# Define a squared stimulus current for the first neuron, and no stimulus for +# the second neuron + +# define a squared current parameters +simulation_duration = 1000 +stimulus_onset = 200 +stimulus_offset = 500 +stimulus_intensity = 1.5 + +# create the input tensor of shape (n_samples, n_neurons, n_dimension_stimuli) +feedforward_input = np.zeros((simulation_duration, n_neurons, 1)) +# inject square input to the first neuron only +feedforward_input[stimulus_onset: stimulus_offset, 0] = stimulus_intensity + +# plot the input +fig, axs = plt.subplots(1,2) +plt.suptitle("Feedforward inputs") +axs[0].set_title("Input to neuron 0") +axs[0].plot(feedforward_input[:, 0]) + +axs[1].set_title("Input to neuron 1") +axs[1].plot(feedforward_input[:, 1]) +axs[1].set_ylim(axs[0].get_ylim()) +# the input for the simulation will be the dot product +# of input_coeff with the feedforward_input +input_coeff = np.ones((n_neurons, 1)) + +# stack the coefficients in a single matrix +basis_coeff = np.hstack((coupling_coeff.reshape(n_neurons, -1), input_coeff)) + +# initialize the spikes for the recurrent simulation +init_spikes = np.zeros((coupling_filter_duration, n_neurons)) + # %% # We can now simulate spikes by calling the `simulate_recurrent` method. diff --git a/docs/examples/scripts/simulation_utils.py b/docs/examples/scripts/simulation_utils.py deleted file mode 100644 index 71f15c5e..00000000 --- a/docs/examples/scripts/simulation_utils.py +++ /dev/null @@ -1,102 +0,0 @@ -"""Utility functions for coupling filter definition.""" - -from typing import Tuple - -import numpy as np -import scipy.stats as sts -from numpy.typing import NDArray - -import nemos as nmo - - -def temporal_fitler(ws: int, inhib_a: float = 1., excit_a: float = 2., inhib_b: float = 2., excit_b: float = 2.)\ - -> NDArray: - """Generate coupling filter as Gamma pdf difference. - - Parameters - ---------- - ws: - The window size of the filter. - inhib_a: - The `a` constant for the gamma pdf of the inhibitory part of the filer. - excit_a: - The `a` constant for the gamma pdf of the excitatory part of the filer. - inhib_b: - The `b` constant for the gamma pdf of the inhibitory part of the filer. - excit_b: - The `a` constant for the gamma pdf of the excitatory part of the filer. - - Returns - ------- - filter: - The coupling filter. - """ - x = np.linspace(0, 5, ws) - gm_inhibition = sts.gamma(a=inhib_a, scale=1/inhib_b) - gm_excitation = sts.gamma(a=excit_a, scale=1/excit_b) - filter = gm_excitation.pdf(x) - gm_inhibition.pdf(x) - # impose a norm < 1 for the filter - filter = 0.8 * filter / np.linalg.norm(filter) - return filter - - -def regress_filter(coupling_filter_bank: NDArray, basis: nmo.basis.Basis) -> Tuple[NDArray, NDArray]: - """Approximate scipy.stats.gamma based filters with basis function. - - Find the ols weights for representing the filters in terms of basis functions. - This is done to re-use the nsl.glm.simulate method. - - Parameters - ---------- - coupling_filter_bank: - The coupling filters. Shape (n_neurons, n_neurons, window_size) - basis: - The basis function to instantiate. - - Returns - ------- - eval_basis: - The basis matrix, shape (window_size, n_basis_funcs) - weights: - The weights for each neuron. Shape (n_neurons, n_neurons, n_basis_funcs) - """ - n_neurons, _, ws = coupling_filter_bank.shape - eval_basis = basis.evaluate(np.linspace(0, 1, ws)) - - # Reshape the coupling_filter_bank for vectorized least squares - filters_reshaped = coupling_filter_bank.reshape(-1, ws) - - # Solve the least squares problem for all filters at once - # (vecotrizing the features) - weights = np.linalg.lstsq(eval_basis, filters_reshaped.T, rcond=None)[0] - - # Reshape back to the original dimensions - weights = weights.T.reshape(n_neurons, n_neurons, -1) - - return eval_basis, weights - - -def define_coupling_filters(n_neurons: int, window_size: int, n_basis_funcs: int = 20): - - np.random.seed(101) - # inhibition params - a_inhib = 1 - b_inhib = 1 - a_excit = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) - b_excit = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) - - # define 2x2 coupling filters of the specific width - coupling_filter_bank = np.zeros((n_neurons, n_neurons, window_size)) - for neu_i in range(n_neurons): - for neu_j in range(n_neurons): - coupling_filter_bank[neu_i, neu_j, :] = temporal_fitler(window_size, - inhib_a=a_inhib, - excit_a=a_excit[neu_i, neu_j], - inhib_b=b_inhib, - excit_b=b_excit[neu_i, neu_j] - ) - basis = nmo.basis.RaisedCosineBasisLog(n_basis_funcs) - coupling_basis, weights = regress_filter(coupling_filter_bank, basis) - weights = weights.reshape(n_neurons, -1) - intercept = -4 * np.ones(n_neurons) - return coupling_basis, weights, intercept diff --git a/src/nemos/__init__.py b/src/nemos/__init__.py index 25c573b3..8938c85c 100644 --- a/src/nemos/__init__.py +++ b/src/nemos/__init__.py @@ -7,5 +7,6 @@ observation_models, regularizer, sample_points, + simulation, utils, ) diff --git a/src/nemos/simulation.py b/src/nemos/simulation.py new file mode 100644 index 00000000..1d1a4ec5 --- /dev/null +++ b/src/nemos/simulation.py @@ -0,0 +1,107 @@ +"""Utility functions for coupling filter definition.""" + +from typing import Tuple + +import numpy as np +import scipy.stats as sts +from numpy.typing import NDArray + +from .basis import Basis + + +def difference_of_gammas( + ws: int, + upper_percentile: float = 0.99, + inhib_a: float = 1.0, + excit_a: float = 2.0, + inhib_b: float = 2.0, + excit_b: float = 2.0, +) -> NDArray: + r"""Generate coupling filter as a Gamma pdf difference. + + Parameters + ---------- + ws: + The window size of the filter. + upper_percentile: + Upper bound of the gamma range as a percentile. The gamma function + will be evaluated over the range [0, ppf(upper_percentile)]. + inhib_a: + The `a` constant for the gamma pdf of the inhibitory part of the filter. + excit_a: + The `a` constant for the gamma pdf of the excitatory part of the filter. + inhib_b: + The `b` constant for the gamma pdf of the inhibitory part of the filter. + excit_b: + The `a` constant for the gamma pdf of the excitatory part of the filter. + + Notes + ----- + The probability density function of a gamma distribution is parametrized as + follows$^1$, + $$ + p(x;\; a, b) = \frac{b^a x^{a-1} e^{-x}}{\Gamma(a)}, + $$ + where $\Gamma(a)$ refers to teh gamma function, see$^1$. + + Returns + ------- + filter: + The coupling filter. + + References + ---------- + 1. [SciPy Docs - "scipy.stats.gamma"](https://docs.scipy.org/doc/ + scipy/reference/generated/scipy.stats.gamma.html) + """ + gm_inhibition = sts.gamma(a=inhib_a, scale=1 / inhib_b) + gm_excitation = sts.gamma(a=excit_a, scale=1 / excit_b) + + # calculate upper bound for the evaluation + xmax = max(gm_inhibition.ppf(upper_percentile), gm_excitation.ppf(upper_percentile)) + # equi-spaced sample covering the range + x = np.linspace(0, xmax, ws) + + # compute difference of gammas & normalize + gamma_diff = gm_excitation.pdf(x) - gm_inhibition.pdf(x) + gamma_diff = gamma_diff / np.linalg.norm(gamma_diff, ord=2) + + return gamma_diff + + +def regress_filter( + coupling_filter_bank: NDArray, basis: Basis +) -> Tuple[NDArray, NDArray]: + """Approximate scipy.stats.gamma based filters with basis function. + + Find the ols weights for representing the filters in terms of basis functions. + This is done to re-use the nsl.glm.simulate method. + + Parameters + ---------- + coupling_filter_bank: + The coupling filters. Shape (n_neurons, n_neurons, window_size) + basis: + The basis function to instantiate. + + Returns + ------- + eval_basis: + The basis matrix, shape (window_size, n_basis_funcs) + weights: + The weights for each neuron. Shape (n_neurons, n_neurons, n_basis_funcs) + """ + n_neurons, _, ws = coupling_filter_bank.shape + eval_basis = basis.evaluate(np.linspace(0, 1, ws)) + + # Reshape the coupling_filter_bank for vectorized least-squares + filters_reshaped = coupling_filter_bank.reshape(-1, ws) + + # Solve the least squares problem for all filters at once + # (vecotrizing the features) + weights = np.linalg.lstsq(eval_basis, filters_reshaped.T, rcond=None)[0] + + # Reshape back to the original dimensions + weights = weights.T.reshape(n_neurons, n_neurons, -1) + + return eval_basis, weights diff --git a/tests/conftest.py b/tests/conftest.py index c39dcd4d..1499b80b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,10 +8,6 @@ This module primarily serves as a utility for test configurations, setting up initial conditions, and loading predefined parameters for testing various functionalities of the `nemos` library. """ -import inspect -import json -import os - import jax import jax.numpy as jnp import numpy as np @@ -62,23 +58,22 @@ def poissonGLM_coupled_model_config_simulate(): - init_spikes (jax.numpy.ndarray): Initial spike values from the config. - jax.random.PRNGKey(123) (jax.random.PRNGKey): A pseudo-random number generator key. """ - current_file = inspect.getfile(inspect.currentframe()) - test_dir = os.path.dirname(os.path.abspath(current_file)) - with open( - os.path.join(test_dir, "simulate_coupled_neurons_params.json"), "r" - ) as fh: - config_dict = json.load(fh) - observations = nmo.observation_models.PoissonObservations(jnp.exp) regularizer = nmo.regularizer.Ridge("BFGS", regularizer_strength=0.1) model = nmo.glm.GLMRecurrent( observation_model=observations, regularizer=regularizer ) - model.coef_ = jnp.asarray(config_dict["coef_"]) - model.intercept_ = jnp.asarray(config_dict["intercept_"]) - coupling_basis = jnp.asarray(config_dict["coupling_basis"]) - feedforward_input = jnp.asarray(config_dict["feedforward_input"]) - init_spikes = jnp.asarray(config_dict["init_spikes"]) + + n_neurons, coupling_duration, sim_duration = 2, 100, 1000 + coupling_basis, basis_coeff, intercept = nmo.simulation.define_coupling_filters(n_neurons, coupling_duration) + model.coef_ = jnp.hstack([basis_coeff, 0.5 * jnp.ones((n_neurons, n_neurons))]) + model.intercept_ = -3 * jnp.ones(n_neurons) + feedforward_input = jnp.c_[ + jnp.cos(jnp.linspace(0, np.pi*4, sim_duration)), + jnp.sin(jnp.linspace(0, np.pi*4, sim_duration)) + ] + feedforward_input = jnp.tile(feedforward_input[:, None], (1, n_neurons, 1)) + init_spikes = jnp.zeros((coupling_duration, n_neurons)) return ( model, From 29303731dbc6e1996215171679e8b0b1be807dd5 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 14 Dec 2023 10:55:11 -0500 Subject: [PATCH 244/250] added testing --- docs/examples/plot_glm_demo.py | 12 ++- src/nemos/simulation.py | 83 ++++++++++++++----- tests/test_simulation.py | 147 +++++++++++++++++++++++++++++++++ 3 files changed, 216 insertions(+), 26 deletions(-) create mode 100644 tests/test_simulation.py diff --git a/docs/examples/plot_glm_demo.py b/docs/examples/plot_glm_demo.py index aeaa3f11..93bc6445 100644 --- a/docs/examples/plot_glm_demo.py +++ b/docs/examples/plot_glm_demo.py @@ -252,10 +252,10 @@ excit_b = np.random.uniform(1.1, 5, size=(n_neurons, n_neurons)) # define 2x2 coupling filters of the specific with create_temporal_filter -coupling_filter_bank = np.zeros((n_neurons, n_neurons, coupling_filter_duration)) +coupling_filter_bank = np.zeros((coupling_filter_duration, n_neurons, n_neurons)) for unit_i in range(n_neurons): for unit_j in range(n_neurons): - coupling_filter_bank[unit_i, unit_j, :] = nmo.simulation.difference_of_gammas( + coupling_filter_bank[:, unit_i, unit_j] = nmo.simulation.difference_of_gammas( coupling_filter_duration, inhib_a=inhib_a, excit_a=excit_a[unit_i, unit_j], @@ -263,6 +263,9 @@ excit_b=excit_b[unit_i, unit_j], ) +# shrink the filters for simulation stability +coupling_filter_bank *= 0.8 + # %% # If we represent our filters in terms of basis functions, we can simulate our network by # directly calling the `simulate` method of the `nmo.glm.GLMRecurrent` class. @@ -272,7 +275,8 @@ basis = nmo.basis.RaisedCosineBasisLog(n_basis_funcs) # approximate the coupling filters in terms of the basis function -coupling_basis, coupling_coeff = simulation.regress_filter(coupling_filter_bank, basis) +_, coupling_basis = basis.evaluate_on_grid(coupling_filter_bank.shape[0]) +coupling_coeff = simulation.regress_filter(coupling_filter_bank, coupling_basis) intercept = -4 * np.ones(n_neurons) # %% @@ -287,7 +291,7 @@ for unit_j in range(n_neurons): axs[unit_i, unit_j].set_title(f"unit {unit_j} -> unit {unit_i}") coeff = coupling_coeff[unit_i, unit_j] - axs[unit_i, unit_j].plot(coupling_filter_bank[unit_i, unit_j], label="gamma difference") + axs[unit_i, unit_j].plot(coupling_filter_bank[:, unit_i, unit_j], label="gamma difference") axs[unit_i, unit_j].plot(np.dot(coupling_basis, coeff), ls="--", color="k", label="basis function") axs[0, 0].legend() plt.tight_layout() diff --git a/src/nemos/simulation.py b/src/nemos/simulation.py index 1d1a4ec5..5b4bd028 100644 --- a/src/nemos/simulation.py +++ b/src/nemos/simulation.py @@ -1,20 +1,17 @@ """Utility functions for coupling filter definition.""" -from typing import Tuple import numpy as np import scipy.stats as sts from numpy.typing import NDArray -from .basis import Basis - def difference_of_gammas( ws: int, upper_percentile: float = 0.99, inhib_a: float = 1.0, excit_a: float = 2.0, - inhib_b: float = 2.0, + inhib_b: float = 1.0, excit_b: float = 2.0, ) -> NDArray: r"""Generate coupling filter as a Gamma pdf difference. @@ -49,11 +46,34 @@ def difference_of_gammas( filter: The coupling filter. + Raises + ------ + ValueError: + - If any of the Gamma parameters is lesser or equal to 0. + - If the upper_percentile is not in [0, 1). + References ---------- 1. [SciPy Docs - "scipy.stats.gamma"](https://docs.scipy.org/doc/ scipy/reference/generated/scipy.stats.gamma.html) """ + # check that the gamma parameters are positive (scipy returns + # nans otherwise but no exception is raised) + variables = { + "excit_a": excit_a, + "inhib_a": inhib_a, + "excit_b": excit_b, + "inhib_b": inhib_b, + } + for name, value in variables.items(): + if value <= 0: + raise ValueError(f"Gamma parameter {name} must be >0.") + # check for valid pecentile + if upper_percentile < 0 or upper_percentile >= 1: + raise ValueError( + f"upper_percentile should lie in the [0, 1) interval. {upper_percentile} provided instead!" + ) + gm_inhibition = sts.gamma(a=inhib_a, scale=1 / inhib_b) gm_excitation = sts.gamma(a=excit_a, scale=1 / excit_b) @@ -69,9 +89,7 @@ def difference_of_gammas( return gamma_diff -def regress_filter( - coupling_filter_bank: NDArray, basis: Basis -) -> Tuple[NDArray, NDArray]: +def regress_filter(coupling_filters: NDArray, eval_basis: NDArray) -> NDArray: """Approximate scipy.stats.gamma based filters with basis function. Find the ols weights for representing the filters in terms of basis functions. @@ -79,29 +97,50 @@ def regress_filter( Parameters ---------- - coupling_filter_bank: - The coupling filters. Shape (n_neurons, n_neurons, window_size) - basis: - The basis function to instantiate. + coupling_filters: + The coupling filters. Shape (window_size, n_neurons_receiver, n_neurons_sender) + eval_basis: + The evaluated basis function, shape (window_size, n_basis_funcs) Returns ------- - eval_basis: - The basis matrix, shape (window_size, n_basis_funcs) weights: - The weights for each neuron. Shape (n_neurons, n_neurons, n_basis_funcs) + The weights for each neuron. Shape (n_neurons_receiver, n_neurons_sender, n_basis_funcs) """ - n_neurons, _, ws = coupling_filter_bank.shape - eval_basis = basis.evaluate(np.linspace(0, 1, ws)) - - # Reshape the coupling_filter_bank for vectorized least-squares - filters_reshaped = coupling_filter_bank.reshape(-1, ws) + # check shapes + if eval_basis.ndim != 2: + raise ValueError( + "eval_basis must be a 2 dimensional array, " + "shape (window_size, n_basis_funcs). " + f"{eval_basis.ndim} dimensional array provided instead!" + ) + if coupling_filters.ndim != 3: + raise ValueError( + "coupling_filters must be a 3 dimensional array, " + "shape (window_size, n_neurons, n_neurons). " + f"{coupling_filters.ndim} dimensional array provided instead!" + ) + + ws, n_neurons_receiver, n_neurons_sender = coupling_filters.shape + + # check that window size matches + if eval_basis.shape[0] != ws: + raise ValueError( + "window_size mismatch. The window size of coupling_filters and eval_basis " + f"does not match. coupling_filters has a window size of {ws}; " + f"eval_basis has a window size of {eval_basis.shape[0]}." + ) + + # Reshape the coupling_filters for vectorized least-squares + filters_reshaped = coupling_filters.reshape(ws, -1) # Solve the least squares problem for all filters at once # (vecotrizing the features) - weights = np.linalg.lstsq(eval_basis, filters_reshaped.T, rcond=None)[0] + weights = np.linalg.lstsq(eval_basis, filters_reshaped, rcond=None)[0] # Reshape back to the original dimensions - weights = weights.T.reshape(n_neurons, n_neurons, -1) + weights = np.transpose( + weights.reshape(-1, n_neurons_receiver, n_neurons_sender), axes=(1, 2, 0) + ) - return eval_basis, weights + return weights diff --git a/tests/test_simulation.py b/tests/test_simulation.py new file mode 100644 index 00000000..5f8fba51 --- /dev/null +++ b/tests/test_simulation.py @@ -0,0 +1,147 @@ +import itertools +from contextlib import nullcontext as does_not_raise + +import numpy as np +import pytest + +import nemos.simulation as simulation + + +@pytest.mark.parametrize( + "inhib_a, expectation", + [ + (-1, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (0, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (1, does_not_raise()), + ], + ) +def test_difference_of_gammas_inhib_a(inhib_a, expectation): + with expectation: + simulation.difference_of_gammas(10, inhib_a=inhib_a) + + +@pytest.mark.parametrize( + "excit_a, expectation", + [ + (-1, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (0, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (1, does_not_raise()), + ], + ) +def test_difference_of_gammas_excit_a(excit_a, expectation): + with expectation: + simulation.difference_of_gammas(10, excit_a=excit_a) + + +@pytest.mark.parametrize( + "inhib_b, expectation", + [ + (-1, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (0, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (1, does_not_raise()), + ], + ) +def test_difference_of_gammas_excit_a(inhib_b, expectation): + with expectation: + simulation.difference_of_gammas(10, inhib_b=inhib_b) + + +@pytest.mark.parametrize( + "excit_b, expectation", + [ + (-1, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (0, pytest.raises(ValueError, match="Gamma parameter [a-z]+_[a,b] must be >0.")), + (1, does_not_raise()), + ], + ) +def test_difference_of_gammas_excit_a(excit_b, expectation): + with expectation: + simulation.difference_of_gammas(10, excit_b=excit_b) + + +@pytest.mark.parametrize( + "upper_percentile, expectation", + [ + (-0.1, pytest.raises(ValueError, match=r"upper_percentile should lie in the \[0, 1\) interval.")), + (0, does_not_raise()), + (0.1, does_not_raise()), + (1, pytest.raises(ValueError, match=r"upper_percentile should lie in the \[0, 1\) interval.")), + (10, pytest.raises(ValueError, match=r"upper_percentile should lie in the \[0, 1\) interval.")), + ], + ) +def test_difference_of_gammas_percentile_params(upper_percentile, expectation): + with expectation: + simulation.difference_of_gammas(10, upper_percentile) + + +@pytest.mark.parametrize("window_size", [0, 1, 2]) +def test_difference_of_gammas_output_shape(window_size): + result_size = simulation.difference_of_gammas(window_size).size + assert result_size == window_size, f"Expected output size {window_size}, but got {result_size}" + + +@pytest.mark.parametrize("window_size", [1, 2, 10]) +def test_difference_of_gammas_output_norm(window_size): + result = simulation.difference_of_gammas(window_size) + assert np.allclose(np.linalg.norm(result, ord=2),1), "The output of difference_of_gammas is not unit norm." + + +@pytest.mark.parametrize( + "coupling_filters, expectation", + [ + (np.zeros((10, )), pytest.raises(ValueError, match=r"coupling_filters must be a 3 dimensional array")), + (np.zeros((10, 2)), pytest.raises(ValueError, match=r"coupling_filters must be a 3 dimensional array")), + (np.zeros((10, 2, 2)), does_not_raise()), + (np.zeros((10, 2, 2, 2)), pytest.raises(ValueError, match=r"coupling_filters must be a 3 dimensional array")) + ], + ) +def test_regress_filter_coupling_filters_dim(coupling_filters, expectation): + ws = coupling_filters.shape[0] + with expectation: + simulation.regress_filter(coupling_filters, np.zeros((ws, 3))) + + +@pytest.mark.parametrize( + "eval_basis, expectation", + [ + (np.zeros((10, )), pytest.raises(ValueError, match=r"eval_basis must be a 2 dimensional array")), + (np.zeros((10, 2)), does_not_raise()), + (np.zeros((10, 2, 2)), pytest.raises(ValueError, match=r"eval_basis must be a 2 dimensional array")), + (np.zeros((10, 2, 2, 2)), pytest.raises(ValueError, match=r"eval_basis must be a 2 dimensional array")) + ], + ) +def test_regress_filter_eval_basis_dim(eval_basis, expectation): + ws = eval_basis.shape[0] + with expectation: + simulation.regress_filter(np.zeros((ws, 1, 1)), eval_basis) + + +@pytest.mark.parametrize( + "delta_ws, expectation", + [ + (-1, pytest.raises(ValueError, match=r"window_size mismatch\. The window size of ")), + (0, does_not_raise()), + (1, pytest.raises(ValueError, match=r"window_size mismatch\. The window size of ")), + ], + ) +def test_regress_filter_window_size_matching(delta_ws, expectation): + ws = 2 + with expectation: + simulation.regress_filter(np.zeros((ws, 1, 1)), np.zeros((ws + delta_ws, 1))) + + +@pytest.mark.parametrize( + "window_size, n_neurons_sender, n_neurons_receiver, n_basis_funcs", + [x for x in itertools.product([1, 2], [1, 2], [1, 2], [1, 2])], + ) +def test_regress_filter_weights_size(window_size, n_neurons_sender, n_neurons_receiver, n_basis_funcs): + weights = simulation.regress_filter( + np.zeros((window_size, n_neurons_sender, n_neurons_receiver)), + np.zeros((window_size, n_basis_funcs)) + ) + assert weights.shape[0] == n_neurons_sender, (f"First dimension of weights (n_neurons_receiver) does not " + f"match the second dimension of coupling_filters.") + assert weights.shape[1] == n_neurons_receiver, (f"Second dimension of weights (n_neuron_sender) does not " + f"match the third dimension of coupling_filters.") + assert weights.shape[2] == n_basis_funcs, (f"Third dimension of weights (n_basis_funcs) does not " + f"match the second dimension of eval_basis.") From 46092ed435b67881ae669db0b9ded4e7f9d4c9e3 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 14 Dec 2023 11:07:48 -0500 Subject: [PATCH 245/250] fixed test by removing dep on json --- tests/conftest.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1499b80b..2bfa9e0a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -65,8 +65,21 @@ def poissonGLM_coupled_model_config_simulate(): ) n_neurons, coupling_duration, sim_duration = 2, 100, 1000 - coupling_basis, basis_coeff, intercept = nmo.simulation.define_coupling_filters(n_neurons, coupling_duration) - model.coef_ = jnp.hstack([basis_coeff, 0.5 * jnp.ones((n_neurons, n_neurons))]) + coupling_filter_bank = np.zeros((coupling_duration, n_neurons, n_neurons)) + for unit_i in range(n_neurons): + for unit_j in range(n_neurons): + coupling_filter_bank[:, unit_i, unit_j] = nmo.simulation.difference_of_gammas( + coupling_duration + ) + # shrink the filters for simulation stability + coupling_filter_bank *= 0.8 + basis = nmo.basis.RaisedCosineBasisLog(20) + + # approximate the coupling filters in terms of the basis function + _, coupling_basis = basis.evaluate_on_grid(coupling_filter_bank.shape[0]) + coupling_coeff = nmo.simulation.regress_filter(coupling_filter_bank, coupling_basis) + + model.coef_ = jnp.hstack((coupling_coeff.reshape(n_neurons, -1), np.ones((n_neurons, 2)))) model.intercept_ = -3 * jnp.ones(n_neurons) feedforward_input = jnp.c_[ jnp.cos(jnp.linspace(0, np.pi*4, sim_duration)), From 2a815b4d0f0f5ebaed812c947867fcd6fab24e57 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 14 Dec 2023 11:25:02 -0500 Subject: [PATCH 246/250] added test of correctness for the lsq --- tests/test_simulation.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_simulation.py b/tests/test_simulation.py index 5f8fba51..53d8ff42 100644 --- a/tests/test_simulation.py +++ b/tests/test_simulation.py @@ -4,6 +4,7 @@ import numpy as np import pytest +import nemos.basis as basis import nemos.simulation as simulation @@ -145,3 +146,24 @@ def test_regress_filter_weights_size(window_size, n_neurons_sender, n_neurons_re f"match the third dimension of coupling_filters.") assert weights.shape[2] == n_basis_funcs, (f"Third dimension of weights (n_basis_funcs) does not " f"match the second dimension of eval_basis.") + + +def test_least_square_correctness(): + """ + Test the correctness of the least square estimate by enforcing an invertible map, + i.e. a map for which the least-square estimator matches the original weights. + """ + # set up problem dimensionality + ws, n_neurons_receiver, n_neurons_sender, n_basis_funcs = 100, 1, 2, 10 + # evaluate a basis + _, eval_basis = basis.RaisedCosineBasisLog(n_basis_funcs).evaluate_on_grid(ws) + # generate random weights to define filters + weights = np.random.normal(size=(n_neurons_receiver, n_neurons_sender, n_basis_funcs)) + # define filters as linear combination of basis elements + coupling_filt = np.einsum("ijk, tk -> tij", weights, eval_basis) + # recover weights by means of linear regression + weights_lsq = simulation.regress_filter(coupling_filt, eval_basis) + # check the exact matching of the filters up to numerical error + assert np.allclose(weights_lsq, weights) + + From 58a5aba073d3bb5b6d9474655415715a70fd3446 Mon Sep 17 00:00:00 2001 From: BalzaniEdoardo Date: Thu, 14 Dec 2023 11:48:42 -0500 Subject: [PATCH 247/250] added raises --- src/nemos/simulation.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/nemos/simulation.py b/src/nemos/simulation.py index 5b4bd028..423dbe09 100644 --- a/src/nemos/simulation.py +++ b/src/nemos/simulation.py @@ -106,6 +106,13 @@ def regress_filter(coupling_filters: NDArray, eval_basis: NDArray) -> NDArray: ------- weights: The weights for each neuron. Shape (n_neurons_receiver, n_neurons_sender, n_basis_funcs) + + Raises + ------ + ValueError + - If eval_basis is not two-dimensional + - If coupling_filters is not three-dimensional + - If window_size differs between eval_basis and coupling_filters """ # check shapes if eval_basis.ndim != 2: From 0d1207ccda2232d18c9d1264f184579cd501f43a Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Thu, 14 Dec 2023 12:16:58 -0500 Subject: [PATCH 248/250] Update src/nemos/simulation.py Co-authored-by: William F. Broderick --- src/nemos/simulation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/nemos/simulation.py b/src/nemos/simulation.py index 5b4bd028..bb6d43a0 100644 --- a/src/nemos/simulation.py +++ b/src/nemos/simulation.py @@ -93,7 +93,6 @@ def regress_filter(coupling_filters: NDArray, eval_basis: NDArray) -> NDArray: """Approximate scipy.stats.gamma based filters with basis function. Find the ols weights for representing the filters in terms of basis functions. - This is done to re-use the nsl.glm.simulate method. Parameters ---------- From c2dd9489c3cdd1b9bdfbae7156e26a40084ba068 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Thu, 14 Dec 2023 12:17:09 -0500 Subject: [PATCH 249/250] Update src/nemos/simulation.py Co-authored-by: William F. Broderick --- src/nemos/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/simulation.py b/src/nemos/simulation.py index bb6d43a0..833010b7 100644 --- a/src/nemos/simulation.py +++ b/src/nemos/simulation.py @@ -92,7 +92,7 @@ def difference_of_gammas( def regress_filter(coupling_filters: NDArray, eval_basis: NDArray) -> NDArray: """Approximate scipy.stats.gamma based filters with basis function. - Find the ols weights for representing the filters in terms of basis functions. + Find the Ordinary Least Squares weights for representing the filters in terms of basis functions. Parameters ---------- From 3034922def79bbfcddbc8af0994e8546544842a2 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Thu, 14 Dec 2023 12:17:19 -0500 Subject: [PATCH 250/250] Update src/nemos/simulation.py Co-authored-by: William F. Broderick --- src/nemos/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nemos/simulation.py b/src/nemos/simulation.py index 833010b7..e6a78cd9 100644 --- a/src/nemos/simulation.py +++ b/src/nemos/simulation.py @@ -39,7 +39,7 @@ def difference_of_gammas( $$ p(x;\; a, b) = \frac{b^a x^{a-1} e^{-x}}{\Gamma(a)}, $$ - where $\Gamma(a)$ refers to teh gamma function, see$^1$. + where $\Gamma(a)$ refers to the gamma function, see$^1$. Returns -------