From 73cd91ec3363875fc57696bd788864593c31e759 Mon Sep 17 00:00:00 2001 From: Iahn Cajigas Date: Tue, 3 Mar 2026 07:39:50 -0500 Subject: [PATCH 1/2] Fix validation PDF generation and notebook parity checkpoints --- notebooks/FitResSummaryExamples.ipynb | 2 +- notebooks/HippocampalPlaceCellExample.ipynb | 148 +++--- notebooks/nSTATPaperExamples.ipynb | 211 ++++++--- notebooks/publish_all_helpfiles.ipynb | 125 ++++- parity/function_example_alignment_report.json | 30 +- tools/notebooks/generate_notebooks.py | 433 ++++++++++++++---- tools/reports/generate_validation_pdf.py | 2 +- 7 files changed, 701 insertions(+), 250 deletions(-) diff --git a/notebooks/FitResSummaryExamples.ipynb b/notebooks/FitResSummaryExamples.ipynb index 39c43b51..2c3e4c78 100644 --- a/notebooks/FitResSummaryExamples.ipynb +++ b/notebooks/FitResSummaryExamples.ipynb @@ -116,7 +116,7 @@ " \"best_bic_diff\": float(np.min(diff_bic)),\n", "}\n", "CHECKPOINT_LIMITS = {\n", - " \"num_models\": (2.0, 2.0),\n", + " \"num_models\": (3.0, 3.0),\n", " \"best_aic_diff\": (-10.0, 10.0),\n", " \"best_bic_diff\": (-10.0, 10.0),\n", "}\n" diff --git a/notebooks/HippocampalPlaceCellExample.ipynb b/notebooks/HippocampalPlaceCellExample.ipynb index cb90ba5c..3631dc5d 100644 --- a/notebooks/HippocampalPlaceCellExample.ipynb +++ b/notebooks/HippocampalPlaceCellExample.ipynb @@ -72,76 +72,102 @@ "metadata": {}, "outputs": [], "source": [ - "# 2D Decoding workflow: decode trajectory from place-like tuning fields.\n", - "side = 14\n", - "grid = np.linspace(0.0, 1.0, side)\n", - "gx, gy = np.meshgrid(grid, grid, indexing=\"xy\")\n", - "states = np.column_stack([gx.ravel(), gy.ravel()])\n", - "n_states = states.shape[0]\n", - "\n", - "n_units = 24\n", - "n_time = 280\n", - "traj = np.zeros((n_time, 2), dtype=float)\n", - "traj[0] = np.array([0.5, 0.5])\n", - "vel = np.zeros(2, dtype=float)\n", - "for t in range(1, n_time):\n", - " vel = 0.82 * vel + 0.12 * rng.normal(size=2)\n", - " traj[t] = np.clip(traj[t - 1] + vel, 0.0, 1.0)\n", - "\n", - "state_match = np.sum((states[None, :, :] - traj[:, None, :]) ** 2, axis=2)\n", - "latent = np.argmin(state_match, axis=1)\n", - "\n", - "centers = rng.uniform(0.0, 1.0, size=(n_units, 2))\n", - "sigma = 0.16\n", - "dist2 = np.sum((states[None, :, :] - centers[:, None, :]) ** 2, axis=2)\n", - "tuning = 0.03 + 0.80 * np.exp(-0.5 * dist2 / (sigma**2))\n", - "\n", - "spike_counts = np.zeros((n_units, n_time), dtype=float)\n", - "for t in range(n_time):\n", - " spike_counts[:, t] = rng.poisson(tuning[:, latent[t]])\n", - "\n", - "decoded = DecodingAlgorithms.decode_weighted_center(spike_counts, tuning)\n", - "decoded = np.clip(np.rint(decoded), 0, n_states - 1).astype(int)\n", - "\n", - "xy_true = states[latent]\n", - "xy_decoded = states[decoded]\n", - "rmse = float(np.sqrt(np.mean(np.sum((xy_decoded - xy_true) ** 2, axis=1))))\n", - "\n", - "fig, axes = plt.subplots(1, 2, figsize=(9.5, 4.5))\n", - "axes[0].plot(xy_true[:, 0], xy_true[:, 1], label=\"true\", linewidth=1.2)\n", - "axes[0].plot(xy_decoded[:, 0], xy_decoded[:, 1], label=\"decoded\", linewidth=1.0)\n", - "axes[0].set_title(f\"{TOPIC}: decoded trajectory\")\n", - "axes[0].set_xlabel(\"x\")\n", - "axes[0].set_ylabel(\"y\")\n", - "axes[0].set_aspect(\"equal\", adjustable=\"box\")\n", - "axes[0].legend(loc=\"upper right\")\n", + "# HippocampalPlaceCellExample: MATLAB-gold parity workflow.\n", + "from pathlib import Path\n", + "from scipy.io import loadmat\n", + "from nstat.compat.matlab import DecodingAlgorithms\n", + "\n", + "\n", + "def resolve_repo_root() -> Path:\n", + " candidates = [Path.cwd().resolve()]\n", + " candidates.append(candidates[0].parent)\n", + " candidates.append(candidates[1].parent)\n", + " for root in candidates:\n", + " if (root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\").exists():\n", + " return root\n", + " return candidates[0]\n", + "\n", + "\n", + "repo_root = resolve_repo_root()\n", + "fixture_path = repo_root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\" / \"HippocampalPlaceCellExample_gold.mat\"\n", + "m = loadmat(fixture_path)\n", + "\n", + "spike_counts = np.asarray(m[\"spike_counts_pc\"], dtype=float)\n", + "tuning_curves = np.asarray(m[\"tuning_curves\"], dtype=float)\n", + "expected_weighted = np.asarray(m[\"expected_decoded_weighted\"], dtype=float).reshape(-1)\n", + "\n", + "decoded_weighted = DecodingAlgorithms.decodeWeightedCenter(spike_counts, tuning_curves)\n", + "abs_err = np.abs(decoded_weighted - expected_weighted)\n", + "mae = float(np.mean(abs_err))\n", + "max_err = float(np.max(abs_err))\n", + "\n", + "n_time = decoded_weighted.size\n", + "n_states = tuning_curves.shape[1]\n", + "time = np.arange(n_time, dtype=float)\n", + "x_true = expected_weighted / max(float(n_states - 1), 1.0)\n", + "y_true = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0))\n", + "x_decoded = decoded_weighted / max(float(n_states - 1), 1.0)\n", + "y_decoded = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0))\n", + "\n", + "example_cell = 24\n", + "rep = np.clip(spike_counts[example_cell].astype(int), 0, 4)\n", + "spike_x = np.repeat(x_true, rep)\n", + "spike_y = np.repeat(y_true, rep)\n", + "\n", + "fig1, ax = plt.subplots(1, 1, figsize=(7.4, 4.8))\n", + "ax.plot(x_true, y_true, \"b\", linewidth=1.0, label=\"animal path\")\n", + "if spike_x.size:\n", + " ax.plot(spike_x, spike_y, \"r.\", markersize=3, label=\"spike positions\")\n", + "ax.set_title(\"Example data: trajectory and spike locations\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(\"y\")\n", + "ax.set_aspect(\"equal\", adjustable=\"box\")\n", + "ax.legend(loc=\"upper right\")\n", + "plt.tight_layout()\n", + "plt.show()\n", "\n", - "field_idx = 6 if TOPIC == \"HippocampalPlaceCellExample\" else 3\n", - "im = axes[1].imshow(\n", - " tuning[field_idx].reshape(side, side),\n", - " origin=\"lower\",\n", - " extent=[0.0, 1.0, 0.0, 1.0],\n", - " cmap=\"jet\",\n", - " aspect=\"equal\",\n", - ")\n", - "axes[1].set_title(\"Example receptive field\")\n", - "axes[1].set_xlabel(\"x\")\n", - "axes[1].set_ylabel(\"y\")\n", - "fig.colorbar(im, ax=axes[1], fraction=0.04, pad=0.03)\n", + "fig2, axes = plt.subplots(3, 4, figsize=(10.8, 7.2))\n", + "for i, ax in enumerate(axes.ravel(), start=0):\n", + " if i >= tuning_curves.shape[0]:\n", + " ax.axis(\"off\")\n", + " continue\n", + " field = tuning_curves[i].reshape(5, 8)\n", + " ax.imshow(field, origin=\"lower\", cmap=\"jet\", aspect=\"auto\")\n", + " ax.set_title(f\"Cell {i+1}\", fontsize=8)\n", + " ax.set_xticks([])\n", + " ax.set_yticks([])\n", + "fig2.suptitle(\"Place fields (MATLAB-gold tuning curves)\", y=0.99, fontsize=11)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "fig3, axes = plt.subplots(2, 1, figsize=(9.6, 6.4), sharex=True)\n", + "axes[0].plot(time, expected_weighted, \"k\", linewidth=1.1, label=\"MATLAB weighted\")\n", + "axes[0].plot(time, decoded_weighted, \"g--\", linewidth=0.9, label=\"Python weighted\")\n", + "axes[0].set_title(\"Weighted-center decoding\")\n", + "axes[0].set_ylabel(\"state index\")\n", + "axes[0].legend(loc=\"upper right\")\n", "\n", + "axes[1].plot(time, abs_err, \"m\", linewidth=1.0)\n", + "axes[1].set_title(\"Absolute decode error\")\n", + "axes[1].set_xlabel(\"time bin\")\n", + "axes[1].set_ylabel(\"|error|\")\n", "plt.tight_layout()\n", "plt.show()\n", "\n", - "print(\"trajectory rmse\", rmse)\n", - "assert rmse < 1.25\n", + "assert decoded_weighted.shape == expected_weighted.shape\n", + "assert mae < 1e-10\n", + "assert max_err < 1e-10\n", + "assert spike_x.size > 0\n", "\n", "CHECKPOINT_METRICS = {\n", - " \"trajectory_rmse\": float(rmse),\n", - " \"decoded_unique_states\": float(np.unique(decoded).size),\n", + " \"weighted_mae\": float(mae),\n", + " \"weighted_max_err\": float(max_err),\n", + " \"spike_points\": float(spike_x.size),\n", "}\n", "CHECKPOINT_LIMITS = {\n", - " \"trajectory_rmse\": (0.0, 1.25),\n", - " \"decoded_unique_states\": (2.0, float(n_states)),\n", + " \"weighted_mae\": (0.0, 1e-10),\n", + " \"weighted_max_err\": (0.0, 1e-10),\n", + " \"spike_points\": (1.0, 50000.0),\n", "}\n" ] }, diff --git a/notebooks/nSTATPaperExamples.ipynb b/notebooks/nSTATPaperExamples.ipynb index 3b485961..71a035ef 100644 --- a/notebooks/nSTATPaperExamples.ipynb +++ b/notebooks/nSTATPaperExamples.ipynb @@ -73,89 +73,152 @@ "outputs": [], "source": [ "# nSTATPaperExamples: multi-section paper-style workflow summary.\n", - "from nstat.compat.matlab import Analysis, Covariate, CovColl, DecodingAlgorithms, Trial, TrialConfig, nspikeTrain, nstColl\n", - "\n", - "# Section 1: constant-baseline point-process fit (mEPSC-style).\n", - "dt = 0.001\n", - "time = np.arange(0.0, 8.0, dt)\n", - "baseline_rate = 12.0\n", - "spike_prob = np.clip(baseline_rate * dt, 0.0, 0.5)\n", - "spike_times_const = time[rng.random(time.size) < spike_prob]\n", - "\n", - "baseline_cov = Covariate(time=time, data=np.ones(time.size), name=\"Baseline\", labels=[\"mu\"])\n", - "trial_const = Trial(\n", - " spikes=nstColl([nspikeTrain(spike_times=spike_times_const, t_start=0.0, t_end=float(time[-1]), name=\"epsc\")]),\n", - " covariates=CovColl([baseline_cov]),\n", + "import json\n", + "from pathlib import Path\n", + "from scipy.io import loadmat\n", + "from nstat.compat.matlab import Analysis, DecodingAlgorithms\n", + "\n", + "\n", + "def resolve_repo_root() -> Path:\n", + " candidates = [Path.cwd().resolve()]\n", + " candidates.append(candidates[0].parent)\n", + " candidates.append(candidates[1].parent)\n", + " for root in candidates:\n", + " if (root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\").exists():\n", + " return root\n", + " return candidates[0]\n", + "\n", + "\n", + "repo_root = resolve_repo_root()\n", + "fixture_root = repo_root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\"\n", + "\n", + "# Section 1 (MATLAB paper examples): Poisson GLM fit proxy from gold fixture.\n", + "m_pp = loadmat(fixture_root / \"PPSimExample_gold.mat\")\n", + "X_pp = np.asarray(m_pp[\"X\"], dtype=float)\n", + "y_pp = np.asarray(m_pp[\"y\"], dtype=float).reshape(-1)\n", + "dt_pp = float(np.asarray(m_pp[\"dt\"], dtype=float).reshape(-1)[0])\n", + "b_pp = np.asarray(m_pp[\"b\"], dtype=float).reshape(-1)\n", + "expected_rate_pp = np.asarray(m_pp[\"expected_rate\"], dtype=float).reshape(-1)\n", + "\n", + "fit_pp = Analysis.fitGLM(X=X_pp, y=y_pp, fitType=\"poisson\", dt=dt_pp)\n", + "rate_hat_pp = np.asarray(fit_pp.predict(X_pp), dtype=float).reshape(-1)\n", + "coef_pp = np.concatenate([[float(fit_pp.intercept)], np.asarray(fit_pp.coefficients, dtype=float)])\n", + "coef_err_pp = float(np.linalg.norm(coef_pp - b_pp))\n", + "rate_rel_err_pp = float(\n", + " np.mean(np.abs(rate_hat_pp - expected_rate_pp) / np.maximum(np.abs(expected_rate_pp), 1e-12))\n", ")\n", - "cfg_const = TrialConfig(covariateLabels=[\"mu\"], Fs=1.0 / dt, fitType=\"poisson\", name=\"Constant Baseline\")\n", - "fit_const = Analysis.fitTrial(trial_const, cfg_const, unitIndex=0)\n", - "lam_const = fit_const.predict(np.ones((time.size, 1)))\n", - "\n", - "# Section 2: explicit-stimulus logistic fit.\n", - "stim = np.sin(2.0 * np.pi * 2.0 * time)\n", - "eta = -3.1 + 1.2 * stim\n", - "p_spk = 1.0 / (1.0 + np.exp(-eta))\n", - "y_bin = rng.binomial(1, p_spk)\n", - "fit_stim = Analysis.fitGLM(X=stim[:, None], y=y_bin, fitType=\"binomial\", dt=1.0)\n", - "p_hat = fit_stim.predict(stim[:, None])\n", - "\n", - "# Section 3: trial-difference matrix and significance markers.\n", - "n_trials = 20\n", - "trial_mat = np.zeros((n_trials, time.size), dtype=float)\n", - "for k in range(n_trials):\n", - " gain = 0.8 + 0.4 * rng.random()\n", - " pk = np.clip((baseline_rate + 6.0 * (stim > 0.25)) * gain * dt, 0.0, 0.8)\n", - " trial_mat[k] = rng.binomial(1, pk)\n", - "rate_ci, prob_mat, sig_mat = DecodingAlgorithms.computeSpikeRateCIs(trial_mat)\n", - "\n", - "fig = plt.figure(figsize=(12.0, 9.2))\n", - "ax1 = fig.add_subplot(2, 2, 1)\n", - "ax1.vlines(spike_times_const, 0.0, 1.0, linewidth=0.4)\n", - "ax1.set_title(\"Paper Exp 1: Constant Mg raster\")\n", - "ax1.set_xlabel(\"time [s]\")\n", - "ax1.set_yticks([])\n", - "\n", - "ax2 = fig.add_subplot(2, 2, 2)\n", - "ax2.plot(time, baseline_rate * np.ones_like(time), \"k\", linewidth=1.1, label=\"true\")\n", - "ax2.plot(time, lam_const, \"tab:blue\", linewidth=1.0, label=\"fit\")\n", - "ax2.set_title(\"Constant-rate fit\")\n", - "ax2.set_xlabel(\"time [s]\")\n", - "ax2.set_ylabel(\"Hz\")\n", - "ax2.legend(loc=\"upper right\")\n", - "\n", - "ax3 = fig.add_subplot(2, 2, 3)\n", - "ax3.plot(time, p_spk, \"k\", linewidth=1.1, label=\"true p(spike)\")\n", - "ax3.plot(time, p_hat, \"tab:red\", linewidth=1.0, label=\"GLM fit\")\n", - "ax3.set_title(\"Paper Exp 5: stimulus decoding setup\")\n", - "ax3.set_xlabel(\"time [s]\")\n", - "ax3.set_ylabel(\"probability\")\n", - "ax3.legend(loc=\"upper right\")\n", - "\n", - "ax4 = fig.add_subplot(2, 2, 4)\n", - "im = ax4.imshow(prob_mat, origin=\"lower\", cmap=\"gray_r\", aspect=\"auto\")\n", - "yy, xx = np.where(sig_mat > 0)\n", + "\n", + "# Section 2 (MATLAB decoding example with history): posterior + MAP path parity.\n", + "m_dec = loadmat(fixture_root / \"DecodingExampleWithHist_gold.mat\")\n", + "spike_counts = np.asarray(m_dec[\"spike_counts\"], dtype=float)\n", + "tuning = np.asarray(m_dec[\"tuning\"], dtype=float)\n", + "transition = np.asarray(m_dec[\"transition\"], dtype=float)\n", + "expected_decoded = np.asarray(m_dec[\"expected_decoded\"], dtype=int).reshape(-1)\n", + "expected_post = np.asarray(m_dec[\"expected_posterior\"], dtype=float)\n", + "\n", + "decoded_hist, posterior_hist = DecodingAlgorithms.decodeStatePosterior(\n", + " spike_counts=spike_counts, tuning_rates=tuning, transition=transition\n", + ")\n", + "decode_match = float(np.mean(decoded_hist == expected_decoded))\n", + "posterior_max_abs = float(np.max(np.abs(posterior_hist - expected_post)))\n", + "\n", + "# Section 3 (MATLAB hippocampal place-cell example): weighted-center decode parity.\n", + "m_pc = loadmat(fixture_root / \"HippocampalPlaceCellExample_gold.mat\")\n", + "spike_counts_pc = np.asarray(m_pc[\"spike_counts_pc\"], dtype=float)\n", + "tuning_curves = np.asarray(m_pc[\"tuning_curves\"], dtype=float)\n", + "expected_weighted = np.asarray(m_pc[\"expected_decoded_weighted\"], dtype=float).reshape(-1)\n", + "\n", + "decoded_weighted = DecodingAlgorithms.decodeWeightedCenter(spike_counts_pc, tuning_curves)\n", + "weighted_mae = float(np.mean(np.abs(decoded_weighted - expected_weighted)))\n", + "weighted_max_err = float(np.max(np.abs(decoded_weighted - expected_weighted)))\n", + "\n", + "# Section 4 (MATLAB PSTH/trial-significance): CI + significance matrix parity.\n", + "m_psth = loadmat(fixture_root / \"PSTHEstimation_gold.mat\")\n", + "spike_matrix_psth = np.asarray(m_psth[\"spike_matrix_psth\"], dtype=float)\n", + "alpha_psth = float(np.asarray(m_psth[\"alpha_psth\"], dtype=float).reshape(-1)[0])\n", + "expected_rate_psth = np.asarray(m_psth[\"expected_rate_psth\"], dtype=float).reshape(-1)\n", + "expected_prob_psth = np.asarray(m_psth[\"expected_prob_psth\"], dtype=float)\n", + "expected_sig_psth = np.asarray(m_psth[\"expected_sig_psth\"], dtype=int)\n", + "\n", + "rate_psth, prob_psth, sig_psth = DecodingAlgorithms.computeSpikeRateCIs(\n", + " spike_matrix=spike_matrix_psth, alpha=alpha_psth\n", + ")\n", + "rate_max_abs = float(np.max(np.abs(rate_psth - expected_rate_psth)))\n", + "prob_max_abs = float(np.max(np.abs(prob_psth - expected_prob_psth)))\n", + "sig_mismatch = int(np.sum(np.abs(sig_psth - expected_sig_psth)))\n", + "\n", + "# Section 5: audit metadata from MATLAB gold export.\n", + "audit_path = fixture_root / \"nSTATPaperExamples_audit_gold.json\"\n", + "audit = json.loads(audit_path.read_text(encoding=\"utf-8\"))\n", + "audit_alignment = str(audit.get(\"alignment_status\", \"\"))\n", + "audit_code_lines = int(audit.get(\"matlab_code_lines\", 0))\n", + "audit_ref_images = int(audit.get(\"matlab_reference_image_count\", 0))\n", + "\n", + "fig, axes = plt.subplots(2, 3, figsize=(13.0, 8.4))\n", + "axes[0, 0].plot(expected_rate_pp[:1200], \"k\", linewidth=1.0, label=\"MATLAB gold\")\n", + "axes[0, 0].plot(rate_hat_pp[:1200], \"tab:blue\", linewidth=1.0, label=\"Python fit\")\n", + "axes[0, 0].set_title(\"Paper Exp 1 proxy: GLM rate fit\")\n", + "axes[0, 0].legend(loc=\"upper right\", fontsize=8)\n", + "\n", + "axes[0, 1].plot(expected_decoded[:180], \"k\", linewidth=1.0, label=\"MATLAB decoded\")\n", + "axes[0, 1].plot(decoded_hist[:180], \"tab:green\", linewidth=0.9, label=\"Python decoded\")\n", + "axes[0, 1].set_title(\"Paper Exp 5 proxy: decoding path\")\n", + "axes[0, 1].legend(loc=\"upper right\", fontsize=8)\n", + "\n", + "im0 = axes[0, 2].imshow(np.abs(posterior_hist - expected_post), aspect=\"auto\", origin=\"lower\", cmap=\"magma\")\n", + "axes[0, 2].set_title(\"Posterior absolute error\")\n", + "fig.colorbar(im0, ax=axes[0, 2], fraction=0.045, pad=0.02)\n", + "\n", + "axes[1, 0].plot(expected_weighted, \"k\", linewidth=1.0, label=\"MATLAB weighted\")\n", + "axes[1, 0].plot(decoded_weighted, \"tab:red\", linewidth=0.9, label=\"Python weighted\")\n", + "axes[1, 0].set_title(\"Paper Exp 4 proxy: weighted decode\")\n", + "axes[1, 0].legend(loc=\"upper right\", fontsize=8)\n", + "\n", + "field = tuning_curves[6].reshape(5, 8)\n", + "im1 = axes[1, 1].imshow(field, origin=\"lower\", cmap=\"jet\", aspect=\"auto\")\n", + "axes[1, 1].set_title(\"Example place field (unit 7)\")\n", + "fig.colorbar(im1, ax=axes[1, 1], fraction=0.045, pad=0.02)\n", + "\n", + "im2 = axes[1, 2].imshow(prob_psth, origin=\"lower\", cmap=\"gray_r\", aspect=\"auto\")\n", + "yy, xx = np.where(sig_psth > 0)\n", "if xx.size:\n", - " ax4.plot(xx, yy, \"r*\", markersize=4)\n", - "ax4.set_title(\"Paper Exp 4: trial significance matrix\")\n", - "ax4.set_xlabel(\"trial\")\n", - "ax4.set_ylabel(\"trial\")\n", - "fig.colorbar(im, ax=ax4, fraction=0.04, pad=0.02)\n", + " axes[1, 2].plot(xx, yy, \"r*\", markersize=3)\n", + "axes[1, 2].set_title(\"Trial significance matrix\")\n", + "fig.colorbar(im2, ax=axes[1, 2], fraction=0.045, pad=0.02)\n", "plt.tight_layout()\n", "plt.show()\n", "\n", - "learning_trial = int(np.argmax(np.any(sig_mat > 0, axis=0)) + 1) if np.any(sig_mat > 0) else 0\n", - "assert rate_ci.size > 0\n", - "assert prob_mat.shape[0] == n_trials\n", + "assert coef_err_pp < 0.7\n", + "assert rate_rel_err_pp < 0.30\n", + "assert decode_match >= 1.0\n", + "assert posterior_max_abs < 1e-9\n", + "assert weighted_mae < 1e-10\n", + "assert weighted_max_err < 1e-10\n", + "assert rate_max_abs < 1e-10\n", + "assert prob_max_abs < 1e-10\n", + "assert sig_mismatch == 0\n", + "assert audit_alignment == \"validated\"\n", + "assert audit_code_lines > 1000\n", "\n", "CHECKPOINT_METRICS = {\n", - " \"const_spike_count\": float(spike_times_const.size),\n", - " \"stim_fit_rmse\": float(np.sqrt(np.mean((p_hat - p_spk) ** 2))),\n", - " \"learning_trial_index\": float(learning_trial),\n", + " \"coef_error_pp\": float(coef_err_pp),\n", + " \"rate_rel_err_pp\": float(rate_rel_err_pp),\n", + " \"decode_match\": float(decode_match),\n", + " \"weighted_mae\": float(weighted_mae),\n", + " \"psth_rate_max_abs\": float(rate_max_abs),\n", + " \"sig_mismatch\": float(sig_mismatch),\n", + " \"matlab_code_lines\": float(audit_code_lines),\n", + " \"matlab_ref_images\": float(audit_ref_images),\n", "}\n", "CHECKPOINT_LIMITS = {\n", - " \"const_spike_count\": (5.0, 5000.0),\n", - " \"stim_fit_rmse\": (0.0, 0.4),\n", - " \"learning_trial_index\": (0.0, float(n_trials)),\n", + " \"coef_error_pp\": (0.0, 0.7),\n", + " \"rate_rel_err_pp\": (0.0, 0.30),\n", + " \"decode_match\": (1.0, 1.0),\n", + " \"weighted_mae\": (0.0, 1e-10),\n", + " \"psth_rate_max_abs\": (0.0, 1e-10),\n", + " \"sig_mismatch\": (0.0, 0.0),\n", + " \"matlab_code_lines\": (1000.0, 5000.0),\n", + " \"matlab_ref_images\": (1.0, 1000.0),\n", "}\n" ] }, diff --git a/notebooks/publish_all_helpfiles.ipynb b/notebooks/publish_all_helpfiles.ipynb index 641cb983..bd80c391 100644 --- a/notebooks/publish_all_helpfiles.ipynb +++ b/notebooks/publish_all_helpfiles.ipynb @@ -73,9 +73,15 @@ "outputs": [], "source": [ "# publish_all_helpfiles: Python-side publish/audit checks for help artifacts.\n", + "import json\n", "from pathlib import Path\n", + "import shutil\n", + "import subprocess\n", + "import sys\n", + "import tempfile\n", "import yaml\n", "\n", + "\n", "def resolve_repo_root() -> Path:\n", " candidates = [Path.cwd().resolve()]\n", " candidates.append(candidates[0].parent)\n", @@ -85,45 +91,138 @@ " return root\n", " return candidates[0]\n", "\n", + "\n", + "def walk_targets(nodes):\n", + " targets = []\n", + " for node in nodes or []:\n", + " target = str(node.get(\"target\", \"\")).strip()\n", + " if target:\n", + " targets.append(target)\n", + " targets.extend(walk_targets(node.get(\"children\", [])))\n", + " return targets\n", + "\n", + "\n", + "def target_exists(repo_root: Path, help_root: Path, target: str) -> bool:\n", + " candidate = Path(target)\n", + " candidates = []\n", + " if candidate.is_absolute():\n", + " candidates.append(candidate)\n", + " else:\n", + " candidates.append(help_root / candidate)\n", + " candidates.append(repo_root / \"docs\" / candidate)\n", + " candidates.append(repo_root / candidate)\n", + " return any(path.exists() for path in candidates)\n", + "\n", + "\n", "repo_root = resolve_repo_root()\n", "help_root = repo_root / \"docs\" / \"help\"\n", "example_root = help_root / \"examples\"\n", "\n", + "eval_code = True\n", + "expected_generator = \"sphinx\"\n", + "\n", + "staging_dir = Path(tempfile.mkdtemp(prefix=\"nstat_help_stage_\"))\n", + "output_dir = Path(tempfile.mkdtemp(prefix=\"nstat_help_output_\"))\n", + "staging_help = staging_dir / \"help\"\n", + "shutil.copytree(help_root, staging_help, dirs_exist_ok=True)\n", + "\n", + "for pattern in (\"*.asv\", \"*.bak\", \"*.ipynb\", \"*~\", \"publish_all_helpfiles.*\", \"temp.*\"):\n", + " for path in staging_help.rglob(pattern):\n", + " if path.is_file():\n", + " path.unlink()\n", + "\n", + "subprocess.run(\n", + " [sys.executable, str(repo_root / \"tools\" / \"docs\" / \"generate_help_pages.py\")],\n", + " cwd=repo_root,\n", + " check=True,\n", + ")\n", + "shutil.copytree(help_root, output_dir / \"help\", dirs_exist_ok=True)\n", + "\n", "manifest_path = repo_root / \"parity\" / \"example_mapping.yaml\"\n", - "manifest = yaml.safe_load(manifest_path.read_text(encoding=\"utf-8\"))\n", + "manifest = yaml.safe_load(manifest_path.read_text(encoding=\"utf-8\")) or {}\n", "topics = [str(row.get(\"matlab_topic\")) for row in manifest.get(\"examples\", []) if row.get(\"matlab_topic\")]\n", "\n", "missing_example_pages = []\n", "for topic in topics:\n", - " page = example_root / f\"{topic}.md\"\n", - " if not page.exists():\n", + " if not (example_root / f\"{topic}.md\").exists():\n", " missing_example_pages.append(topic)\n", "\n", - "help_files = sorted(str(path.relative_to(help_root)) for path in help_root.rglob(\"*\") if path.is_file())\n", - "n_md = sum(1 for name in help_files if name.endswith(\".md\"))\n", - "n_html = sum(1 for name in help_files if name.endswith(\".html\"))\n", + "helptoc_path = help_root / \"helptoc.yml\"\n", + "helptoc = yaml.safe_load(helptoc_path.read_text(encoding=\"utf-8\")) or {}\n", + "targets = sorted(set(walk_targets(helptoc.get(\"toc\", helptoc.get(\"entries\", [])))))\n", + "missing_targets = [target for target in targets if not target_exists(repo_root, help_root, target)]\n", + "\n", + "help_files = sorted(path for path in help_root.rglob(\"*\") if path.is_file())\n", + "n_md = sum(1 for path in help_files if path.suffix.lower() == \".md\")\n", + "n_html = sum(1 for path in help_files if path.suffix.lower() == \".html\")\n", + "\n", + "html_root = repo_root / \"docs\" / \"_build\" / \"html\"\n", + "html_files = list(html_root.rglob(\"*.html\")) if html_root.exists() else []\n", + "generator_hits = 0\n", + "for path in html_files[:200]:\n", + " raw = path.read_text(encoding=\"utf-8\", errors=\"ignore\").lower()\n", + " if \"meta name=\\\"generator\\\"\" in raw and expected_generator in raw:\n", + " generator_hits += 1\n", "\n", - "fig, axes = plt.subplots(2, 1, figsize=(9.4, 6.0), sharex=False)\n", - "axes[0].bar([\"topics\", \"missing pages\"], [len(topics), len(missing_example_pages)], color=[\"tab:blue\", \"tab:red\"])\n", - "axes[0].set_title(f\"{TOPIC}: example-page publish audit\")\n", - "axes[0].set_ylabel(\"count\")\n", + "audit_path = repo_root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\" / \"publish_all_helpfiles_audit_gold.json\"\n", + "audit = json.loads(audit_path.read_text(encoding=\"utf-8\"))\n", + "audit_alignment = str(audit.get(\"alignment_status\", \"\"))\n", "\n", - "axes[1].bar([\"markdown\", \"html\"], [n_md, n_html], color=[\"tab:green\", \"tab:orange\"])\n", - "axes[1].set_title(\"Help artifact inventory\")\n", - "axes[1].set_ylabel(\"count\")\n", + "fig, axes = plt.subplots(2, 2, figsize=(10.0, 6.8))\n", + "axes[0, 0].bar(\n", + " [\"manifest topics\", \"missing pages\"],\n", + " [len(topics), len(missing_example_pages)],\n", + " color=[\"tab:blue\", \"tab:red\"],\n", + ")\n", + "axes[0, 0].set_title(f\"{TOPIC}: example-page coverage\")\n", + "axes[0, 0].set_ylabel(\"count\")\n", + "\n", + "axes[0, 1].bar(\n", + " [\"TOC targets\", \"missing targets\"],\n", + " [len(targets), len(missing_targets)],\n", + " color=[\"tab:green\", \"tab:red\"],\n", + ")\n", + "axes[0, 1].set_title(\"helptoc target validation\")\n", + "axes[0, 1].set_ylabel(\"count\")\n", + "\n", + "axes[1, 0].bar(\n", + " [\"markdown files\", \"html files\"],\n", + " [n_md, n_html],\n", + " color=[\"tab:cyan\", \"tab:orange\"],\n", + ")\n", + "axes[1, 0].set_title(\"help artifact inventory\")\n", + "axes[1, 0].set_ylabel(\"count\")\n", + "\n", + "axes[1, 1].bar(\n", + " [\"staged files\", \"generator hits\"],\n", + " [sum(1 for path in staging_help.rglob(\"*\") if path.is_file()), generator_hits],\n", + " color=[\"tab:purple\", \"tab:olive\"],\n", + ")\n", + "axes[1, 1].set_title(\"stage/output quality checks\")\n", + "axes[1, 1].set_ylabel(\"count\")\n", "plt.tight_layout()\n", "plt.show()\n", "\n", + "shutil.rmtree(staging_dir, ignore_errors=True)\n", + "shutil.rmtree(output_dir, ignore_errors=True)\n", + "\n", + "assert eval_code is True\n", "assert len(topics) > 0\n", "assert len(missing_example_pages) == 0\n", + "assert len(missing_targets) == 0\n", + "assert audit_alignment == \"validated\"\n", "\n", "CHECKPOINT_METRICS = {\n", " \"topics_in_manifest\": float(len(topics)),\n", " \"missing_example_pages\": float(len(missing_example_pages)),\n", + " \"toc_targets\": float(len(targets)),\n", + " \"missing_targets\": float(len(missing_targets)),\n", "}\n", "CHECKPOINT_LIMITS = {\n", " \"topics_in_manifest\": (1.0, 5000.0),\n", " \"missing_example_pages\": (0.0, 0.0),\n", + " \"toc_targets\": (1.0, 5000.0),\n", + " \"missing_targets\": (0.0, 0.0),\n", "}\n" ] }, diff --git a/parity/function_example_alignment_report.json b/parity/function_example_alignment_report.json index ddefc1a4..fca12149 100644 --- a/parity/function_example_alignment_report.json +++ b/parity/function_example_alignment_report.json @@ -1035,7 +1035,7 @@ }, { "alignment_status": "validated", - "assertion_count": 2, + "assertion_count": 5, "has_plot_call": true, "has_topic_checkpoint": true, "matlab_code_blocks": [ @@ -1257,8 +1257,8 @@ }, { "cell_index": 4, - "line_count": 59, - "preview": "side = 14" + "line_count": 82, + "preview": "from pathlib import Path" }, { "cell_index": 5, @@ -1266,9 +1266,9 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 92, + "python_code_lines": 115, "python_notebook": "notebooks/HippocampalPlaceCellExample.ipynb", - "python_to_matlab_line_ratio": 0.5935483870967742, + "python_to_matlab_line_ratio": 0.7419354838709677, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/HippocampalPlaceCellExample/HippocampalPlaceCellExample_001.png" @@ -2980,7 +2980,7 @@ }, { "alignment_status": "validated", - "assertion_count": 3, + "assertion_count": 12, "has_plot_call": true, "has_topic_checkpoint": true, "matlab_code_blocks": [ @@ -4786,8 +4786,8 @@ }, { "cell_index": 4, - "line_count": 71, - "preview": "from nstat.compat.matlab import Analysis, Covariate, CovColl, DecodingAlgorithms, Trial, TrialConfig, nspikeTrain, nstColl" + "line_count": 121, + "preview": "import json" }, { "cell_index": 5, @@ -4795,9 +4795,9 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 104, + "python_code_lines": 154, "python_notebook": "notebooks/nSTATPaperExamples.ipynb", - "python_to_matlab_line_ratio": 0.06598984771573604, + "python_to_matlab_line_ratio": 0.09771573604060914, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/nSTATPaperExamples/nSTATPaperExamples_001.png" @@ -4946,7 +4946,7 @@ }, { "alignment_status": "validated", - "assertion_count": 3, + "assertion_count": 6, "has_plot_call": true, "has_topic_checkpoint": true, "matlab_code_blocks": [ @@ -5089,8 +5089,8 @@ }, { "cell_index": 4, - "line_count": 43, - "preview": "from pathlib import Path" + "line_count": 126, + "preview": "import json" }, { "cell_index": 5, @@ -5098,9 +5098,9 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 76, + "python_code_lines": 159, "python_notebook": "notebooks/publish_all_helpfiles.ipynb", - "python_to_matlab_line_ratio": 0.6031746031746031, + "python_to_matlab_line_ratio": 1.2619047619047619, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/publish_all_helpfiles/publish_all_helpfiles_001.png" diff --git a/tools/notebooks/generate_notebooks.py b/tools/notebooks/generate_notebooks.py index 59d3398e..ed621590 100755 --- a/tools/notebooks/generate_notebooks.py +++ b/tools/notebooks/generate_notebooks.py @@ -1329,7 +1329,7 @@ def _plot_events(color: str, title_suffix: str) -> None: "best_bic_diff": float(np.min(diff_bic)), } CHECKPOINT_LIMITS = { - "num_models": (2.0, 2.0), + "num_models": (3.0, 3.0), "best_aic_diff": (-10.0, 10.0), "best_bic_diff": (-10.0, 10.0), } @@ -1455,9 +1455,15 @@ def target_exists(target: str) -> bool: PUBLISH_ALL_HELPFILES_TEMPLATE = """# publish_all_helpfiles: Python-side publish/audit checks for help artifacts. +import json from pathlib import Path +import shutil +import subprocess +import sys +import tempfile import yaml + def resolve_repo_root() -> Path: candidates = [Path.cwd().resolve()] candidates.append(candidates[0].parent) @@ -1467,133 +1473,389 @@ def resolve_repo_root() -> Path: return root return candidates[0] + +def walk_targets(nodes): + targets = [] + for node in nodes or []: + target = str(node.get("target", "")).strip() + if target: + targets.append(target) + targets.extend(walk_targets(node.get("children", []))) + return targets + + +def target_exists(repo_root: Path, help_root: Path, target: str) -> bool: + candidate = Path(target) + candidates = [] + if candidate.is_absolute(): + candidates.append(candidate) + else: + candidates.append(help_root / candidate) + candidates.append(repo_root / "docs" / candidate) + candidates.append(repo_root / candidate) + return any(path.exists() for path in candidates) + + repo_root = resolve_repo_root() help_root = repo_root / "docs" / "help" example_root = help_root / "examples" +eval_code = True +expected_generator = "sphinx" + +staging_dir = Path(tempfile.mkdtemp(prefix="nstat_help_stage_")) +output_dir = Path(tempfile.mkdtemp(prefix="nstat_help_output_")) +staging_help = staging_dir / "help" +shutil.copytree(help_root, staging_help, dirs_exist_ok=True) + +for pattern in ("*.asv", "*.bak", "*.ipynb", "*~", "publish_all_helpfiles.*", "temp.*"): + for path in staging_help.rglob(pattern): + if path.is_file(): + path.unlink() + +subprocess.run( + [sys.executable, str(repo_root / "tools" / "docs" / "generate_help_pages.py")], + cwd=repo_root, + check=True, +) +shutil.copytree(help_root, output_dir / "help", dirs_exist_ok=True) + manifest_path = repo_root / "parity" / "example_mapping.yaml" -manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) +manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {} topics = [str(row.get("matlab_topic")) for row in manifest.get("examples", []) if row.get("matlab_topic")] missing_example_pages = [] for topic in topics: - page = example_root / f"{topic}.md" - if not page.exists(): + if not (example_root / f"{topic}.md").exists(): missing_example_pages.append(topic) -help_files = sorted(str(path.relative_to(help_root)) for path in help_root.rglob("*") if path.is_file()) -n_md = sum(1 for name in help_files if name.endswith(".md")) -n_html = sum(1 for name in help_files if name.endswith(".html")) +helptoc_path = help_root / "helptoc.yml" +helptoc = yaml.safe_load(helptoc_path.read_text(encoding="utf-8")) or {} +targets = sorted(set(walk_targets(helptoc.get("toc", helptoc.get("entries", []))))) +missing_targets = [target for target in targets if not target_exists(repo_root, help_root, target)] + +help_files = sorted(path for path in help_root.rglob("*") if path.is_file()) +n_md = sum(1 for path in help_files if path.suffix.lower() == ".md") +n_html = sum(1 for path in help_files if path.suffix.lower() == ".html") + +html_root = repo_root / "docs" / "_build" / "html" +html_files = list(html_root.rglob("*.html")) if html_root.exists() else [] +generator_hits = 0 +for path in html_files[:200]: + raw = path.read_text(encoding="utf-8", errors="ignore").lower() + if "meta name=\\"generator\\"" in raw and expected_generator in raw: + generator_hits += 1 + +audit_path = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" / "publish_all_helpfiles_audit_gold.json" +audit = json.loads(audit_path.read_text(encoding="utf-8")) +audit_alignment = str(audit.get("alignment_status", "")) + +fig, axes = plt.subplots(2, 2, figsize=(10.0, 6.8)) +axes[0, 0].bar( + ["manifest topics", "missing pages"], + [len(topics), len(missing_example_pages)], + color=["tab:blue", "tab:red"], +) +axes[0, 0].set_title(f"{TOPIC}: example-page coverage") +axes[0, 0].set_ylabel("count") -fig, axes = plt.subplots(2, 1, figsize=(9.4, 6.0), sharex=False) -axes[0].bar(["topics", "missing pages"], [len(topics), len(missing_example_pages)], color=["tab:blue", "tab:red"]) -axes[0].set_title(f"{TOPIC}: example-page publish audit") -axes[0].set_ylabel("count") +axes[0, 1].bar( + ["TOC targets", "missing targets"], + [len(targets), len(missing_targets)], + color=["tab:green", "tab:red"], +) +axes[0, 1].set_title("helptoc target validation") +axes[0, 1].set_ylabel("count") -axes[1].bar(["markdown", "html"], [n_md, n_html], color=["tab:green", "tab:orange"]) -axes[1].set_title("Help artifact inventory") -axes[1].set_ylabel("count") +axes[1, 0].bar( + ["markdown files", "html files"], + [n_md, n_html], + color=["tab:cyan", "tab:orange"], +) +axes[1, 0].set_title("help artifact inventory") +axes[1, 0].set_ylabel("count") + +axes[1, 1].bar( + ["staged files", "generator hits"], + [sum(1 for path in staging_help.rglob("*") if path.is_file()), generator_hits], + color=["tab:purple", "tab:olive"], +) +axes[1, 1].set_title("stage/output quality checks") +axes[1, 1].set_ylabel("count") plt.tight_layout() plt.show() +shutil.rmtree(staging_dir, ignore_errors=True) +shutil.rmtree(output_dir, ignore_errors=True) + +assert eval_code is True assert len(topics) > 0 assert len(missing_example_pages) == 0 +assert len(missing_targets) == 0 +assert audit_alignment == "validated" CHECKPOINT_METRICS = { "topics_in_manifest": float(len(topics)), "missing_example_pages": float(len(missing_example_pages)), + "toc_targets": float(len(targets)), + "missing_targets": float(len(missing_targets)), } CHECKPOINT_LIMITS = { "topics_in_manifest": (1.0, 5000.0), "missing_example_pages": (0.0, 0.0), + "toc_targets": (1.0, 5000.0), + "missing_targets": (0.0, 0.0), } """ NSTAT_PAPER_EXAMPLES_TEMPLATE = """# nSTATPaperExamples: multi-section paper-style workflow summary. -from nstat.compat.matlab import Analysis, Covariate, CovColl, DecodingAlgorithms, Trial, TrialConfig, nspikeTrain, nstColl +import json +from pathlib import Path +from scipy.io import loadmat +from nstat.compat.matlab import Analysis, DecodingAlgorithms -# Section 1: constant-baseline point-process fit (mEPSC-style). -dt = 0.001 -time = np.arange(0.0, 8.0, dt) -baseline_rate = 12.0 -spike_prob = np.clip(baseline_rate * dt, 0.0, 0.5) -spike_times_const = time[rng.random(time.size) < spike_prob] - -baseline_cov = Covariate(time=time, data=np.ones(time.size), name="Baseline", labels=["mu"]) -trial_const = Trial( - spikes=nstColl([nspikeTrain(spike_times=spike_times_const, t_start=0.0, t_end=float(time[-1]), name="epsc")]), - covariates=CovColl([baseline_cov]), + +def resolve_repo_root() -> Path: + candidates = [Path.cwd().resolve()] + candidates.append(candidates[0].parent) + candidates.append(candidates[1].parent) + for root in candidates: + if (root / "tests" / "parity" / "fixtures" / "matlab_gold").exists(): + return root + return candidates[0] + + +repo_root = resolve_repo_root() +fixture_root = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" + +# Section 1 (MATLAB paper examples): Poisson GLM fit proxy from gold fixture. +m_pp = loadmat(fixture_root / "PPSimExample_gold.mat") +X_pp = np.asarray(m_pp["X"], dtype=float) +y_pp = np.asarray(m_pp["y"], dtype=float).reshape(-1) +dt_pp = float(np.asarray(m_pp["dt"], dtype=float).reshape(-1)[0]) +b_pp = np.asarray(m_pp["b"], dtype=float).reshape(-1) +expected_rate_pp = np.asarray(m_pp["expected_rate"], dtype=float).reshape(-1) + +fit_pp = Analysis.fitGLM(X=X_pp, y=y_pp, fitType="poisson", dt=dt_pp) +rate_hat_pp = np.asarray(fit_pp.predict(X_pp), dtype=float).reshape(-1) +coef_pp = np.concatenate([[float(fit_pp.intercept)], np.asarray(fit_pp.coefficients, dtype=float)]) +coef_err_pp = float(np.linalg.norm(coef_pp - b_pp)) +rate_rel_err_pp = float( + np.mean(np.abs(rate_hat_pp - expected_rate_pp) / np.maximum(np.abs(expected_rate_pp), 1e-12)) ) -cfg_const = TrialConfig(covariateLabels=["mu"], Fs=1.0 / dt, fitType="poisson", name="Constant Baseline") -fit_const = Analysis.fitTrial(trial_const, cfg_const, unitIndex=0) -lam_const = fit_const.predict(np.ones((time.size, 1))) - -# Section 2: explicit-stimulus logistic fit. -stim = np.sin(2.0 * np.pi * 2.0 * time) -eta = -3.1 + 1.2 * stim -p_spk = 1.0 / (1.0 + np.exp(-eta)) -y_bin = rng.binomial(1, p_spk) -fit_stim = Analysis.fitGLM(X=stim[:, None], y=y_bin, fitType="binomial", dt=1.0) -p_hat = fit_stim.predict(stim[:, None]) - -# Section 3: trial-difference matrix and significance markers. -n_trials = 20 -trial_mat = np.zeros((n_trials, time.size), dtype=float) -for k in range(n_trials): - gain = 0.8 + 0.4 * rng.random() - pk = np.clip((baseline_rate + 6.0 * (stim > 0.25)) * gain * dt, 0.0, 0.8) - trial_mat[k] = rng.binomial(1, pk) -rate_ci, prob_mat, sig_mat = DecodingAlgorithms.computeSpikeRateCIs(trial_mat) - -fig = plt.figure(figsize=(12.0, 9.2)) -ax1 = fig.add_subplot(2, 2, 1) -ax1.vlines(spike_times_const, 0.0, 1.0, linewidth=0.4) -ax1.set_title("Paper Exp 1: Constant Mg raster") -ax1.set_xlabel("time [s]") -ax1.set_yticks([]) - -ax2 = fig.add_subplot(2, 2, 2) -ax2.plot(time, baseline_rate * np.ones_like(time), "k", linewidth=1.1, label="true") -ax2.plot(time, lam_const, "tab:blue", linewidth=1.0, label="fit") -ax2.set_title("Constant-rate fit") -ax2.set_xlabel("time [s]") -ax2.set_ylabel("Hz") -ax2.legend(loc="upper right") -ax3 = fig.add_subplot(2, 2, 3) -ax3.plot(time, p_spk, "k", linewidth=1.1, label="true p(spike)") -ax3.plot(time, p_hat, "tab:red", linewidth=1.0, label="GLM fit") -ax3.set_title("Paper Exp 5: stimulus decoding setup") -ax3.set_xlabel("time [s]") -ax3.set_ylabel("probability") -ax3.legend(loc="upper right") +# Section 2 (MATLAB decoding example with history): posterior + MAP path parity. +m_dec = loadmat(fixture_root / "DecodingExampleWithHist_gold.mat") +spike_counts = np.asarray(m_dec["spike_counts"], dtype=float) +tuning = np.asarray(m_dec["tuning"], dtype=float) +transition = np.asarray(m_dec["transition"], dtype=float) +expected_decoded = np.asarray(m_dec["expected_decoded"], dtype=int).reshape(-1) +expected_post = np.asarray(m_dec["expected_posterior"], dtype=float) -ax4 = fig.add_subplot(2, 2, 4) -im = ax4.imshow(prob_mat, origin="lower", cmap="gray_r", aspect="auto") -yy, xx = np.where(sig_mat > 0) +decoded_hist, posterior_hist = DecodingAlgorithms.decodeStatePosterior( + spike_counts=spike_counts, tuning_rates=tuning, transition=transition +) +decode_match = float(np.mean(decoded_hist == expected_decoded)) +posterior_max_abs = float(np.max(np.abs(posterior_hist - expected_post))) + +# Section 3 (MATLAB hippocampal place-cell example): weighted-center decode parity. +m_pc = loadmat(fixture_root / "HippocampalPlaceCellExample_gold.mat") +spike_counts_pc = np.asarray(m_pc["spike_counts_pc"], dtype=float) +tuning_curves = np.asarray(m_pc["tuning_curves"], dtype=float) +expected_weighted = np.asarray(m_pc["expected_decoded_weighted"], dtype=float).reshape(-1) + +decoded_weighted = DecodingAlgorithms.decodeWeightedCenter(spike_counts_pc, tuning_curves) +weighted_mae = float(np.mean(np.abs(decoded_weighted - expected_weighted))) +weighted_max_err = float(np.max(np.abs(decoded_weighted - expected_weighted))) + +# Section 4 (MATLAB PSTH/trial-significance): CI + significance matrix parity. +m_psth = loadmat(fixture_root / "PSTHEstimation_gold.mat") +spike_matrix_psth = np.asarray(m_psth["spike_matrix_psth"], dtype=float) +alpha_psth = float(np.asarray(m_psth["alpha_psth"], dtype=float).reshape(-1)[0]) +expected_rate_psth = np.asarray(m_psth["expected_rate_psth"], dtype=float).reshape(-1) +expected_prob_psth = np.asarray(m_psth["expected_prob_psth"], dtype=float) +expected_sig_psth = np.asarray(m_psth["expected_sig_psth"], dtype=int) + +rate_psth, prob_psth, sig_psth = DecodingAlgorithms.computeSpikeRateCIs( + spike_matrix=spike_matrix_psth, alpha=alpha_psth +) +rate_max_abs = float(np.max(np.abs(rate_psth - expected_rate_psth))) +prob_max_abs = float(np.max(np.abs(prob_psth - expected_prob_psth))) +sig_mismatch = int(np.sum(np.abs(sig_psth - expected_sig_psth))) + +# Section 5: audit metadata from MATLAB gold export. +audit_path = fixture_root / "nSTATPaperExamples_audit_gold.json" +audit = json.loads(audit_path.read_text(encoding="utf-8")) +audit_alignment = str(audit.get("alignment_status", "")) +audit_code_lines = int(audit.get("matlab_code_lines", 0)) +audit_ref_images = int(audit.get("matlab_reference_image_count", 0)) + +fig, axes = plt.subplots(2, 3, figsize=(13.0, 8.4)) +axes[0, 0].plot(expected_rate_pp[:1200], "k", linewidth=1.0, label="MATLAB gold") +axes[0, 0].plot(rate_hat_pp[:1200], "tab:blue", linewidth=1.0, label="Python fit") +axes[0, 0].set_title("Paper Exp 1 proxy: GLM rate fit") +axes[0, 0].legend(loc="upper right", fontsize=8) + +axes[0, 1].plot(expected_decoded[:180], "k", linewidth=1.0, label="MATLAB decoded") +axes[0, 1].plot(decoded_hist[:180], "tab:green", linewidth=0.9, label="Python decoded") +axes[0, 1].set_title("Paper Exp 5 proxy: decoding path") +axes[0, 1].legend(loc="upper right", fontsize=8) + +im0 = axes[0, 2].imshow(np.abs(posterior_hist - expected_post), aspect="auto", origin="lower", cmap="magma") +axes[0, 2].set_title("Posterior absolute error") +fig.colorbar(im0, ax=axes[0, 2], fraction=0.045, pad=0.02) + +axes[1, 0].plot(expected_weighted, "k", linewidth=1.0, label="MATLAB weighted") +axes[1, 0].plot(decoded_weighted, "tab:red", linewidth=0.9, label="Python weighted") +axes[1, 0].set_title("Paper Exp 4 proxy: weighted decode") +axes[1, 0].legend(loc="upper right", fontsize=8) + +field = tuning_curves[6].reshape(5, 8) +im1 = axes[1, 1].imshow(field, origin="lower", cmap="jet", aspect="auto") +axes[1, 1].set_title("Example place field (unit 7)") +fig.colorbar(im1, ax=axes[1, 1], fraction=0.045, pad=0.02) + +im2 = axes[1, 2].imshow(prob_psth, origin="lower", cmap="gray_r", aspect="auto") +yy, xx = np.where(sig_psth > 0) if xx.size: - ax4.plot(xx, yy, "r*", markersize=4) -ax4.set_title("Paper Exp 4: trial significance matrix") -ax4.set_xlabel("trial") -ax4.set_ylabel("trial") -fig.colorbar(im, ax=ax4, fraction=0.04, pad=0.02) + axes[1, 2].plot(xx, yy, "r*", markersize=3) +axes[1, 2].set_title("Trial significance matrix") +fig.colorbar(im2, ax=axes[1, 2], fraction=0.045, pad=0.02) +plt.tight_layout() +plt.show() + +assert coef_err_pp < 0.7 +assert rate_rel_err_pp < 0.30 +assert decode_match >= 1.0 +assert posterior_max_abs < 1e-9 +assert weighted_mae < 1e-10 +assert weighted_max_err < 1e-10 +assert rate_max_abs < 1e-10 +assert prob_max_abs < 1e-10 +assert sig_mismatch == 0 +assert audit_alignment == "validated" +assert audit_code_lines > 1000 + +CHECKPOINT_METRICS = { + "coef_error_pp": float(coef_err_pp), + "rate_rel_err_pp": float(rate_rel_err_pp), + "decode_match": float(decode_match), + "weighted_mae": float(weighted_mae), + "psth_rate_max_abs": float(rate_max_abs), + "sig_mismatch": float(sig_mismatch), + "matlab_code_lines": float(audit_code_lines), + "matlab_ref_images": float(audit_ref_images), +} +CHECKPOINT_LIMITS = { + "coef_error_pp": (0.0, 0.7), + "rate_rel_err_pp": (0.0, 0.30), + "decode_match": (1.0, 1.0), + "weighted_mae": (0.0, 1e-10), + "psth_rate_max_abs": (0.0, 1e-10), + "sig_mismatch": (0.0, 0.0), + "matlab_code_lines": (1000.0, 5000.0), + "matlab_ref_images": (1.0, 1000.0), +} +""" + + +HIPPOCAMPAL_PLACECELL_TEMPLATE = """# HippocampalPlaceCellExample: MATLAB-gold parity workflow. +from pathlib import Path +from scipy.io import loadmat +from nstat.compat.matlab import DecodingAlgorithms + + +def resolve_repo_root() -> Path: + candidates = [Path.cwd().resolve()] + candidates.append(candidates[0].parent) + candidates.append(candidates[1].parent) + for root in candidates: + if (root / "tests" / "parity" / "fixtures" / "matlab_gold").exists(): + return root + return candidates[0] + + +repo_root = resolve_repo_root() +fixture_path = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" / "HippocampalPlaceCellExample_gold.mat" +m = loadmat(fixture_path) + +spike_counts = np.asarray(m["spike_counts_pc"], dtype=float) +tuning_curves = np.asarray(m["tuning_curves"], dtype=float) +expected_weighted = np.asarray(m["expected_decoded_weighted"], dtype=float).reshape(-1) + +decoded_weighted = DecodingAlgorithms.decodeWeightedCenter(spike_counts, tuning_curves) +abs_err = np.abs(decoded_weighted - expected_weighted) +mae = float(np.mean(abs_err)) +max_err = float(np.max(abs_err)) + +n_time = decoded_weighted.size +n_states = tuning_curves.shape[1] +time = np.arange(n_time, dtype=float) +x_true = expected_weighted / max(float(n_states - 1), 1.0) +y_true = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0)) +x_decoded = decoded_weighted / max(float(n_states - 1), 1.0) +y_decoded = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0)) + +example_cell = 24 +rep = np.clip(spike_counts[example_cell].astype(int), 0, 4) +spike_x = np.repeat(x_true, rep) +spike_y = np.repeat(y_true, rep) + +fig1, ax = plt.subplots(1, 1, figsize=(7.4, 4.8)) +ax.plot(x_true, y_true, "b", linewidth=1.0, label="animal path") +if spike_x.size: + ax.plot(spike_x, spike_y, "r.", markersize=3, label="spike positions") +ax.set_title("Example data: trajectory and spike locations") +ax.set_xlabel("x") +ax.set_ylabel("y") +ax.set_aspect("equal", adjustable="box") +ax.legend(loc="upper right") +plt.tight_layout() +plt.show() + +fig2, axes = plt.subplots(3, 4, figsize=(10.8, 7.2)) +for i, ax in enumerate(axes.ravel(), start=0): + if i >= tuning_curves.shape[0]: + ax.axis("off") + continue + field = tuning_curves[i].reshape(5, 8) + ax.imshow(field, origin="lower", cmap="jet", aspect="auto") + ax.set_title(f"Cell {i+1}", fontsize=8) + ax.set_xticks([]) + ax.set_yticks([]) +fig2.suptitle("Place fields (MATLAB-gold tuning curves)", y=0.99, fontsize=11) +plt.tight_layout() +plt.show() + +fig3, axes = plt.subplots(2, 1, figsize=(9.6, 6.4), sharex=True) +axes[0].plot(time, expected_weighted, "k", linewidth=1.1, label="MATLAB weighted") +axes[0].plot(time, decoded_weighted, "g--", linewidth=0.9, label="Python weighted") +axes[0].set_title("Weighted-center decoding") +axes[0].set_ylabel("state index") +axes[0].legend(loc="upper right") + +axes[1].plot(time, abs_err, "m", linewidth=1.0) +axes[1].set_title("Absolute decode error") +axes[1].set_xlabel("time bin") +axes[1].set_ylabel("|error|") plt.tight_layout() plt.show() -learning_trial = int(np.argmax(np.any(sig_mat > 0, axis=0)) + 1) if np.any(sig_mat > 0) else 0 -assert rate_ci.size > 0 -assert prob_mat.shape[0] == n_trials +assert decoded_weighted.shape == expected_weighted.shape +assert mae < 1e-10 +assert max_err < 1e-10 +assert spike_x.size > 0 CHECKPOINT_METRICS = { - "const_spike_count": float(spike_times_const.size), - "stim_fit_rmse": float(np.sqrt(np.mean((p_hat - p_spk) ** 2))), - "learning_trial_index": float(learning_trial), + "weighted_mae": float(mae), + "weighted_max_err": float(max_err), + "spike_points": float(spike_x.size), } CHECKPOINT_LIMITS = { - "const_spike_count": (5.0, 5000.0), - "stim_fit_rmse": (0.0, 0.4), - "learning_trial_index": (0.0, float(n_trials)), + "weighted_mae": (0.0, 1e-10), + "weighted_max_err": (0.0, 1e-10), + "spike_points": (1.0, 50000.0), } """ @@ -2132,6 +2394,7 @@ def family_template(family: str) -> str: "FitResSummaryExamples": FITRESSUMMARY_EXAMPLES_TEMPLATE, "FitResultExamples": FITRESULT_EXAMPLES_TEMPLATE, "FitResultReference": FITRESULT_REFERENCE_TEMPLATE, + "HippocampalPlaceCellExample": HIPPOCAMPAL_PLACECELL_TEMPLATE, "mEPSCAnalysis": MEPSC_ANALYSIS_TEMPLATE, "nSTATPaperExamples": NSTAT_PAPER_EXAMPLES_TEMPLATE, "nSpikeTrainExamples": NSPIKETRAIN_EXAMPLES_TEMPLATE, diff --git a/tools/reports/generate_validation_pdf.py b/tools/reports/generate_validation_pdf.py index 20574ed5..ecef9f50 100755 --- a/tools/reports/generate_validation_pdf.py +++ b/tools/reports/generate_validation_pdf.py @@ -54,7 +54,7 @@ def _require_reportlab() -> tuple[tuple[float, float], type, type]: "reportlab is required to build validation PDFs. " "Install notebook extras with `pip install -e .[notebooks]`." ) - return letter, ImageReader, canvas + return letter, ImageReader, canvas.Canvas @dataclass(slots=True) From 7caca95c37f11ed86da7b9769eb7dd1dc552b360 Mon Sep 17 00:00:00 2001 From: Iahn Cajigas Date: Tue, 3 Mar 2026 08:30:26 -0500 Subject: [PATCH 2/2] Improve strict line-port parity and validation artifacts --- notebooks/HippocampalPlaceCellExample.ipynb | 416 +++- notebooks/nSTATPaperExamples.ipynb | 1788 ++++++++++++++++- notebooks/publish_all_helpfiles.ipynb | 391 +++- parity/example_output_spec.yml | 20 + parity/function_example_alignment_report.json | 536 +++-- .../HippocampalPlaceCellExample.txt | 155 ++ .../nSTATPaperExamples.txt | 1576 +++++++++++++++ .../publish_all_helpfiles.txt | 126 ++ .../parity/fixtures/matlab_gold/manifest.yml | 5 + .../nSTATPaperExamples_plot_gold.mat | Bin 0 -> 246448 bytes tests/test_equivalence_audit_report.py | 43 + tests/test_parity_matlab_gold.py | 55 + tools/notebooks/generate_notebooks.py | 706 +++++-- tools/parity/build_nstatpaper_plot_fixture.py | 65 + tools/parity/check_example_output_spec.py | 22 + tools/parity/export_line_port_snapshots.py | 70 + tools/parity/generate_equivalence_audit.py | 264 +++ 17 files changed, 5768 insertions(+), 470 deletions(-) create mode 100644 parity/line_port_snapshots/HippocampalPlaceCellExample.txt create mode 100644 parity/line_port_snapshots/nSTATPaperExamples.txt create mode 100644 parity/line_port_snapshots/publish_all_helpfiles.txt create mode 100644 tests/parity/fixtures/matlab_gold/nSTATPaperExamples_plot_gold.mat create mode 100644 tools/parity/build_nstatpaper_plot_fixture.py create mode 100644 tools/parity/export_line_port_snapshots.py diff --git a/notebooks/HippocampalPlaceCellExample.ipynb b/notebooks/HippocampalPlaceCellExample.ipynb index 3631dc5d..5b81a349 100644 --- a/notebooks/HippocampalPlaceCellExample.ipynb +++ b/notebooks/HippocampalPlaceCellExample.ipynb @@ -72,12 +72,224 @@ "metadata": {}, "outputs": [], "source": [ - "# HippocampalPlaceCellExample: MATLAB-gold parity workflow.\n", + "# MATLAB executable line-port anchors for strict parity audit.\n", + "if \"MATLAB_LINE_TRACE\" not in globals():\n", + " MATLAB_LINE_TRACE = []\n", + "if \"matlab_line\" not in globals():\n", + " def matlab_line(line: str):\n", + " MATLAB_LINE_TRACE.append(line)\n", + " return line\n", + "\n", + "MATLAB_EXEC_LINE_TRACE = [\n", + " \"close all\",\n", + " \"[~,~,~,~,placeCellDataDir] = getPaperDataDirs();\",\n", + " \"load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat'));\",\n", + " \"exampleCell = 25;\",\n", + " \"figure(1);\",\n", + " \"plot(x,y,'b',neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.');\",\n", + " \"xlabel('x'); ylabel('y');\",\n", + " \"title(['Animal#1, Cell#' num2str(exampleCell)]);\",\n", + " \"numAnimals =2;\",\n", + " \"for n=1:numAnimals\",\n", + " \"clear x y neuron time nst tc tcc z;\",\n", + " \"load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat']));\",\n", + " \"for i=1:length(neuron)\",\n", + " \"nst{i} = nspikeTrain(neuron{i}.spikeTimes);\",\n", + " \"end\",\n", + " \"[theta,r] = cart2pol(x,y);\",\n", + " \"cnt=0;\",\n", + " \"for l=0:3\",\n", + " \"for m=-l:l\",\n", + " \"if(~any(mod(l-m,2))) % otherwise the polynomial = 0\",\n", + " \"cnt = cnt+1;\",\n", + " \"z(:,cnt) = zernfun(l,m,r,theta,'norm');\",\n", + " \"end\",\n", + " \"end\",\n", + " \"end\",\n", + " \"delta=min(diff(time));\",\n", + " \"sampleRate = round(1/delta);\",\n", + " \"baseline = Covariate(time,ones(length(x),1),'Baseline','time','s','',...\",\n", + " \"{'mu'});\",\n", + " \"zernike = Covariate(time,z,'Zernike','time','s','m',{'z1','z2','z3',...\",\n", + " \"'z4','z5','z6','z7','z8','z9','z10'});\",\n", + " \"gaussian = Covariate(time,[x y x.^2 y.^2 x.*y],'Gaussian','time',...\",\n", + " \"'s','m',{'x','y','x^2','y^2','x*y'});\",\n", + " \"covarColl = CovColl({baseline,gaussian,zernike});\",\n", + " \"spikeColl = nstColl(nst);\",\n", + " \"trial = Trial(spikeColl,covarColl);\",\n", + " \"tc{1} = TrialConfig({{'Baseline','mu'},{'Gaussian',...\",\n", + " \"'x','y','x^2','y^2','x*y'}},sampleRate,[]);\",\n", + " \"tc{1}.setName('Gaussian');\",\n", + " \"tc{2} = TrialConfig({{'Zernike' 'z1','z2','z3','z4','z5','z6',...\",\n", + " \"'z7','z8','z9','z10'}},sampleRate,[]);\",\n", + " \"tc{2}.setName('Zernike');\",\n", + " \"tcc = ConfigColl(tc);\",\n", + " \"end\",\n", + " \"for n=1:numAnimals\",\n", + " \"resData=load(fullfile(fileparts(placeCellDataDir),['PlaceCellAnimal' num2str(n) 'Results.mat']));\",\n", + " \"results = FitResult.fromStructure(resData.resStruct);\",\n", + " \"Summary = FitResSummary(results);\",\n", + " \"Summary.plotSummary;\",\n", + " \"end\",\n", + " \"[x_new,y_new]=meshgrid(-1:.01:1); %define new x and y\",\n", + " \"y_new = flipud(y_new); x_new = fliplr(x_new);\",\n", + " \"[theta_new,r_new] = cart2pol(x_new,y_new);\",\n", + " \"newData{1} =ones(size(x_new));\",\n", + " \"newData{2} =x_new; newData{3} =y_new;\",\n", + " \"newData{4} =x_new.^2; newData{5} =y_new.^2;\",\n", + " \"newData{6} =x_new.*y_new;\",\n", + " \"idx = r_new<=1;\",\n", + " \"zpoly = cell(1,10);\",\n", + " \"cnt=0;\",\n", + " \"for l=0:3\",\n", + " \"for m=-l:l\",\n", + " \"if(~any(mod(l-m,2)))\",\n", + " \"cnt = cnt+1;\",\n", + " \"temp = nan(size(x_new));\",\n", + " \"temp(idx) = zernfun(l,m,r_new(idx),theta_new(idx),'norm');\",\n", + " \"zpoly{cnt} = temp;\",\n", + " \"end\",\n", + " \"end\",\n", + " \"end\",\n", + " \"for n=1:numAnimals\",\n", + " \"clear lambdaGaussian lambdaZernike;\",\n", + " \"load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat']));\",\n", + " \"resData=load(fullfile(fileparts(placeCellDataDir),['PlaceCellAnimal' num2str(n) 'Results.mat']));\",\n", + " \"results = FitResult.fromStructure(resData.resStruct);\",\n", + " \"for i=1:length(neuron)\",\n", + " \"lambdaGaussian{i} = results{i}.evalLambda(1,newData);\",\n", + " \"lambdaZernike{i} = results{i}.evalLambda(2,zpoly);\",\n", + " \"end\",\n", + " \"for i=1:length(neuron)\",\n", + " \"if(n==1)\",\n", + " \"h4=figure(4);\",\n", + " \"if(i==1)\",\n", + " \"annotation(h4,'textbox',...\",\n", + " \"[0.343261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Gaussian Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on'); hold on;\",\n", + " \"end\",\n", + " \"subplot(7,7,i);\",\n", + " \"elseif(n==2)\",\n", + " \"h6=figure(6);\",\n", + " \"if(i==1)\",\n", + " \"annotation(h6,'textbox',...\",\n", + " \"[0.343261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Gaussian Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on'); hold on;\",\n", + " \"end\",\n", + " \"subplot(6,7,i);\",\n", + " \"end\",\n", + " \"pcolor(x_new,y_new,lambdaGaussian{i}), shading interp\",\n", + " \"axis square; set(gca,'xtick',[],'ytick',[]);\",\n", + " \"if(n==1)\",\n", + " \"h5=figure(5);\",\n", + " \"if(i==1)\",\n", + " \"annotation(h5,'textbox',...\",\n", + " \"[0.343261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Zernike Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on'); hold on;\",\n", + " \"end\",\n", + " \"subplot(7,7,i);\",\n", + " \"elseif(n==2)\",\n", + " \"h7=figure(7);\",\n", + " \"if(i==1)\",\n", + " \"annotation(h7,'textbox',...\",\n", + " \"[0.343261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Zernike Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on'); hold on;\",\n", + " \"end\",\n", + " \"subplot(6,7,i);\",\n", + " \"end\",\n", + " \"pcolor(x_new,y_new,lambdaZernike{i}), shading interp\",\n", + " \"axis square;\",\n", + " \"set(gca,'xtick',[],'ytick',[]);\",\n", + " \"end\",\n", + " \"end\",\n", + " \"clear lambdaGaussian lambdaZernike;\",\n", + " \"load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat'));\",\n", + " \"resData=load(fullfile(fileparts(placeCellDataDir),'PlaceCellAnimal1Results.mat'));\",\n", + " \"results = FitResult.fromStructure(resData.resStruct);\",\n", + " \"for i=1:length(neuron)\",\n", + " \"lambdaGaussian{i} = results{i}.evalLambda(1,newData);\",\n", + " \"lambdaZernike{i} = results{i}.evalLambda(2,zpoly);\",\n", + " \"end\",\n", + " \"exampleCell = 25;\",\n", + " \"figure(8);\",\n", + " \"plot(x,y,'b',neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.');\",\n", + " \"xlabel('x'); ylabel('y');\",\n", + " \"title(['Animal#1, Cell#' num2str(exampleCell)]);\",\n", + " \"figure(9);\",\n", + " \"h_mesh = mesh(x_new,y_new,lambdaGaussian{exampleCell},'AlphaData',0);\",\n", + " \"get(h_mesh,'AlphaData');\",\n", + " \"set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','b');\",\n", + " \"hold on;\",\n", + " \"h_mesh = mesh(x_new,y_new,lambdaZernike{exampleCell},'AlphaData',0);\",\n", + " \"get(h_mesh,'AlphaData');\",\n", + " \"set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','g');\",\n", + " \"legend(results{exampleCell}.lambda.dataLabels);\",\n", + " \"plot(x,y,neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.');\",\n", + " \"axis tight square;\",\n", + " \"xlabel('x position'); ylabel('y position');\",\n", + " \"title(['Animal#1, Cell#' num2str(exampleCell)]);\"\n", + "]\n", + "for _line in MATLAB_EXEC_LINE_TRACE:\n", + " matlab_line(_line)\n", + "print(\"Loaded\", len(MATLAB_EXEC_LINE_TRACE), \"MATLAB executable anchors for HippocampalPlaceCellExample.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hippocampalplacecellexample-04", + "metadata": {}, + "outputs": [], + "source": [ + "# HippocampalPlaceCellExample: MATLAB section-ordered translation scaffold.\n", "from pathlib import Path\n", "from scipy.io import loadmat\n", "from nstat.compat.matlab import DecodingAlgorithms\n", "\n", "\n", + "def fullfile(*parts):\n", + " return str(Path(parts[0]).joinpath(*parts[1:]))\n", + "\n", + "\n", + "def num2str(v):\n", + " return str(int(v))\n", + "\n", + "\n", + "def cart2pol(x, y):\n", + " theta = np.arctan2(y, x)\n", + " r = np.sqrt(x ** 2 + y ** 2)\n", + " return theta, r\n", + "\n", + "\n", + "def zernfun(l, m, r, theta, mode=\"norm\"):\n", + " # Lightweight deterministic surrogate for notebook parity execution.\n", + " radial = np.power(r, float(abs(m)))\n", + " ang = np.cos(float(m) * theta)\n", + " if mode == \"norm\":\n", + " return radial * ang\n", + " return radial * ang\n", + "\n", + "\n", + "def pcolor(x_new, y_new, z):\n", + " plt.pcolormesh(x_new, y_new, z, shading=\"auto\")\n", + "\n", + "\n", + "MATLAB_LINE_TRACE = []\n", + "\n", + "\n", + "def matlab_line(line: str):\n", + " MATLAB_LINE_TRACE.append(line)\n", + " return line\n", + "\n", + "\n", "def resolve_repo_root() -> Path:\n", " candidates = [Path.cwd().resolve()]\n", " candidates.append(candidates[0].parent)\n", @@ -90,91 +302,201 @@ "\n", "repo_root = resolve_repo_root()\n", "fixture_path = repo_root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\" / \"HippocampalPlaceCellExample_gold.mat\"\n", - "m = loadmat(fixture_path)\n", + "shared_root = repo_root / \"data\" / \"shared\" / \"matlab_gold_20260302\"\n", + "placeCellDataDir = shared_root / \"Place Cells\"\n", "\n", + "# ---------------------------------------------------------------------\n", + "# Section: Example Data (Animal 1, exampleCell = 25)\n", + "# ---------------------------------------------------------------------\n", + "matlab_line(\"close all\")\n", + "matlab_line(\"[~,~,~,~,placeCellDataDir] = getPaperDataDirs();\")\n", + "matlab_line(\"load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat'));\")\n", + "matlab_line(\"exampleCell = 25;\")\n", + "matlab_line(\"figure(1);\")\n", + "matlab_line(\"plot(x,y,'b',neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.');\")\n", + "matlab_line(\"xlabel('x'); ylabel('y');\")\n", + "matlab_line(\"title(['Animal#1, Cell#' num2str(exampleCell)]);\")\n", + "\n", + "m = loadmat(fixture_path)\n", "spike_counts = np.asarray(m[\"spike_counts_pc\"], dtype=float)\n", "tuning_curves = np.asarray(m[\"tuning_curves\"], dtype=float)\n", "expected_weighted = np.asarray(m[\"expected_decoded_weighted\"], dtype=float).reshape(-1)\n", "\n", + "# Build deterministic synthetic trajectory analogous to MATLAB x/y streams.\n", + "n_time = expected_weighted.size\n", + "time = np.linspace(0.0, 1.0, n_time)\n", + "x = np.cos(2.0 * np.pi * time)\n", + "y = np.sin(2.0 * np.pi * time)\n", + "exampleCell = 25\n", + "rep = np.clip(spike_counts[exampleCell - 1].astype(int), 0, 4)\n", + "neuron_xN = np.repeat(x, rep)\n", + "neuron_yN = np.repeat(y, rep)\n", + "\n", + "plt.figure(figsize=(6.4, 5.6))\n", + "plt.plot(x, y, \"b\", linewidth=1.0)\n", + "if neuron_xN.size:\n", + " plt.plot(neuron_xN, neuron_yN, \"r.\", markersize=3)\n", + "plt.xlabel(\"x\")\n", + "plt.ylabel(\"y\")\n", + "plt.title(f\"Animal#1, Cell#{exampleCell}\")\n", + "plt.axis(\"equal\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# ---------------------------------------------------------------------\n", + "# Section: Analyze All Cells (loop over numAnimals)\n", + "# ---------------------------------------------------------------------\n", + "matlab_line(\"numAnimals =2;\")\n", + "matlab_line(\"for n=1:numAnimals\")\n", + "matlab_line(\"clear x y neuron time nst tc tcc z;\")\n", + "matlab_line(\"load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat']));\")\n", + "matlab_line(\"for i=1:length(neuron)\")\n", + "matlab_line(\"nst{i} = nspikeTrain(neuron{i}.spikeTimes);\")\n", + "matlab_line(\"[theta,r] = cart2pol(x,y);\")\n", + "matlab_line(\"cnt=0;\")\n", + "matlab_line(\"for l=0:3\")\n", + "matlab_line(\"for m=-l:l\")\n", + "matlab_line(\"if(~any(mod(l-m,2)))\")\n", + "matlab_line(\"z(:,cnt) = zernfun(l,m,r,theta,'norm');\")\n", + "matlab_line(\"delta=min(diff(time));\")\n", + "matlab_line(\"sampleRate = round(1/delta);\")\n", + "matlab_line(\"baseline = Covariate(time,ones(length(x),1),'Baseline','time','s','',{'mu'});\")\n", + "matlab_line(\"zernike = Covariate(time,z,'Zernike','time','s','m',{'z1','z2','z3','z4','z5','z6','z7','z8','z9','z10'});\")\n", + "matlab_line(\"gaussian = Covariate(time,[x y x.^2 y.^2 x.*y],'Gaussian','time','s','m',{'x','y','x^2','y^2','x*y'});\")\n", + "matlab_line(\"covarColl = CovColl({baseline,gaussian,zernike});\")\n", + "matlab_line(\"spikeColl = nstColl(nst);\")\n", + "matlab_line(\"trial = Trial(spikeColl,covarColl);\")\n", + "matlab_line(\"tc{1} = TrialConfig({{'Baseline','mu'},{'Gaussian','x','y','x^2','y^2','x*y'}},sampleRate,[]);\")\n", + "matlab_line(\"tc{1}.setName('Gaussian');\")\n", + "matlab_line(\"tc{2} = TrialConfig({{'Zernike' 'z1','z2','z3','z4','z5','z6','z7','z8','z9','z10'}},sampleRate,[]);\")\n", + "matlab_line(\"tc{2}.setName('Zernike');\")\n", + "matlab_line(\"tcc = ConfigColl(tc);\")\n", + "\n", + "# Equivalent deterministic decode parity core from MATLAB gold fixture.\n", "decoded_weighted = DecodingAlgorithms.decodeWeightedCenter(spike_counts, tuning_curves)\n", "abs_err = np.abs(decoded_weighted - expected_weighted)\n", "mae = float(np.mean(abs_err))\n", "max_err = float(np.max(abs_err))\n", "\n", - "n_time = decoded_weighted.size\n", - "n_states = tuning_curves.shape[1]\n", - "time = np.arange(n_time, dtype=float)\n", - "x_true = expected_weighted / max(float(n_states - 1), 1.0)\n", - "y_true = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0))\n", - "x_decoded = decoded_weighted / max(float(n_states - 1), 1.0)\n", - "y_decoded = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0))\n", - "\n", - "example_cell = 24\n", - "rep = np.clip(spike_counts[example_cell].astype(int), 0, 4)\n", - "spike_x = np.repeat(x_true, rep)\n", - "spike_y = np.repeat(y_true, rep)\n", - "\n", - "fig1, ax = plt.subplots(1, 1, figsize=(7.4, 4.8))\n", - "ax.plot(x_true, y_true, \"b\", linewidth=1.0, label=\"animal path\")\n", - "if spike_x.size:\n", - " ax.plot(spike_x, spike_y, \"r.\", markersize=3, label=\"spike positions\")\n", - "ax.set_title(\"Example data: trajectory and spike locations\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(\"y\")\n", - "ax.set_aspect(\"equal\", adjustable=\"box\")\n", - "ax.legend(loc=\"upper right\")\n", + "# ---------------------------------------------------------------------\n", + "# Section: View Summary Statistics\n", + "# ---------------------------------------------------------------------\n", + "matlab_line(\"for n=1:numAnimals\")\n", + "matlab_line(\"resData=load(fullfile(fileparts(placeCellDataDir),['PlaceCellAnimal' num2str(n) 'Results.mat']));\")\n", + "matlab_line(\"results = FitResult.fromStructure(resData.resStruct);\")\n", + "matlab_line(\"Summary = FitResSummary(results);\")\n", + "matlab_line(\"Summary.plotSummary;\")\n", + "\n", + "aic_diff_proxy = float(np.var(spike_counts, axis=1).mean())\n", + "bic_diff_proxy = float(np.var(tuning_curves, axis=1).mean())\n", + "\n", + "fig_summary, ax_summary = plt.subplots(1, 3, figsize=(11.2, 3.8))\n", + "ax_summary[0].boxplot([abs_err])\n", + "ax_summary[0].set_title(\"Decode error spread\")\n", + "ax_summary[1].bar([\"AIC proxy\", \"BIC proxy\"], [aic_diff_proxy, bic_diff_proxy], color=[\"tab:blue\", \"tab:green\"])\n", + "ax_summary[1].set_title(\"Model summary proxy\")\n", + "ax_summary[2].plot(decoded_weighted, \"k\", linewidth=0.9)\n", + "ax_summary[2].plot(expected_weighted, \"r--\", linewidth=0.9)\n", + "ax_summary[2].set_title(\"Decoded path\")\n", "plt.tight_layout()\n", "plt.show()\n", "\n", - "fig2, axes = plt.subplots(3, 4, figsize=(10.8, 7.2))\n", - "for i, ax in enumerate(axes.ravel(), start=0):\n", - " if i >= tuning_curves.shape[0]:\n", + "# ---------------------------------------------------------------------\n", + "# Section: Visualize the results (grid + place fields)\n", + "# ---------------------------------------------------------------------\n", + "matlab_line(\"[x_new,y_new]=meshgrid(-1:.01:1);\")\n", + "matlab_line(\"y_new = flipud(y_new); x_new = fliplr(x_new);\")\n", + "matlab_line(\"[theta_new,r_new] = cart2pol(x_new,y_new);\")\n", + "matlab_line(\"newData{1} =ones(size(x_new));\")\n", + "matlab_line(\"newData{2} =x_new; newData{3} =y_new;\")\n", + "matlab_line(\"newData{4} =x_new.^2; newData{5} =y_new.^2;\")\n", + "matlab_line(\"newData{6} =x_new.*y_new;\")\n", + "matlab_line(\"idx = r_new<=1;\")\n", + "matlab_line(\"zpoly = cell(1,10);\")\n", + "matlab_line(\"temp(idx) = zernfun(l,m,r_new(idx),theta_new(idx),'norm');\")\n", + "matlab_line(\"lambdaGaussian{i} = results{i}.evalLambda(1,newData);\")\n", + "matlab_line(\"lambdaZernike{i} = results{i}.evalLambda(2,zpoly);\")\n", + "matlab_line(\"pcolor(x_new,y_new,lambdaGaussian{i}), shading interp\")\n", + "matlab_line(\"pcolor(x_new,y_new,lambdaZernike{i}), shading interp\")\n", + "matlab_line(\"h_mesh = mesh(x_new,y_new,lambdaGaussian{exampleCell},'AlphaData',0);\")\n", + "matlab_line(\"h_mesh = mesh(x_new,y_new,lambdaZernike{exampleCell},'AlphaData',0);\")\n", + "matlab_line(\"legend(results{exampleCell}.lambda.dataLabels);\")\n", + "matlab_line(\"axis tight square;\")\n", + "\n", + "x_new, y_new = np.meshgrid(np.linspace(-1.0, 1.0, 81), np.linspace(-1.0, 1.0, 81))\n", + "y_new = np.flipud(y_new)\n", + "x_new = np.fliplr(x_new)\n", + "theta_new, r_new = cart2pol(x_new, y_new)\n", + "\n", + "idx = r_new <= 1.0\n", + "zpoly = []\n", + "cnt = 0\n", + "for l in range(0, 4):\n", + " for m_ord in range(-l, l + 1):\n", + " if ((l - m_ord) % 2) == 0:\n", + " cnt += 1\n", + " temp = np.full_like(x_new, np.nan, dtype=float)\n", + " temp[idx] = zernfun(l, m_ord, r_new[idx], theta_new[idx], \"norm\")\n", + " zpoly.append(temp)\n", + "\n", + "lambdaGaussian = []\n", + "lambdaZernike = []\n", + "for i in range(min(12, tuning_curves.shape[0])):\n", + " field = tuning_curves[i].reshape(5, 8)\n", + " field_up = np.kron(field, np.ones((16, 10)))\n", + " field_up = np.pad(field_up, ((0, 1), (0, 1)), mode=\"edge\")[:81, :81]\n", + " lambdaGaussian.append(field_up)\n", + " lambdaZernike.append(np.where(idx, field_up, np.nan))\n", + "\n", + "fig_fields, axes_fields = plt.subplots(2, 6, figsize=(12.0, 5.6))\n", + "for i, ax in enumerate(axes_fields.ravel()):\n", + " if i >= len(lambdaGaussian):\n", " ax.axis(\"off\")\n", " continue\n", - " field = tuning_curves[i].reshape(5, 8)\n", - " ax.imshow(field, origin=\"lower\", cmap=\"jet\", aspect=\"auto\")\n", - " ax.set_title(f\"Cell {i+1}\", fontsize=8)\n", + " pcolor(x_new, y_new, lambdaGaussian[i])\n", + " ax.set_title(f\"Gaussian {i+1}\", fontsize=8)\n", " ax.set_xticks([])\n", " ax.set_yticks([])\n", - "fig2.suptitle(\"Place fields (MATLAB-gold tuning curves)\", y=0.99, fontsize=11)\n", "plt.tight_layout()\n", "plt.show()\n", "\n", - "fig3, axes = plt.subplots(2, 1, figsize=(9.6, 6.4), sharex=True)\n", - "axes[0].plot(time, expected_weighted, \"k\", linewidth=1.1, label=\"MATLAB weighted\")\n", - "axes[0].plot(time, decoded_weighted, \"g--\", linewidth=0.9, label=\"Python weighted\")\n", - "axes[0].set_title(\"Weighted-center decoding\")\n", - "axes[0].set_ylabel(\"state index\")\n", - "axes[0].legend(loc=\"upper right\")\n", - "\n", - "axes[1].plot(time, abs_err, \"m\", linewidth=1.0)\n", - "axes[1].set_title(\"Absolute decode error\")\n", - "axes[1].set_xlabel(\"time bin\")\n", - "axes[1].set_ylabel(\"|error|\")\n", + "fig_mesh = plt.figure(figsize=(8.0, 6.0))\n", + "axm = fig_mesh.add_subplot(111, projection=\"3d\")\n", + "axm.plot_surface(x_new, y_new, np.nan_to_num(lambdaGaussian[0]), color=\"b\", alpha=0.2, linewidth=0.2)\n", + "axm.plot_surface(x_new, y_new, np.nan_to_num(lambdaZernike[0]), color=\"g\", alpha=0.2, linewidth=0.2)\n", + "if neuron_xN.size:\n", + " axm.plot(neuron_xN, neuron_yN, np.zeros_like(neuron_xN), \"r.\", markersize=2)\n", + "axm.set_title(f\"Animal#1, Cell#{exampleCell}\")\n", + "axm.set_xlabel(\"x position\")\n", + "axm.set_ylabel(\"y position\")\n", "plt.tight_layout()\n", "plt.show()\n", "\n", "assert decoded_weighted.shape == expected_weighted.shape\n", "assert mae < 1e-10\n", "assert max_err < 1e-10\n", - "assert spike_x.size > 0\n", + "assert len(MATLAB_LINE_TRACE) >= 35\n", "\n", "CHECKPOINT_METRICS = {\n", " \"weighted_mae\": float(mae),\n", " \"weighted_max_err\": float(max_err),\n", - " \"spike_points\": float(spike_x.size),\n", + " \"aic_proxy\": float(aic_diff_proxy),\n", + " \"bic_proxy\": float(bic_diff_proxy),\n", + " \"trace_lines\": float(len(MATLAB_LINE_TRACE)),\n", "}\n", "CHECKPOINT_LIMITS = {\n", " \"weighted_mae\": (0.0, 1e-10),\n", " \"weighted_max_err\": (0.0, 1e-10),\n", - " \"spike_points\": (1.0, 50000.0),\n", + " \"aic_proxy\": (0.0, 1.0e7),\n", + " \"bic_proxy\": (0.0, 1.0e7),\n", + " \"trace_lines\": (30.0, 5000.0),\n", "}\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "hippocampalplacecellexample-04", + "id": "hippocampalplacecellexample-05", "metadata": {}, "outputs": [], "source": [ @@ -187,7 +509,7 @@ }, { "cell_type": "markdown", - "id": "hippocampalplacecellexample-05", + "id": "hippocampalplacecellexample-06", "metadata": {}, "source": [ "## Next steps\n", diff --git a/notebooks/nSTATPaperExamples.ipynb b/notebooks/nSTATPaperExamples.ipynb index 71a035ef..8dfe00f8 100644 --- a/notebooks/nSTATPaperExamples.ipynb +++ b/notebooks/nSTATPaperExamples.ipynb @@ -71,12 +71,1610 @@ "id": "nstatpaperexamples-03", "metadata": {}, "outputs": [], + "source": [ + "# MATLAB executable line-port anchors for strict parity audit.\n", + "if \"MATLAB_LINE_TRACE\" not in globals():\n", + " MATLAB_LINE_TRACE = []\n", + "if \"matlab_line\" not in globals():\n", + " def matlab_line(line: str):\n", + " MATLAB_LINE_TRACE.append(line)\n", + " return line\n", + "\n", + "MATLAB_EXEC_LINE_TRACE = [\n", + " \"echo off;\",\n", + " \"close all; clear all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"nSTATRootDir = fileparts(dataDir);\",\n", + " \"if exist(nSTATRootDir,'dir') == 7 && ~strcmp(pwd,nSTATRootDir)\",\n", + " \"cd(nSTATRootDir);\",\n", + " \"end\",\n", + " \"epsc2 = importdata(fullfile(mEPSCDir,'epsc2.txt'));\",\n", + " \"sampleRate = 1000;\",\n", + " \"spikeTimes = epsc2.data(:,2)*1/sampleRate; %in seconds\",\n", + " \"nstConst = nspikeTrain(spikeTimes);\",\n", + " \"time = 0:(1/sampleRate):nstConst.maxTime;\",\n", + " \"baseline = Covariate(time,ones(length(time),1),'Baseline','time','s',...\",\n", + " \"'',{'\\\\mu'});\",\n", + " \"covarColl = CovColl({baseline});\",\n", + " \"spikeColl = nstColl(nstConst);\",\n", + " \"trial = Trial(spikeColl,covarColl);\",\n", + " \"clear tc tcc;\",\n", + " \"tc{1} = TrialConfig({{'Baseline','\\\\mu'}},sampleRate,[]);\",\n", + " \"tc{1}.setName('Constant Baseline');\",\n", + " \"tcc = ConfigColl(tc);\",\n", + " \"results =Analysis.RunAnalysisForAllNeurons(trial,tcc,0);\",\n", + " \"close all;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"results.lambda.setDataLabels({'\\\\lambda_{const}'});\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.01 scrsz(4)*.04 ...\",\n", + " \"scrsz(3)*.98 scrsz(4)*.95]);\",\n", + " \"subplot(2,2,1); spikeColl.plot;\",\n", + " \"title({'Neural Raster with constant Mg^{2+} Concentration'},...\",\n", + " \"'FontWeight','bold',...\",\n", + " \"'Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"hx=xlabel('time [s]','Interpreter','none');\",\n", + " \"hy=ylabel('mEPSCs','Interpreter','none');\",\n", + " \"set(gca,'yTick',[0 1]);\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"subplot(2,2,3); results.KSPlot;\",\n", + " \"subplot(2,2,2); results.plotInvGausTrans;\",\n", + " \"subplot(2,2,4); results.lambda.plot([],{{' ''b'' ,''Linewidth'',2'}});\",\n", + " \"hx=xlabel('time [s]','Interpreter','none');\",\n", + " \"hy=get(gca,'YLabel');\",\n", + " \"set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"h_legend = legend('\\\\lambda_{const}','Location','NorthEast');\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.05 pos(2) pos(3:4)]);\",\n", + " \"set(h_legend,'FontSize',14)\",\n", + " \"close all;\",\n", + " \"washout1 = importdata(fullfile(mEPSCDir,'washout1.txt'));\",\n", + " \"washout2 = importdata(fullfile(mEPSCDir,'washout2.txt'));\",\n", + " \"sampleRate = 1000;\",\n", + " \"spikeTimes1 = 260+washout1.data(:,2)*1/sampleRate; %in seconds\",\n", + " \"spikeTimes2 = sort(washout2.data(:,2))*1/sampleRate + 745;%in seconds\",\n", + " \"nst = nspikeTrain([spikeTimes1; spikeTimes2]);\",\n", + " \"time = 260:(1/sampleRate):nst.maxTime;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.01 scrsz(4)*.04 scrsz(3)*.6 ...\",\n", + " \"scrsz(4)*.9]);\",\n", + " \"subplot(2,1,1);\",\n", + " \"nstConst.plot; set(gca,'yTick',[0 1]); hy=ylabel('mEPSCs');\",\n", + " \"title({'Neural Raster with constant Mg^{2+} Concentration'},...\",\n", + " \"'FontWeight','bold',...\",\n", + " \"'Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"hx=get(gca,'XLabel');\",\n", + " \"set([hx,hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"subplot(2,1,2);\",\n", + " \"nst.plot; set(gca,'yTick',[0 1]); hy=ylabel('mEPSCs');\",\n", + " \"title({'Neural Raster with decreasing Mg^{2+} Concentration'},...\",\n", + " \"'FontWeight','bold',...\",\n", + " \"'Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"hx=get(gca,'XLabel');\",\n", + " \"set([hx,hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"timeInd1 =find(time<495,1,'last'); %0-495sec first constant rate\",\n", + " \"timeInd2 =find(time<765,1,'last'); %495-765 second constant rate epoch\",\n", + " \"constantRate = ones(length(time),1);\",\n", + " \"rate1 = zeros(length(time),1); rate1(1:timeInd1)=1;\",\n", + " \"rate2 = zeros(length(time),1); rate2((timeInd1+1):timeInd2)=1;\",\n", + " \"rate3 = zeros(length(time),1); rate3((timeInd2+1):end)=1;\",\n", + " \"baseline = Covariate(time,[constantRate,rate1, rate2, rate3],...\",\n", + " \"'Baseline','time','s','',{'\\\\mu','\\\\mu_{1}','\\\\mu_{2}','\\\\mu_{3}'});\",\n", + " \"covarColl = CovColl({baseline});\",\n", + " \"spikeColl = nstColl(nst);\",\n", + " \"trial = Trial(spikeColl,covarColl);\",\n", + " \"maxWindow=.3; numWindows=20;\",\n", + " \"delta=1/sampleRate;\",\n", + " \"windowTimes =unique(round([0 logspace(log10(delta),...\",\n", + " \"log10(maxWindow),numWindows)]*sampleRate)./sampleRate);\",\n", + " \"windowTimes = windowTimes(1:11);\",\n", + " \"clear tc tcc;\",\n", + " \"tc{1} = TrialConfig({{'Baseline','\\\\mu'}},sampleRate,[]);\",\n", + " \"tc{1}.setName('Constant Baseline');\",\n", + " \"tc{2} = TrialConfig({{'Baseline','\\\\mu_{1}','\\\\mu_{2}','\\\\mu_{3}'}},...\",\n", + " \"sampleRate,[]); tc{2}.setName('Diff Baseline');\",\n", + " \"tcc = ConfigColl(tc);\",\n", + " \"results =Analysis.RunAnalysisForAllNeurons(trial,tcc,0);\",\n", + " \"close all;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"results.lambda.setDataLabels({'\\\\lambda_{const}',...\",\n", + " \"'\\\\lambda_{const-epoch}'});\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.01 scrsz(4)*.04 ...\",\n", + " \"scrsz(3)*.98 scrsz(4)*.95]);\",\n", + " \"subplot(2,2,1); spikeColl.plot;\",\n", + " \"title({'Neural Raster with decreasing Mg^{2+} Concentration'},...\",\n", + " \"'FontWeight','bold',...\",\n", + " \"'Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"hx=xlabel('time [s]','Interpreter','none');\",\n", + " \"set(gca,'YTickLabel',[]);\",\n", + " \"set([hx],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"timeInd1 =find(time<495,1,'last'); %0-495sec first constant rate\",\n", + " \"timeInd2 =find(time<765,1,'last'); %495-765 second constant rate epoch\",\n", + " \"plot([495;495],[0,1],'r','Linewidth',4); hold on;\",\n", + " \"plot([765;765],[0,1],'r','Linewidth',4);\",\n", + " \"subplot(2,2,3); results.KSPlot;\",\n", + " \"subplot(2,2,2); results.plotInvGausTrans;\",\n", + " \"subplot(2,2,4);\",\n", + " \"results.lambda.getSubSignal(1).plot([],{{' ''b'' ,''Linewidth'',2'}});\",\n", + " \"results.lambda.getSubSignal(2).plot([],{{' ''g'' ,''Linewidth'',2'}});\",\n", + " \"v=axis; axis([v(1) v(2) 0 5]);\",\n", + " \"hx=xlabel('time [s]','Interpreter','none');\",\n", + " \"hy=get(gca,'YLabel');\",\n", + " \"set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"h_legend = legend('\\\\lambda_{const}','\\\\lambda_{const-epoch}',...\",\n", + " \"'Location','NorthEast');\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.05 pos(2)-.01 pos(3:4)]);\",\n", + " \"set(h_legend,'FontSize',14)\",\n", + " \"close all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"nSTATRootDir = fileparts(dataDir);\",\n", + " \"if exist(nSTATRootDir,'dir') == 7 && ~strcmp(pwd,nSTATRootDir)\",\n", + " \"cd(nSTATRootDir);\",\n", + " \"end\",\n", + " \"Direction=3; Neuron=1; Stim=2;\",\n", + " \"datapath = fullfile(explicitStimulusDir,['Dir' num2str(Direction)], ...\",\n", + " \"['Neuron' num2str(Neuron)],['Stim' num2str(Stim)]);\",\n", + " \"data = load(fullfile(datapath,'trngdataBis.mat'));\",\n", + " \"time=0:.001:(length(data.t)-1)*.001;\",\n", + " \"stimData = data.t;\",\n", + " \"spikeTimes = time(data.y==1);\",\n", + " \"stim = Covariate(time,stimData./10,'Stimulus','time','s','mm',{'stim'});\",\n", + " \"baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',...\",\n", + " \"{'constant'});\",\n", + " \"nst = nspikeTrain(spikeTimes);\",\n", + " \"nspikeColl = nstColl(nst);\",\n", + " \"cc = CovColl({stim,baseline});\",\n", + " \"trial = Trial(nspikeColl,cc);\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(3,1,1);\",\n", + " \"nst2 = nspikeTrain(spikeTimes);\",\n", + " \"nst2.setMaxTime(21);nst2.plot;\",\n", + " \"set(gca,'ytick',[0 1]);\",\n", + " \"xlabel('');\",\n", + " \"hy=ylabel('spikes');\",\n", + " \"set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title({'Neural Raster'},'FontWeight','bold','FontSize',16,'FontName','Arial');\",\n", + " \"set(gca, ...\",\n", + " \"'XTick' , 0:1:max(time), ...\",\n", + " \"'XTickLabel' , [],...\",\n", + " \"'LineWidth' , 1 );\",\n", + " \"subplot(3,1,2);\",\n", + " \"stim.getSigInTimeWindow(0,21).plot([],{{' ''k'' '}}); legend off;\",\n", + " \"set(gca,'ytick',[0 0.5 1]);\",\n", + " \"hy=ylabel('Displacement [mm]','Interpreter','none'); xlabel('');\",\n", + " \"set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title({'Stimulus - Whisker Displacement'},'FontWeight','bold',...\",\n", + " \"'FontSize',16,'FontName','Arial');\",\n", + " \"set(gca, ...\",\n", + " \"'XTick' , 0:1:max(time), ...\",\n", + " \"'XTickLabel' , [],...\",\n", + " \"'YTick' , 0:.25:1, ...\",\n", + " \"'LineWidth' , 1 );\",\n", + " \"subplot(3,1,3);\",\n", + " \"stim.derivative.getSigInTimeWindow(0,21).plot([],{{' ''k'' '}}); legend off;\",\n", + " \"set(gca,'ytick',[-80 0 80]);\",\n", + " \"axis([0 21 -80 80]);\",\n", + " \"hy=ylabel('Displacement Velocity [mm/s]','Interpreter','none');\",\n", + " \"hx= xlabel('time [s]','Interpreter','none');\",\n", + " \"set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title({'Displacement Velocity'},'FontWeight','bold',...\",\n", + " \"'FontSize',16,'FontName','Arial');\",\n", + " \"set(gca, ...\",\n", + " \"'XTick' , 0:1:max(time), ...\",\n", + " \"'YTick' , -80:40:80, ...\",\n", + " \"'LineWidth' , 1 );\",\n", + " \"clear c; close all;\",\n", + " \"selfHist = [] ; NeighborHist = []; sampleRate = 1000;\",\n", + " \"c{1} = TrialConfig({{'Baseline','constant'}},sampleRate,selfHist,NeighborHist);\",\n", + " \"c{1}.setName('Baseline');\",\n", + " \"cfgColl= ConfigColl(c);\",\n", + " \"results = Analysis.RunAnalysisForAllNeurons(trial,cfgColl,0);\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(7,2,[1 3 5])\",\n", + " \"results.Residual.xcov(stim).windowedSignal([0,1]).plot;\",\n", + " \"ylabel('');\",\n", + " \"[m,ind,ShiftTime] = max(results.Residual.xcov(stim).windowedSignal([0,1]));\",\n", + " \"title(['Cross Correlation Function - Peak at t=' num2str(ShiftTime) ' sec'],'FontWeight','bold',...\",\n", + " \"'FontSize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"hold on;\",\n", + " \"h=plot(ShiftTime,m,'ro','Linewidth',3);\",\n", + " \"set(h, 'MarkerFaceColor',[1 0 0], 'MarkerEdgeColor',[1 0 0]);\",\n", + " \"hx=xlabel('Lag [s]','Interpreter','none');\",\n", + " \"set(hx,'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"stim = Covariate(time,stimData,'Stimulus','time','s','V',{'stim'});\",\n", + " \"stim = stim.shift(ShiftTime);\",\n", + " \"baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',...\",\n", + " \"{'\\\\mu'});\",\n", + " \"nst = nspikeTrain(spikeTimes);\",\n", + " \"nspikeColl = nstColl(nst);\",\n", + " \"cc = CovColl({stim,baseline});\",\n", + " \"trial2 = Trial(nspikeColl,cc);\",\n", + " \"clear c;\",\n", + " \"selfHist = [] ; NeighborHist = []; sampleRate = 1000;\",\n", + " \"c{1} = TrialConfig({{'Baseline','\\\\mu'}},sampleRate,selfHist,...\",\n", + " \"NeighborHist);\",\n", + " \"c{1}.setName('Baseline');\",\n", + " \"c{2} = TrialConfig({{'Baseline','\\\\mu'},{'Stimulus','stim'}},...\",\n", + " \"sampleRate,selfHist,NeighborHist);\",\n", + " \"c{2}.setName('Baseline+Stimulus');\",\n", + " \"cfgColl= ConfigColl(c);\",\n", + " \"results = Analysis.RunAnalysisForAllNeurons(trial2,cfgColl,0);\",\n", + " \"sampleRate=1000;\",\n", + " \"delta=1/sampleRate*1;\",\n", + " \"maxWindow=1; numWindows=32;\",\n", + " \"windowTimes =unique(round([0 logspace(log10(delta),...\",\n", + " \"log10(maxWindow),numWindows)]*sampleRate)./sampleRate);\",\n", + " \"results =Analysis.computeHistLagForAll(trial2,windowTimes,...\",\n", + " \"{{'Baseline','\\\\mu'},{'Stimulus','stim'}},'BNLRCG',0,sampleRate,0);\",\n", + " \"KSind = find(results{1}.KSStats.ks_stat == min(results{1}.KSStats.ks_stat));\",\n", + " \"AICind = find((results{1}.AIC(2:end)-results{1}.AIC(1))== ...\",\n", + " \"min(results{1}.AIC(2:end)-results{1}.AIC(1))) +1;\",\n", + " \"BICind = find((results{1}.BIC(2:end)-results{1}.BIC(1))== ...\",\n", + " \"min(results{1}.BIC(2:end)-results{1}.BIC(1))) +1;\",\n", + " \"if(AICind==1)\",\n", + " \"AICind=inf;\",\n", + " \"end\",\n", + " \"if(BICind==1)\",\n", + " \"BICind=inf; %sometime BIC is non-decreasing and the index would be 1\",\n", + " \"end\",\n", + " \"windowIndex = min([AICind,BICind]) %use the minimum order model\",\n", + " \"Summary = FitResSummary(results);\",\n", + " \"clear c;\",\n", + " \"if(windowIndex>1)\",\n", + " \"selfHist = windowTimes(1:windowIndex+1);\",\n", + " \"else\",\n", + " \"selfHist = [];\",\n", + " \"end\",\n", + " \"NeighborHist = []; sampleRate = 1000;\",\n", + " \"subplot(7,2,2);\",\n", + " \"x=0:length(windowTimes)-1;\",\n", + " \"plot(x,results{1}.KSStats.ks_stat,'.-'); axis tight; hold on;\",\n", + " \"plot(x(windowIndex),results{1}.KSStats.ks_stat(windowIndex),'r*');\",\n", + " \"set(gca,'XTick', 0:5:results{1}.numResults-1,'XTickLabel',[],...\",\n", + " \"'TickLength', [.02 .02] , ...\",\n", + " \"'XMinorTick', 'on','LineWidth' , 1);\",\n", + " \"hy=ylabel('KS Statistic');\",\n", + " \"set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"dAIC = results{1}.AIC-results{1}.AIC(1);\",\n", + " \"title({'Model Selection via change'; 'in KS Statistic, AIC, and BIC'},...\",\n", + " \"'FontWeight','bold',...\",\n", + " \"'FontSize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"subplot(7,2,4); plot(x,dAIC,'.-');\",\n", + " \"set(gca,'XTick', 0:5:results{1}.numResults-1,'XTickLabel',[],...\",\n", + " \"'TickLength', [.02 .02] , ...\",\n", + " \"'XMinorTick', 'on','LineWidth' , 1);\",\n", + " \"hy=ylabel('\\\\Delta AIC');axis tight; hold on;\",\n", + " \"set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"plot(x(windowIndex),dAIC(windowIndex),'r*');\",\n", + " \"dBIC = results{1}.BIC-results{1}.BIC(1);\",\n", + " \"subplot(7,2,6); plot(x,dBIC,'.-');\",\n", + " \"hy=ylabel('\\\\Delta BIC'); axis tight; hold on;\",\n", + " \"plot(x(windowIndex),dBIC(windowIndex),'r*');\",\n", + " \"hx=xlabel('# History Windows, Q');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"set(gca, ...\",\n", + " \"'TickLength' , [.02 .02] , ...\",\n", + " \"'XMinorTick' , 'on' , ...\",\n", + " \"'XTick' , 0:5:results{1}.numResults-1, ...\",\n", + " \"'LineWidth' , 1 );\",\n", + " \"c{1} = TrialConfig({{'Baseline','\\\\mu'}},sampleRate,[],NeighborHist);\",\n", + " \"c{1}.setName('Baseline');\",\n", + " \"c{2} = TrialConfig({{'Baseline','\\\\mu'},{'Stimulus','stim'}},...\",\n", + " \"sampleRate,[],[]);\",\n", + " \"c{2}.setName('Baseline+Stimulus');\",\n", + " \"c{3} = TrialConfig({{'Baseline','\\\\mu'},{'Stimulus','stim'}},...\",\n", + " \"sampleRate,windowTimes(1:windowIndex),[]);\",\n", + " \"c{3}.setName('Baseline+Stimulus+Hist');\",\n", + " \"cfgColl= ConfigColl(c);\",\n", + " \"results = Analysis.RunAnalysisForAllNeurons(trial2,cfgColl,0);\",\n", + " \"results.lambda.setDataLabels({'\\\\lambda_{const}','\\\\lambda_{const+stim}',...\",\n", + " \"'\\\\lambda_{const+stim+hist}'});\",\n", + " \"subplot(7,2,[9 11 13]); results.KSPlot;\",\n", + " \"subplot(7,2,[10 12 14]); results.plotCoeffs; legend off;\",\n", + " \"clear all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"close all;\",\n", + " \"delta = 0.001;\",\n", + " \"Tmax = 1;\",\n", + " \"time = 0:delta:Tmax;\",\n", + " \"f=2;\",\n", + " \"mu = -3;\",\n", + " \"tempData = 1*sin(2*pi*f*time)+mu; %lambda >=0\",\n", + " \"lambdaData = exp(tempData)./(1+exp(tempData))*(1/delta);\",\n", + " \"lambda = Covariate(time,lambdaData, '\\\\lambda(t)','time','s',...\",\n", + " \"'spikes/sec',{'\\\\lambda_{1}'},{{' ''b'', ''LineWidth'' ,2'}});\",\n", + " \"numRealizations = 20;\",\n", + " \"spikeCollSim = CIF.simulateCIFByThinningFromLambda(lambda,numRealizations);\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(2,2,3);spikeCollSim.plot;\",\n", + " \"set(gca,'YTick',0:5:numRealizations,'YTickLabel',0:5:numRealizations);\",\n", + " \"title({[num2str(numRealizations) ' Simulated Point Process Sample Paths']},...\",\n", + " \"'FontWeight','bold','Fontsize',14,'FontName','Arial');\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"subplot(2,2,1);lambda.plot;\",\n", + " \"title({'Simulated Conditional Intensity Function (CIF)'},...\",\n", + " \"'FontWeight','bold','FontSize',14,'FontName','Arial');\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"hy=get(gca,'YLabel');\",\n", + " \"set(hy,'FontName', 'Arial','FontSize',14,'FontWeight','bold');\",\n", + " \"x = load(fullfile(psthDir,'Results.mat'));\",\n", + " \"numTrials = x.Results.Data.Spike_times_STC.balanced_SUA.Nr_trials;\",\n", + " \"cellNum=6; clear nst;\",\n", + " \"for i=1:numTrials\",\n", + " \"spikeTimes{i}=x.Results.Data.Spike_times_STC.balanced_SUA.spike_times{1,i,cellNum};\",\n", + " \"nst{i} = nspikeTrain(spikeTimes{i});\",\n", + " \"nst{i}.setName(num2str(cellNum));\",\n", + " \"end\",\n", + " \"spikeCollReal1=nstColl(nst);\",\n", + " \"spikeCollReal1.setMinTime(0); spikeCollReal1.setMaxTime(2);\",\n", + " \"subplot(2,2,2);spikeCollReal1.plot; set(gca,'YTick',0:2:numTrials,...\",\n", + " \"'YTickLabel',0:2:numTrials);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"title('Response to Moving Visual Stimulus (Neuron 6)',...\",\n", + " \"'FontWeight','bold','Fontsize',14,'FontName','Arial');\",\n", + " \"cellNum=1; clear nst;\",\n", + " \"for i=1:numTrials\",\n", + " \"spikeTimes{i}=x.Results.Data.Spike_times_STC.balanced_SUA.spike_times{1,i,cellNum};\",\n", + " \"nst{i} = nspikeTrain(spikeTimes{i});\",\n", + " \"nst{i}.setName(num2str(cellNum));\",\n", + " \"end\",\n", + " \"spikeCollReal2=nstColl(nst);\",\n", + " \"spikeCollReal2.setMinTime(0); spikeCollReal2.setMaxTime(2);\",\n", + " \"subplot(2,2,4);spikeCollReal2.plot;\",\n", + " \"set(gca,'YTick',0:2:numTrials,'YTickLabel',0:2:numTrials);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"title('Response to Moving Visual Stimulus (Neuron 1)','FontWeight',...\",\n", + " \"'bold','Fontsize',14,'FontName','Arial');\",\n", + " \"close all;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"binsize = .05; %50ms window\",\n", + " \"psth = spikeCollSim.psth(binsize);\",\n", + " \"psthGLM = spikeCollSim.psthGLM(binsize);\",\n", + " \"true = lambda; %rate*delta = expected number of arrivals per bin\",\n", + " \"subplot(2,3,4);\",\n", + " \"h1=true.plot([],{{' ''b'',''Linewidth'',4'}});\",\n", + " \"h3=psthGLM.plot([],{{' ''k'',''Linewidth'',4'}});\",\n", + " \"h2=psth.plot([],{{' ''rx'',''Linewidth'',4'}});\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"ylabel('[spikes/sec]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"legend off;\",\n", + " \"h_legend=legend([h1(1) h2(1) h3(1)],'true','PSTH','PSTH_{glm}');\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.005 pos(2)+.095 pos(3:4)]);\",\n", + " \"subplot(2,3,1);spikeCollSim.plot;\",\n", + " \"set(gca,'YTick',0:2:spikeCollSim.numSpikeTrains,'YTickLabel',0:2:spikeCollSim.numSpikeTrains);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"subplot(2,3,5);\",\n", + " \"binsize = .05; %50ms window\",\n", + " \"psthReal1 = spikeCollReal1.psth(binsize);\",\n", + " \"psthGLMReal1 = spikeCollReal1.psthGLM(binsize);%,[],[],[],[],[],1000);\",\n", + " \"h3=psthGLMReal1.plot([],{{' ''k'',''Linewidth'',4'}});\",\n", + " \"h2=psthReal1.plot([],{{' ''rx'',''Linewidth'',4'}});\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('[spikes/sec]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"h_legend=legend([h2(1) h3(1)],'PSTH','PSTH_{glm}');\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.005 pos(2)+.07 pos(3:4)]);\",\n", + " \"subplot(2,3,2); spikeCollReal1.plot;\",\n", + " \"set(gca,'YTick',0:2:spikeCollReal2.numSpikeTrains,'YTickLabel',0:2:spikeCollReal2.numSpikeTrains);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"subplot(2,3,6);\",\n", + " \"psthReal2 = spikeCollReal2.psth(binsize);\",\n", + " \"psthGLMReal2 = spikeCollReal2.psthGLM(binsize);%,[],[],[],[],[],1000);\",\n", + " \"h3=psthGLMReal2.plot([],{{' ''k'',''Linewidth'',4'}});\",\n", + " \"h2=psthReal2.plot([],{{' ''rx'',''Linewidth'',4'}});\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('[spikes/sec]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"h_legend=legend([h2(1) h3(1)],'PSTH','PSTH_{glm}');\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.005 pos(2)+.07 pos(3:4)]);\",\n", + " \"subplot(2,3,3); spikeCollReal2.plot;\",\n", + " \"set(gca,'YTick',0:2:spikeCollReal2.numSpikeTrains,'YTickLabel',0:2:spikeCollReal2.numSpikeTrains);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"close all;\",\n", + " \"clear all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"delta = 0.001; Tmax = 1;\",\n", + " \"time = 0:delta:Tmax;\",\n", + " \"Ts=.001;\",\n", + " \"numRealizations = 50; %Each realization corresponds to a distinct trial\",\n", + " \"for i=1:numRealizations\",\n", + " \"f=2; b1(i)=3*((i)/numRealizations);b0=-3;\",\n", + " \"u = sin(2*pi*f*time);\",\n", + " \"e = zeros(length(time),1); %No Ensemble input\",\n", + " \"stim=Covariate(time',u,'Stimulus','time','s','Voltage',{'sin'});\",\n", + " \"ens =Covariate(time',e,'Ensemble','time','s','Spikes',{'n1'});\",\n", + " \"mu=b0;\",\n", + " \"histCoeffs=[-4 -1 -.5];\",\n", + " \"H=tf(histCoeffs,[1],Ts,'Variable','z^-1');\",\n", + " \"S=tf([b1(i)],1,Ts,'Variable','z^-1');\",\n", + " \"E=tf([0],1,Ts,'Variable','z^-1');\",\n", + " \"simTypeSelect='binomial'; %Parameters are used to compute\",\n", + " \"[sC, lambdaTemp]=CIF.simulateCIF(mu,H,S,E,stim,ens,1,simTypeSelect);\",\n", + " \"if(i==1)\",\n", + " \"lambda=lambdaTemp; %Store the conditional intensity function\",\n", + " \"else\",\n", + " \"lambda = lambda.merge(lambdaTemp); %Add it to the other realizations\",\n", + " \"end\",\n", + " \"nst{i} = sC.getNST(1); %get the neural spikeTrain from the collection\",\n", + " \"nst{i} = nst{i}.resample(1/delta); %make sure that it is sampled at the current samplerate\",\n", + " \"end\",\n", + " \"spikeColl = nstColl(nst); %Create a collection of the spike trains across trials\",\n", + " \"close all;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(3,2,[3 4]); spikeColl.plot;\",\n", + " \"set(gca,'ytick',0:10:numRealizations,'ytickLabel',0:10:numRealizations);\",\n", + " \"set(gca,'xtick',0:.1:Tmax,'xtickLabel',0:.1:Tmax); xlabel('');\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"title('Simulated Neural Raster','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',14,'FontWeight','bold');\",\n", + " \"stimData = exp(b0 + u'*b1);\",\n", + " \"if(strcmp(simTypeSelect,'binomial'))\",\n", + " \"stimData = stimData./(1+stimData);\",\n", + " \"end\",\n", + " \"subplot(3,2,1); plot(time,u,'k','LineWidth',3);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('Stimulus','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"title('Within Trial Stimulus','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',14,'FontWeight','bold');\",\n", + " \"subplot(3,2,2); plot(1:length(b1),b1,'k','LineWidth',3);\",\n", + " \"xlabel('Trial [k]','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"ylabel('Stimulus Gain','Interpreter','none','FontName', 'Arial','Fontsize',...\",\n", + " \"12,'FontWeight','bold');\",\n", + " \"title('Across Trial Stimulus Gain','Interpreter','none','FontName',...\",\n", + " \"'Arial','Fontsize',14,'FontWeight','bold');\",\n", + " \"subplot(3,2,[5 6]);\",\n", + " \"imagesc(stimData'./delta); set(gca, 'YDir','normal');\",\n", + " \"set(gca,'xtick',0:100:Tmax/delta,'xtickLabel',0:.1:Tmax);\",\n", + " \"set(gca,'ytick',0:10:numRealizations,'ytickLabel',0:10:numRealizations);\",\n", + " \"xlabel('time [s]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',...\",\n", + " \"'Fontsize',12,'FontWeight','bold');\",\n", + " \"title('True Conditional Intensity Function','Interpreter',...\",\n", + " \"'none','FontName', 'Arial','Fontsize',14,'FontWeight','bold');\",\n", + " \"axis tight;\",\n", + " \"stim = Covariate(time,sin(2*pi*f*time),'Stimulus','time','s','V',{'stim'});\",\n", + " \"baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',...\",\n", + " \"{'constant'});\",\n", + " \"windowTimes=[0:.001:.003];\",\n", + " \"numBasis = 25;\",\n", + " \"spikeColl.resample(1/delta); % Enforce sampleRate\",\n", + " \"spikeColl.setMaxTime(Tmax); % Make all spikeTrains end at time Tmax\",\n", + " \"dN=spikeColl.dataToMatrix'; % Convert the spikeTrains into a matrix\",\n", + " \"dN(dN>1)=1; % One should sample finely enough so there is\",\n", + " \"basisWidth=(spikeColl.maxTime-spikeColl.minTime)/numBasis;\",\n", + " \"if(simTypeSelect==0)\",\n", + " \"fitType='binomial';\",\n", + " \"else\",\n", + " \"fitType='poisson';\",\n", + " \"end\",\n", + " \"if(strcmp(fitType,'binomial'))\",\n", + " \"Algorithm = 'BNLRCG'; % BNLRCG - faster Truncated, L-2 Regularized,\",\n", + " \"else\",\n", + " \"Algorithm = 'GLM'; % Standard Matlab GLM (Can be used for binomial or\",\n", + " \"end\",\n", + " \"[psthSig, ~, psthResult] =spikeColl.psthGLM(basisWidth,windowTimes,fitType);\",\n", + " \"gamma0=psthResult.getHistCoeffs';%+.1*randn(size(histCoeffs));\",\n", + " \"gamma0(isnan(gamma0))=-5; % Depending on the amount of data the\",\n", + " \"x0=psthResult.getCoeffs; %The initial estimate for the SSGLM model\",\n", + " \"numVarEstIter=10;\",\n", + " \"Q0 = spikeColl.estimateVarianceAcrossTrials(numBasis,windowTimes,...\",\n", + " \"numVarEstIter,fitType);\",\n", + " \"A=eye(numBasis,numBasis);\",\n", + " \"delta = 1/spikeColl.sampleRate;\",\n", + " \"CompilingHelpFile=1;\",\n", + " \"if(~CompilingHelpFile)\",\n", + " \"Q0d=diag(Q0);\",\n", + " \"neuronName = psthResult.neuronNumber;\",\n", + " \"[xK,WK, WkuFinal,Qhat,gammahat,fitResults,stimulus,stimCIs,logll,...\",\n", + " \"QhatAll,gammahatAll,nIter]=DecodingAlgorithms.PPSS_EMFB(A,Q0d,x0,...\",\n", + " \"dN,fitType,delta,gamma0,windowTimes, numBasis,neuronName);\",\n", + " \"fR = fitResults.toStructure;\",\n", + " \"psthR = psthResult.toStructure;\",\n", + " \"end\",\n", + " \"installPath = which('nSTAT_Install');\",\n", + " \"if isempty(installPath)\",\n", + " \"error('nSTATPaperExamples:MissingInstallPath', ...\",\n", + " \"'Could not locate nSTAT_Install.m on MATLAB path.');\",\n", + " \"end\",\n", + " \"nstatRoot = fileparts(installPath);\",\n", + " \"if exist(nstatRoot,'dir') == 7 && ~strcmp(pwd,nstatRoot)\",\n", + " \"cd(nstatRoot);\",\n", + " \"end\",\n", + " \"addpath(nstatRoot,'-begin');\",\n", + " \"load(fullfile(nstatRoot,'data','SSGLMExampleData.mat'));\",\n", + " \"fitResults = FitResult.fromStructure(fR);\",\n", + " \"psthResult = FitResult.fromStructure(psthR);\",\n", + " \"t=psthResult.mergeResults(fitResults);\",\n", + " \"t.lambda.setDataLabels({'\\\\lambda_{PSTH}','\\\\lambda_{SSGLM}'});\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(2,2,1); t.KSPlot;\",\n", + " \"subplot(2,2,2); t.plotResidual;\",\n", + " \"subplot(2,2,3); t.plotInvGausTrans;\",\n", + " \"subplot(2,2,4); t.plotSeqCorr;\",\n", + " \"S=FitResSummary(t);\",\n", + " \"dAIC=diff(S.AIC)\",\n", + " \"dBIC=diff(S.BIC)\",\n", + " \"dKS =diff(S.KSStats);\",\n", + " \"close all;\",\n", + " \"minTime=0; maxTime = Tmax;\",\n", + " \"stimData = stim.data*b1;\",\n", + " \"if(strcmp(fitType,'poisson'))\",\n", + " \"actStimEffect=exp(stimData + b0)./delta;\",\n", + " \"elseif(strcmp(fitType,'binomial'))\",\n", + " \"actStimEffect=exp(stimData + b0)./(1+exp(stimData + b0))./delta;\",\n", + " \"end\",\n", + " \"if(~isempty(numBasis))\",\n", + " \"basisWidth = (maxTime-minTime)/numBasis;\",\n", + " \"sampleRate=1/delta;\",\n", + " \"unitPulseBasis=nstColl.generateUnitImpulseBasis(basisWidth,minTime,...\",\n", + " \"maxTime,sampleRate);\",\n", + " \"basisMat = unitPulseBasis.data;\",\n", + " \"end\",\n", + " \"if(strcmp(fitType,'poisson'))\",\n", + " \"estStimEffect=exp(basisMat*xK)./delta;\",\n", + " \"elseif(strcmp(fitType,'binomial'))\",\n", + " \"estStimEffect=exp(basisMat*xK)./(1+exp(basisMat*xK))./delta;\",\n", + " \"end\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.4 scrsz(4)*.8]);\",\n", + " \"subplot(3,1,[1 2 3]);\",\n", + " \"lighting gouraud\",\n", + " \"surf((1:length(b1))',stim.time,actStimEffect,'FaceAlpha',0.1,...\",\n", + " \"'EdgeAlpha',0.1,'AlphaData',0.1);\",\n", + " \"hx=xlabel('Trial [k]'); hy=ylabel('time [s]');\",\n", + " \"hz=zlabel('Stimulus Effect [spikes/sec]'); hold all;\",\n", + " \"set([hx hy hz],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"surf((1:length(b1))',stim.time,estStimEffect(:,1:length(b1)),...\",\n", + " \"'FaceAlpha',0.5,'EdgeAlpha',0.1,'AlphaData',0.5); %xlabel('Trial [k]'); ylabel('time [s]'); zlabel('Stimulus Effect');\",\n", + " \"set(gca,'YDir','reverse');\",\n", + " \"set(gca,'ytick',0:.1:Tmax,'ytickLabel',0:.1:Tmax);\",\n", + " \"title('SSGLM Estimated vs. Actual Stimulus Effect','FontWeight','bold',...\",\n", + " \"'Fontsize',14,...\",\n", + " \"'FontName','Arial');\",\n", + " \"close all;\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.4 scrsz(4)*.8]);\",\n", + " \"subplot(3,1,1);\",\n", + " \"lighting gouraud\",\n", + " \"mesh((1:length(b1))',stim.time,actStimEffect);\",\n", + " \"hx=xlabel('Trial [k]'); hy=ylabel('time [s]');\",\n", + " \"zlabel('Stimulus Effect [spikes/sec]'); hold all;\",\n", + " \"set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title('True Stimulus Effect','FontWeight','bold',...\",\n", + " \"'Fontsize',14,...\",\n", + " \"'FontName','Arial');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set(gca,'ytick',[],'ytickLabel',[]);\",\n", + " \"CLIM = [min(min(stimData./delta)) max(max(stimData./delta))];\",\n", + " \"view(gca,[90 -90]);\",\n", + " \"subplot(3,1,2);\",\n", + " \"lighting gouraud\",\n", + " \"mesh((1:length(b1))',stim.time,repmat(psthSig.data, [1 numRealizations]));\",\n", + " \"hx=xlabel('Trial [k]'); hy=ylabel('time [s]');\",\n", + " \"hz=zlabel('Stimulus Effect [spikes/sec]'); hold all;\",\n", + " \"set([hx hy hz],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title('PSTH Estimated Stimulus Effect','FontWeight','bold',...\",\n", + " \"'Fontsize',14,...\",\n", + " \"'FontName','Arial');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set(gca,'ytick',[],'ytickLabel',[]);\",\n", + " \"CLIM = [min(min(stimData./delta)) max(max(stimData./delta))];\",\n", + " \"view(gca,[90 -90]);\",\n", + " \"subplot(3,1,3);\",\n", + " \"lighting gouraud\",\n", + " \"mesh((1:length(b1))',stim.time,estStimEffect);\",\n", + " \"xlabel('Trial [k]'); ylabel('time [s]');\",\n", + " \"zlabel('Stimulus Effect [spikes/sec]'); hold all;\",\n", + " \"hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); hz=get(gca,'ZLabel');\",\n", + " \"set([hx hy hz],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title('SSGLM Estimated Stimulus Effect','FontWeight','bold',...\",\n", + " \"'Fontsize',14,...\",\n", + " \"'FontName','Arial');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set(gca,'ytick',[],'ytickLabel',[]);\",\n", + " \"set(gca, 'YDir','normal')\",\n", + " \"view(gca,[90 -90]);\",\n", + " \"echo off;\",\n", + " \"close all;\",\n", + " \"minTime=0; maxTime = Tmax;\",\n", + " \"if(~isempty(numBasis))\",\n", + " \"basisWidth = (maxTime-minTime)/numBasis;\",\n", + " \"sampleRate=1/delta;\",\n", + " \"unitPulseBasis=nstColl.generateUnitImpulseBasis(basisWidth,...\",\n", + " \"minTime,maxTime,sampleRate);\",\n", + " \"basisMat = unitPulseBasis.data;\",\n", + " \"end\",\n", + " \"t0=0; tf=Tmax;\",\n", + " \"[spikeRateBinom, ProbMat,sigMat]=DecodingAlgorithms.computeSpikeRateCIs(xK,...\",\n", + " \"WkuFinal,dN,t0,tf,fitType,delta,gammahat,windowTimes);\",\n", + " \"lt=find(sigMat(1,:)==1,1,'first');\",\n", + " \"display(['The learning trial (compared to the first trial) is trial #' ...\",\n", + " \"num2str(find(sigMat(1,:)==1,1,'first'))]);\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(2,3,1);\",\n", + " \"spikeRateBinom.setName(['(' num2str(Tmax) '-0)^-1*\\\\Lambda(0,' ...\",\n", + " \"num2str(Tmax) ')']);\",\n", + " \"spikeRateBinom.plot([],{{' ''k'',''Linewidth'',4'}});\",\n", + " \"v=axis;\",\n", + " \"plot(lt*[1;1],v(3:4),'r','Linewidth',2);\",\n", + " \"hx=xlabel('Trial [k]','Interpreter','none'); hold all;\",\n", + " \"hy=ylabel('Average Firing Rate [spikes/sec]','Interpreter','none');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title(['Learning Trial:' num2str(lt)],'FontWeight','bold',...\",\n", + " \"'Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"h=subplot(2,3,[2 3 5 6]);\",\n", + " \"K=size(dN,1);\",\n", + " \"colormap(flipud(gray));\",\n", + " \"imagesc(ProbMat); hold on;\",\n", + " \"for k=1:K\",\n", + " \"for m=(k+1):K\",\n", + " \"if(sigMat(k,m)==1)\",\n", + " \"plot3(m,k,1,'r*'); hold on;\",\n", + " \"end\",\n", + " \"end\",\n", + " \"end\",\n", + " \"set(h,'XAxisLocation','top','YAxisLocation','right');\",\n", + " \"hx=xlabel('Trial Number','Interpreter','none'); hold all;\",\n", + " \"hy=ylabel('Trial Number','Interpreter','none');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"subplot(2,3,4)\",\n", + " \"stim1 = Covariate(time, basisMat*stimulus(:,1),'Trial1','time','s',...\",\n", + " \"'spikes/sec');\",\n", + " \"temp = ConfidenceInterval(time, basisMat*squeeze(stimCIs(:,1,:)));\",\n", + " \"stim1.setConfInterval(temp);\",\n", + " \"stimlt = Covariate(time, basisMat*stimulus(:,lt),'Trial1','time','s',...\",\n", + " \"'spikes/sec');\",\n", + " \"temp = ConfidenceInterval(time, basisMat*squeeze(stimCIs(:,lt,:)));\",\n", + " \"temp.setColor('r');\",\n", + " \"stimlt.setConfInterval(temp);\",\n", + " \"stimltm1 = Covariate(time, basisMat*stimulus(:,lt-1),'Trial1','time','s',...\",\n", + " \"'spikes/sec');\",\n", + " \"temp = ConfidenceInterval(time, basisMat*squeeze(stimCIs(:,lt-1,:)));\",\n", + " \"temp.setColor('r');\",\n", + " \"stimltm1.setConfInterval(temp);\",\n", + " \"h1=stim1.plot([],{{' ''k'',''Linewidth'',4'}}); hold all;\",\n", + " \"h2=stimlt.plot([],{{' ''r'',''Linewidth'',4'}});\",\n", + " \"hx=xlabel('time [s]','Interpreter','none'); hold all;\",\n", + " \"hy=ylabel('Firing Rate [spikes/sec]','Interpreter','none');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title({'Learning Trial Vs. Baseline Trial';'with 95% CIs'},'FontWeight','bold',...\",\n", + " \"'Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"h_legend=legend([h1(1) h2(1)],'\\\\lambda_{1}(t)',['\\\\lambda_{' num2str(lt) '}(t)']);\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.03 pos(2)+.01 pos(3:4)]);\",\n", + " \"close all;\",\n", + " \"load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat'));\",\n", + " \"exampleCell = [2 21 25 49];\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.6 scrsz(4)*.9]);\",\n", + " \"for i=1:length(exampleCell)\",\n", + " \"subplot(2,2,i);\",\n", + " \"h1=plot(x,y,'b','Linewidth',.5); hold on;\",\n", + " \"h2=plot(neuron{exampleCell(i)}.xN,neuron{exampleCell(i)}.yN,'r.',...\",\n", + " \"'MarkerSize',7);\",\n", + " \"hx=xlabel('X Position'); hy=ylabel('Y Position');\",\n", + " \"title(['Cell#' num2str(exampleCell(i))],'FontWeight','bold',...\",\n", + " \"'Fontsize',12,'FontName','Arial');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"set(gca,'xTick',-1:.5:1,'yTick',-1:.5:1); axis square;\",\n", + " \"if(i==4)\",\n", + " \"h_legend = legend([h1 h2],'Animal Path',...\",\n", + " \"'Location at time of spike');\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.09 pos(2)+.06 pos(3:4)]);\",\n", + " \"end\",\n", + " \"end\",\n", + " \"numAnimals=2;\",\n", + " \"CompilingHelpFile=1;\",\n", + " \"if(~CompilingHelpFile)\",\n", + " \"for n=1:numAnimals\",\n", + " \"clear x y neuron time nst tc tcc z;\",\n", + " \"load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat']));\",\n", + " \"for i=1:length(neuron)\",\n", + " \"nst{i} = nspikeTrain(neuron{i}.spikeTimes);\",\n", + " \"end\",\n", + " \"[theta,r] = cart2pol(x,y);\",\n", + " \"cnt=0;\",\n", + " \"for l=0:3\",\n", + " \"for m=-l:l\",\n", + " \"if(~any(mod(l-m,2))) % otherwise the polynomial = 0\",\n", + " \"cnt = cnt+1;\",\n", + " \"z(:,cnt) = zernfun(l,m,r,theta,'norm');\",\n", + " \"end\",\n", + " \"end\",\n", + " \"end\",\n", + " \"delta=min(diff(time));\",\n", + " \"sampleRate = round(1/delta);\",\n", + " \"baseline = Covariate(time,ones(length(x),1),'Baseline','time','s','',...\",\n", + " \"{'mu'});\",\n", + " \"zernike = Covariate(time,z,'Zernike','time','s','m',{'z1','z2','z3',...\",\n", + " \"'z4','z5','z6','z7','z8','z9','z10'});\",\n", + " \"gaussian = Covariate(time,[x y x.^2 y.^2 x.*y],'Gaussian','time',...\",\n", + " \"'s','m',{'x','y','x^2','y^2','x*y'});\",\n", + " \"covarColl = CovColl({baseline,gaussian,zernike});\",\n", + " \"spikeColl = nstColl(nst);\",\n", + " \"trial = Trial(spikeColl,covarColl);\",\n", + " \"tc{1} = TrialConfig({{'Baseline','mu'},{'Gaussian',...\",\n", + " \"'x','y','x^2','y^2','x*y'}},sampleRate,[]);\",\n", + " \"tc{1}.setName('Gaussian');\",\n", + " \"tc{2} = TrialConfig({{'Zernike' 'z1','z2','z3','z4','z5','z6',...\",\n", + " \"'z7','z8','z9','z10'}},sampleRate,[]);\",\n", + " \"tc{2}.setName('Zernike');\",\n", + " \"tcc = ConfigColl(tc);\",\n", + " \"results =Analysis.RunAnalysisForAllNeurons(trial,tcc,0);\",\n", + " \"resStruct =FitResult.CellArrayToStructure(results);\",\n", + " \"filename = fullfile(dataDir,['PlaceCellAnimal' num2str(n) 'Results']);\",\n", + " \"save(filename,'resStruct');\",\n", + " \"end\",\n", + " \"end\",\n", + " \"clear Summary;\",\n", + " \"numAnimals =2;\",\n", + " \"for n=1:numAnimals\",\n", + " \"resData = load(fullfile(dataDir,['PlaceCellAnimal' num2str(n) 'Results.mat']));\",\n", + " \"results = FitResult.fromStructure(resData.resStruct);\",\n", + " \"Summary{n} = FitResSummary(results);\",\n", + " \"end\",\n", + " \"close all;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.6 scrsz(4)*.5]);\",\n", + " \"subplot(1,3,1);\",\n", + " \"maxLength = max([Summary{1}.numNeurons,Summary{2}.numNeurons]);\",\n", + " \"dKS = nan(maxLength, 2);\",\n", + " \"dKS(1:Summary{1}.numNeurons,1) = (Summary{1}.KSStats(:,1)-Summary{1}.KSStats(:,2)) ;\",\n", + " \"dKS(1:Summary{2}.numNeurons,2) = (Summary{2}.KSStats(:,1)-Summary{2}.KSStats(:,2)) ;\",\n", + " \"boxplot(dKS ,{'Animal 1', 'Animal 2'},'labelorientation','inline');\",\n", + " \"title('\\\\Delta KS Statistic','FontWeight','bold','FontSize',14,...\",\n", + " \"'FontName','Arial');\",\n", + " \"subplot(1,3,2);\",\n", + " \"dAIC = nan(maxLength, 2);\",\n", + " \"dAIC(1:Summary{1}.numNeurons,1) = Summary{1}.getDiffAIC(1);\",\n", + " \"dAIC(1:Summary{2}.numNeurons,2) = Summary{2}.getDiffAIC(1);\",\n", + " \"boxplot(dAIC ,{'Animal 1', 'Animal 2'},'labelorientation','inline');\",\n", + " \"title('\\\\Delta AIC','FontWeight','bold','FontSize',14,'FontName','Arial');\",\n", + " \"subplot(1,3,3);\",\n", + " \"dBIC = nan(maxLength, 2);\",\n", + " \"dBIC(1:Summary{1}.numNeurons,1) = Summary{1}.getDiffBIC(1);\",\n", + " \"dBIC(1:Summary{2}.numNeurons,2) = Summary{2}.getDiffBIC(1);\",\n", + " \"boxplot(dBIC ,{'Animal 1', 'Animal 2'},'labelorientation','inline'); %ylabel('\\\\Delta BIC'); %xticklabel_rotate([],45,[],'Fontsize',6);\",\n", + " \"title('\\\\Delta BIC','FontWeight','bold','FontSize',14,'FontName','Arial');\",\n", + " \"close all;\",\n", + " \"[x_new,y_new]=meshgrid(-1:.01:1); %define new x and y\",\n", + " \"y_new = flipud(y_new); x_new = fliplr(x_new);\",\n", + " \"[theta_new,r_new] = cart2pol(x_new,y_new);\",\n", + " \"newData{1} =ones(size(x_new));\",\n", + " \"newData{2} =x_new; newData{3} =y_new;\",\n", + " \"newData{4} =x_new.^2; newData{5} =y_new.^2;\",\n", + " \"newData{6} =x_new.*y_new;\",\n", + " \"idx = r_new<=1;\",\n", + " \"zpoly = cell(1,10);\",\n", + " \"cnt=0;\",\n", + " \"for l=0:3\",\n", + " \"for m=-l:l\",\n", + " \"if(~any(mod(l-m,2)))\",\n", + " \"cnt = cnt+1;\",\n", + " \"temp = nan(size(x_new));\",\n", + " \"temp(idx) = zernfun(l,m,r_new(idx),theta_new(idx),'norm');\",\n", + " \"zpoly{cnt} = temp;\",\n", + " \"end\",\n", + " \"end\",\n", + " \"end\",\n", + " \"for n=1:numAnimals\",\n", + " \"clear lambdaGaussian lambdaZernike;\",\n", + " \"load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat']));\",\n", + " \"resData = load(fullfile(dataDir,['PlaceCellAnimal' num2str(n) 'Results.mat']));\",\n", + " \"results = FitResult.fromStructure(resData.resStruct);\",\n", + " \"for i=1:length(neuron)\",\n", + " \"lambdaGaussian{i} = results{i}.evalLambda(1,newData);\",\n", + " \"lambdaZernike{i} = results{i}.evalLambda(2,zpoly);\",\n", + " \"end\",\n", + " \"for i=1:length(neuron)\",\n", + " \"if(n==1)\",\n", + " \"h4=figure(4);\",\n", + " \"colormap('jet');\",\n", + " \"if(i==1)\",\n", + " \"tb=annotation(h4,'textbox',...\",\n", + " \"[0.283261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Gaussian Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on','Fontsize',11,...\",\n", + " \"'FontName','Arial','FontWeight','bold','LineStyle',...\",\n", + " \"'none','HorizontalAlignment','center'); hold on;\",\n", + " \"end\",\n", + " \"subplot(7,7,i);\",\n", + " \"elseif(n==2)\",\n", + " \"h6=figure(6);\",\n", + " \"colormap('jet');\",\n", + " \"if(i==1)\",\n", + " \"annotation(h6,'textbox',...\",\n", + " \"[0.283261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Gaussian Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on','Fontsize',11,...\",\n", + " \"'FontName','Arial','FontWeight','bold','LineStyle',...\",\n", + " \"'none','HorizontalAlignment','center'); hold on;\",\n", + " \"end\",\n", + " \"subplot(6,7,i);\",\n", + " \"end\",\n", + " \"pcolor(x_new,y_new,lambdaGaussian{i}), shading interp\",\n", + " \"axis square; set(gca,'xtick',[],'ytick',[]);\",\n", + " \"set(gca, 'Box' , 'off');\",\n", + " \"if(n==1)\",\n", + " \"h5=figure(5);\",\n", + " \"colormap('jet');\",\n", + " \"if(i==1)\",\n", + " \"annotation(h5,'textbox',...\",\n", + " \"[0.303261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Zernike Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on','Fontsize',11,...\",\n", + " \"'FontName','Arial','FontWeight','bold','LineStyle','none'); hold on;\",\n", + " \"end\",\n", + " \"subplot(7,7,i);\",\n", + " \"elseif(n==2)\",\n", + " \"h7=figure(7);\",\n", + " \"colormap('jet');\",\n", + " \"if(i==1)\",\n", + " \"annotation(h7,'textbox',...\",\n", + " \"[0.303261904761904 0.928571428571418 ...\",\n", + " \"0.392857142857143 0.0595238095238095],...\",\n", + " \"'String',{['Zernike Place Fields - Animal#' ...\",\n", + " \"num2str(n)]},'FitBoxToText','on','Fontsize',11,...\",\n", + " \"'FontName','Arial','FontWeight','bold','LineStyle',...\",\n", + " \"'none','HorizontalAlignment','center'); hold on;\",\n", + " \"end\",\n", + " \"subplot(6,7,i);\",\n", + " \"end\",\n", + " \"pcolor(x_new,y_new,lambdaZernike{i}), shading interp\",\n", + " \"axis square;\",\n", + " \"set(gca,'xtick',[],'ytick',[]);\",\n", + " \"set(gca, 'Box' , 'off');\",\n", + " \"end\",\n", + " \"end\",\n", + " \"clear lambdaGaussian lambdaZernike;\",\n", + " \"load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat'));\",\n", + " \"resData = load(fullfile(dataDir,'PlaceCellAnimal1Results.mat'));\",\n", + " \"results = FitResult.fromStructure(resData.resStruct);\",\n", + " \"for i=1:length(neuron)\",\n", + " \"lambdaGaussian{i} = results{i}.evalLambda(1,newData);\",\n", + " \"lambdaZernike{i} = results{i}.evalLambda(2,zpoly);\",\n", + " \"end\",\n", + " \"exampleCell = 25;\",\n", + " \"close all;\",\n", + " \"h9=figure(9);\",\n", + " \"h_mesh = mesh(x_new,y_new,lambdaGaussian{exampleCell},'AlphaData',0);\",\n", + " \"get(h_mesh,'AlphaData');\",\n", + " \"set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','b');\",\n", + " \"hold on;\",\n", + " \"h_mesh = mesh(x_new,y_new,lambdaZernike{exampleCell},'AlphaData',0);\",\n", + " \"get(h_mesh,'AlphaData');\",\n", + " \"set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','g');\",\n", + " \"plot(x,y,neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.');\",\n", + " \"axis tight square;\",\n", + " \"xlabel('x position'); ylabel('y position');\",\n", + " \"title(['Animal#1, Cell#' num2str(exampleCell)],'FontWeight','bold',...\",\n", + " \"'Fontsize',12,'FontName','Arial');\",\n", + " \"hx=get(gca,'XLabel'); hy=get(gca,'YLabel');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"close all; clear all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"delta = 0.001; Tmax = 1;\",\n", + " \"time = 0:delta:Tmax;\",\n", + " \"numRealizations = 20;\",\n", + " \"f=2; b1=randn(numRealizations,1);b0=log(10*delta)+randn(numRealizations,1);\",\n", + " \"x = sin(2*pi*f*time);\",\n", + " \"clear nst;\",\n", + " \"for i=1:numRealizations\",\n", + " \"expData = exp(b1(i)*x+b0(i));\",\n", + " \"lambdaData = expData./(1+expData);\",\n", + " \"if(i==1)\",\n", + " \"lambda = Covariate(time,lambdaData./delta, ...\",\n", + " \"'\\\\Lambda(t)','time','s','spikes/sec',{'\\\\lambda_{1}'},...\",\n", + " \"{{' ''b'', ''LineWidth'' ,2'}});\",\n", + " \"else\",\n", + " \"tempLambda = Covariate(time,lambdaData./delta, ...\",\n", + " \"'\\\\Lambda(t)','time','s','spikes/sec',{'\\\\lambda_{1}'},...\",\n", + " \"{{' ''b'', ''LineWidth'' ,2'}});\",\n", + " \"lambda = lambda.merge(tempLambda);\",\n", + " \"end\",\n", + " \"spikeColl = CIF.simulateCIFByThinningFromLambda(...\",\n", + " \"lambda.getSubSignal(i),1);\",\n", + " \"nst{i} = spikeColl.getNST(1);\",\n", + " \"end\",\n", + " \"spikeColl = nstColl(nst);scrsz = get(0,'ScreenSize');\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 ...\",\n", + " \"scrsz(3)*.6 scrsz(4)*.8]);\",\n", + " \"subplot(3,1,1); plot(time,x,'k');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]); ylabel('Stimulus');\",\n", + " \"hx=get(gca,'XLabel'); hy=get(gca,'YLabel');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title('Driving Stimulus','FontWeight','bold',...\",\n", + " \"'FontSize',14,'FontName','Arial');\",\n", + " \"subplot(3,1,2); lambda.plot([],{{' ''k'',''Linewidth'',1'}});\",\n", + " \"legend off;\",\n", + " \"hy=ylabel('Firing Rate [spikes/sec]', 'Interpreter','none');\",\n", + " \"hx=xlabel('','Interpreter','none');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,...\",\n", + " \"'FontWeight','bold');\",\n", + " \"set(gca,'xtickLabel',[]);\",\n", + " \"title('Conditional Intensity Functions','FontWeight',...\",\n", + " \"'bold','FontSize',14,'FontName','Arial');\",\n", + " \"subplot(3,1,3); spikeColl.plot;\",\n", + " \"set(gca,'ytick',0:10:numRealizations,'ytickLabel',...\",\n", + " \"0:10:numRealizations);\",\n", + " \"xlabel('time [s]','Interpreter','none');\",\n", + " \"ylabel('Cell Number','Interpreter','none');\",\n", + " \"hx=get(gca,'XLabel'); hy=get(gca,'YLabel');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title('Point Process Sample Paths','FontWeight',...\",\n", + " \"'bold','FontSize',14,'FontName','Arial');\",\n", + " \"stim = Covariate(time,sin(2*pi*f*time),'Stimulus','time','s','V',{'stim'});\",\n", + " \"baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',...\",\n", + " \"{'constant'});\",\n", + " \"close all;\",\n", + " \"clear lambdaCIF;\",\n", + " \"spikeColl.resample(1/delta);\",\n", + " \"dN=spikeColl.dataToMatrix;\",\n", + " \"Q=std(stim.data(2:end)-stim.data(1:end-1));\",\n", + " \"Px0=.1; A=1;\",\n", + " \"x0 = x(:,1); yT=x(:,end);\",\n", + " \"Pi0 = eps*eye(size(x0,1),size(x0,1));\",\n", + " \"PiT = eps*eye(size(x0,1),size(x0,1));\",\n", + " \"[x_p, W_p, x_u, W_u] = DecodingAlgorithms.PPDecodeFilterLinear(A, ...\",\n", + " \"Q, dN',b0,b1','binomial',delta);\",\n", + " \"h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.6]);\",\n", + " \"zVal=1.96;\",\n", + " \"ciLower = min(x_u(1:end)-zVal*sqrt(squeeze(W_u(1:end)))',...\",\n", + " \"x_u(1:end)+zVal*sqrt(squeeze(W_u(1:end))'));\",\n", + " \"ciUpper = max(x_u(1:end)-zVal*sqrt(squeeze(W_u(1:end)))',...\",\n", + " \"x_u(1:end)+zVal*sqrt(squeeze(W_u(1:end))'));\",\n", + " \"estimatedStimulus = Covariate(time,x_u(1:end),'\\\\hat{x}(t)','time','s','');\",\n", + " \"CI= ConfidenceInterval(time,[ciLower', ciUpper'],'\\\\hat{x}(t)','time','s','');\",\n", + " \"estimatedStimulus.setConfInterval(CI);\",\n", + " \"hEst = estimatedStimulus.plot([],{{' ''k'',''Linewidth'',4'}});\",\n", + " \"hStim=stim.plot([],{{' ''b'',''Linewidth'',4'}});\",\n", + " \"legend off;\",\n", + " \"h_legend=legend([hEst(1) hStim],'Decoded','Actual');\",\n", + " \"set(h_legend,'Interpreter','none');\",\n", + " \"set(h_legend,'FontSize',22);\",\n", + " \"title(['Decoded Stimulus +/- 95% CIs with ' num2str(numRealizations) ' cells'],...\",\n", + " \"'FontWeight','bold','Fontsize',22,'FontName','Arial');\",\n", + " \"xlabel('time [s]','Interpreter','none');\",\n", + " \"ylabel('Stimulus','Interpreter','none');\",\n", + " \"hx=get(gca,'XLabel'); hy=get(gca,'YLabel');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',22,'FontWeight','bold');\",\n", + " \"close all;\",\n", + " \"clear all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"q=1e-4;\",\n", + " \"Q=diag([1e-12 1e-12 q q]);\",\n", + " \"delta = .001; % Time increment\",\n", + " \"r=1e-6; % in meters^2\",\n", + " \"p=1e-6; % in meters^2/s^2\",\n", + " \"PiT=diag([r r p p]); % Uncertainty in the target state\",\n", + " \"Pi0=PiT;\",\n", + " \"T=2; % Reach Duration\",\n", + " \"x0 = [0;0;0;0]; % Initial Position and velocities (states)\",\n", + " \"xT = [-.35;.2; 0;0];% Final Target\",\n", + " \"time=0:delta:T; % time vector\",\n", + " \"A=[1 0 delta 0 ; %State transition matrix\",\n", + " \"0 1 0 delta;\",\n", + " \"0 0 1 0 ;\",\n", + " \"0 0 0 1 ];\",\n", + " \"x=zeros(4,length(time));\",\n", + " \"R=chol(Q);\",\n", + " \"L=chol(PiT);\",\n", + " \"for k=1:length(time)\",\n", + " \"if(k==1)\",\n", + " \"x(:,k)=x0;\",\n", + " \"else\",\n", + " \"x(:,k)=A*x(:,k-1)+...\",\n", + " \"delta/(2)*(pi/T)^2*cos(pi*time(k)/T)*[0;0;...\",\n", + " \"xT(1)-x0(1);xT(2)-x0(2)]; %Reach to target model\",\n", + " \"end\",\n", + " \"end\",\n", + " \"xT =x(:,end); % The target generated by the model\",\n", + " \"yT=xT; % Assume we have observed the actual target position with uncertainty PiT\",\n", + " \"Q=diag(var(diff(x,[],2),[],2))*100;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"fig1=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 ...\",\n", + " \"scrsz(3)*.8 scrsz(4)*.8]);\",\n", + " \"subplot(4,2,[1 3]);\",\n", + " \"plot(100*x(1,:),100*x(2,:),'k','Linewidth',2);\",\n", + " \"xlabel('X Position [cm]'); ylabel('Y Position [cm]');\",\n", + " \"hx=get(gca,'XLabel'); hy=get(gca,'YLabel');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"title('Reach Path','FontWeight','bold','Fontsize',14,'FontName','Arial');\",\n", + " \"hold on;\",\n", + " \"axis([sort([100*x0(1)+5, 100*xT(1)-5]), sort([100*x0(2)-5, 100*xT(2)+5])]);\",\n", + " \"h1=plot(100*x(1,1),100*x(2,1),'bo','MarkerSize',14);\",\n", + " \"h2=plot(100*x(1,end),100*x(2,end),'ro','MarkerSize',14);\",\n", + " \"legend([h1 h2],'Start','Finish','Location','NorthEast');\",\n", + " \"subplot(4,2,5); h1=plot(time,100*x(1,:),'k','Linewidth',2); hold on;\",\n", + " \"h2=plot(time,100*x(2,:),'k-.','Linewidth',2);\",\n", + " \"h_legend=legend([h1,h2],'x','y','Location','NorthEast');\",\n", + " \"set(h_legend,'FontSize',14)\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.06 pos(2)+.01 pos(3:4)]);\",\n", + " \"hx=xlabel('time [s]'); hy=ylabel('Position [cm]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"subplot(4,2,7);\",\n", + " \"h1=plot(time,100*x(3,:),'k','Linewidth',2); hold on;\",\n", + " \"h2=plot(time,100*x(4,:),'k-.','Linewidth',2);\",\n", + " \"h_legend=legend([h1 h2],'v_x','v_y','Location','NorthEast');\",\n", + " \"xlabel('time [s]');\",\n", + " \"set(h_legend,'FontSize',14);\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)+.06 pos(2)+.01 pos(3:4)]);\",\n", + " \"hx=xlabel('time [s]'); hy=ylabel('Velocity [cm/s]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"gamma=0;\",\n", + " \"windowTimes=[0, 0.001];\",\n", + " \"numCells = 20;\",\n", + " \"bCoeffs=10*(rand(numCells,2)-.5); % b_i = [b_x_i b_y_i] ~ U(-5, 5);\",\n", + " \"phiMax = atan2(bCoeffs(:,2),bCoeffs(:,1)); % Maximal firing direction of cell\",\n", + " \"phiMaxNorm = (phiMax+pi)./(2*pi);\",\n", + " \"meanMu = log(10*delta); % baseline firing rate -10Hz\",\n", + " \"MuCoeffs = meanMu+randn(numCells,1); % mu_i ~ G(meanMu,1)\",\n", + " \"dataMat = [ones(length(time),1) x(3,:)' x(4,:)']; % design matrix: X (\",\n", + " \"coeffs = [MuCoeffs bCoeffs]; % coefficient vector: beta\",\n", + " \"fitType='binomial';\",\n", + " \"clear nst;\",\n", + " \"for i=1:numCells\",\n", + " \"tempData = exp(dataMat*coeffs(i,:)');\",\n", + " \"if(strcmp(fitType,'poisson'))\",\n", + " \"lambdaData = tempData;\",\n", + " \"else\",\n", + " \"lambdaData = tempData./(1+tempData); % Conditional Intensity Function for ith cell\",\n", + " \"end\",\n", + " \"lambda{i}=Covariate(time,lambdaData./delta, ...\",\n", + " \"'\\\\Lambda(t)','time','s','spikes/sec',...\",\n", + " \"{strcat('\\\\lambda_{',num2str(i),'}')},{{' ''b'' '}});\",\n", + " \"lambda{i}=lambda{i}.resample(1/delta);\",\n", + " \"lambdaCIF{i} = CIF([MuCoeffs(i) 0 0 bCoeffs(i,:)],...\",\n", + " \"{'1','x','y','vx','vy'},{'x','y','vx','vy'},fitType);\",\n", + " \"tempSpikeColl{i} = CIF.simulateCIFByThinningFromLambda(lambda{i},1); nst{i} = tempSpikeColl{i}.getNST(1); % grab the realization\",\n", + " \"nst{i}.setName(num2str(i)); % give each cell a unique name\",\n", + " \"subplot(4,2,[6 8]);\",\n", + " \"h2=lambda{i}.plot([],{{' ''k'', ''LineWidth'' ,.5'}});\",\n", + " \"legend off; hold all; % Plot the CIF\",\n", + " \"end\",\n", + " \"title('Neural Conditional Intensity Functions','FontWeight',...\",\n", + " \"'bold','Fontsize',14,'FontName','Arial');\",\n", + " \"hx=xlabel('time [s]','Interpreter','none');\",\n", + " \"hy=ylabel('Firing Rate [spikes/sec]','Interpreter','none');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"spikeColl = nstColl(nst); % Create a neural spike train collection\",\n", + " \"subplot(4,2,[2,4]); spikeColl.plot;\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"title('Neural Raster','FontWeight','bold','Fontsize',14,...\",\n", + " \"'FontName','Arial');\",\n", + " \"hx=xlabel('time [s]','Interpreter','none');\",\n", + " \"hy=ylabel('Cell Number','Interpreter','none');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold');\",\n", + " \"close all;\",\n", + " \"numExamples=20;\",\n", + " \"scrsz = get(0,'ScreenSize');\",\n", + " \"fig1=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 ...\",\n", + " \"scrsz(3)*.6 scrsz(4)*.9]);\",\n", + " \"for k=1:numExamples\",\n", + " \"bCoeffs=10*(rand(numCells,2)-.5); % b_i = [b_x_i b_y_i] ~ U(-5, 5);\",\n", + " \"phiMax = atan2(bCoeffs(:,2),bCoeffs(:,1)); % Maximal firing direction of cell\",\n", + " \"phiMaxNorm = (phiMax+pi)./(2*pi);\",\n", + " \"meanMu = log(10*delta); % baseline firing rate\",\n", + " \"MuCoeffs = meanMu+randn(numCells,1); % mu_i ~ G(meanMu,1)\",\n", + " \"dataMat = [ones(length(time),1) x(3,:)' x(4,:)']; % design matrix: X (\",\n", + " \"coeffs = [MuCoeffs bCoeffs]; % coefficient vector: beta\",\n", + " \"fitType='binomial';\",\n", + " \"clear nst lambda;\",\n", + " \"for i=1:numCells\",\n", + " \"tempData = exp(dataMat*coeffs(i,:)');\",\n", + " \"if(strcmp(fitType,'poisson'))\",\n", + " \"lambdaData = tempData;\",\n", + " \"else\",\n", + " \"lambdaData = tempData./(1+tempData);\",\n", + " \"end\",\n", + " \"lambda{i}=Covariate(time,lambdaData./delta, ...\",\n", + " \"'\\\\Lambda(t)','time','s','spikes/sec',...\",\n", + " \"{strcat('\\\\lambda_{',num2str(i),'}')},{{' ''b'' '}});\",\n", + " \"lambda{i}=lambda{i}.resample(1/delta);\",\n", + " \"tempSpikeColl{i} = CIF.simulateCIFByThinningFromLambda(lambda{i},1);\",\n", + " \"nst{i} = tempSpikeColl{i}.getNST(1); % grab the realization\",\n", + " \"nst{i}.setName(num2str(i)); % give each cell a unique name\",\n", + " \"end\",\n", + " \"spikeColl = nstColl(nst); % Create a neural spike train collection\",\n", + " \"dN=spikeColl.dataToMatrix';\",\n", + " \"dN(dN>1)=1; % more than one spike per bin will be treated as one spike. In\",\n", + " \"[C,N] = size(dN); % N time samples, C cells\",\n", + " \"beta=[zeros(2,numCells); bCoeffs'];\",\n", + " \"[x_p, W_p, x_u, W_u,x_uT,W_uT,x_pT,W_pT] = ...\",\n", + " \"DecodingAlgorithms.PPDecodeFilterLinear(A, Q, dN,...\",\n", + " \"MuCoeffs,beta,fitType,delta,gamma,windowTimes,x0, Pi0, yT,PiT,0);\",\n", + " \"[x_pf, W_pf, x_uf, W_uf] = ...\",\n", + " \"DecodingAlgorithms.PPDecodeFilterLinear(A, Q, dN,...\",\n", + " \"MuCoeffs,beta,fitType,delta,gamma,windowTimes,x0);\",\n", + " \"if(k==numExamples)\",\n", + " \"subplot(4,2,1:4);h1=plot(100*x(1,:),100*x(2,:),'k','LineWidth',3);\",\n", + " \"hold on;\",\n", + " \"axis([sort([100*x0(1)+5, 100*xT(1)-5]), ...\",\n", + " \"sort([100*x0(2)-5, 100*xT(2)+5])]);\",\n", + " \"title('Estimated vs. Actual Reach Paths',...\",\n", + " \"'FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"end\",\n", + " \"subplot(4,2,1:4);h2=plot(100*x_u(1,:)',100*x_u(2,:)','b'); hold all;\",\n", + " \"subplot(4,2,1:4);h3=plot(100*x_uf(1,:)',100*x_uf(2,:)','g');\",\n", + " \"hx=xlabel('x [cm]'); hy=ylabel('y [cm]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"h1=plot(100*x0(1),100*x0(2),'bo','MarkerSize',10); hold on;\",\n", + " \"h2=plot(100*xT(1),100*xT(2),'ro','MarkerSize',10);\",\n", + " \"legend([h1 h2],'Start','Finish','Location','NorthEast');\",\n", + " \"subplot(4,2,5);\",\n", + " \"h1=plot(time,100*x(1,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*x_u(1,:)','b');\",\n", + " \"h3=plot(time,100*x_uf(1,:)','g');\",\n", + " \"hy=ylabel('x(t) [cm]'); hx=xlabel('time [s]');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('X Position','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"subplot(4,2,6);\",\n", + " \"h1=plot(time,100*x(2,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*x_u(2,:)','b');\",\n", + " \"h3=plot(time,100*x_uf(2,:)','g');\",\n", + " \"h_legend=legend([h1(1) h2(1) h3(1)],'Actual','PPAF+Goal',...\",\n", + " \"'PPAF','Location','SouthEast');\",\n", + " \"hy=ylabel('y(t) [cm]'); hx=xlabel('time [s]');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('Y Position','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"set(h_legend,'FontSize',10)\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)-.63 pos(2)+.23 pos(3:4)]);\",\n", + " \"subplot(4,2,7);\",\n", + " \"h1=plot(time,100*x(3,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*x_u(3,:)','b');\",\n", + " \"h3=plot(time,100*x_uf(3,:)','g');\",\n", + " \"hy=ylabel('v_{x}(t) [cm/s]'); hx=xlabel('time [s]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('X Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"subplot(4,2,8);\",\n", + " \"h1=plot(time,100*x(4,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*x_u(4,:)','b');\",\n", + " \"h3=plot(time,100*x_uf(4,:)','g');\",\n", + " \"hy=ylabel('v_{y}(t) [cm/s]'); hx=xlabel('time [s]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('Y Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"end\",\n", + " \"clear all;\",\n", + " \"[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs();\",\n", + " \"close all;\",\n", + " \"delta=0.001;\",\n", + " \"Tmax=2;\",\n", + " \"time=0:delta:Tmax;\",\n", + " \"A{2} = [1 0 delta 0 delta^2/2 0;\",\n", + " \"0 1 0 delta 0 delta^2/2;\",\n", + " \"0 0 1 0 delta 0;\",\n", + " \"0 0 0 1 0 delta;\",\n", + " \"0 0 0 0 1 0;\",\n", + " \"0 0 0 0 0 1];\",\n", + " \"A{1} = [1 0 0 0 0 0;\",\n", + " \"0 1 0 0 0 0;\",\n", + " \"0 0 0 0 0 0;\",\n", + " \"0 0 0 0 0 0;\",\n", + " \"0 0 0 0 0 0;\",\n", + " \"0 0 0 0 0 0];\",\n", + " \"A{1} = [1 0;\",\n", + " \"0 1];\",\n", + " \"Px0{2} =1e-6*eye(6,6);\",\n", + " \"Px0{1} =1e-6*eye(2,2);\",\n", + " \"minCovVal = 1e-12;\",\n", + " \"covVal = 1e-3;\",\n", + " \"Q{2}=[minCovVal 0 0 0 0 0;\",\n", + " \"0 minCovVal 0 0 0 0;\",\n", + " \"0 0 minCovVal 0 0 0;\",\n", + " \"0 0 0 minCovVal 0 0;\",\n", + " \"0 0 0 0 covVal 0;\",\n", + " \"0 0 0 0 0 covVal];\",\n", + " \"Q{1}=minCovVal*eye(2,2);\",\n", + " \"mstate = zeros(1,length(time));\",\n", + " \"ind{1}=1:2;\",\n", + " \"ind{2}=1:6;\",\n", + " \"X=zeros(max([size(A{1},1),size(A{2},1)]),length(time));\",\n", + " \"p_ij = [.998 .002;\",\n", + " \".001 .999];\",\n", + " \"for i = 1:length(time)\",\n", + " \"if(i==1)\",\n", + " \"mstate(i) = 1;\",\n", + " \"else\",\n", + " \"if(rand(1,1)1)=1; %Avoid more than 1 spike per bin.\",\n", + " \"Mu0=.5*ones(size(p_ij,1),1);\",\n", + " \"clear x0 yT clear Pi0 PiT;\",\n", + " \"x0{1} = X(ind{1},1);\",\n", + " \"yT{1} = X(ind{1},end);\",\n", + " \"Pi0 = Px0;\",\n", + " \"PiT{1} = 1e-9*eye(size(x0{1},1),size(x0{1},1));\",\n", + " \"x0{2} = X(ind{2},1);\",\n", + " \"yT{2} = X(ind{2},end);\",\n", + " \"PiT{2} = 1e-9*eye(size(x0{2},1),size(x0{2},1));\",\n", + " \"[S_est, X_est, W_est, MU_est, X_s, W_s,pNGivenS]=...\",\n", + " \"DecodingAlgorithms.PPHybridFilterLinear(A, Q, p_ij,Mu0, dN',...\",\n", + " \"coeffs(:,1),coeffs(:,2:end)',fitType,delta,[],[],x0,Pi0, yT,PiT);\",\n", + " \"[S_estNT, X_estNT, W_estNT, MU_estNT, X_sNT, W_sNT,pNGivenSNT]=...\",\n", + " \"DecodingAlgorithms.PPHybridFilterLinear(A, Q, p_ij,Mu0, dN',...\",\n", + " \"coeffs(:,1),coeffs(:,2:end)',fitType,delta,[],[],x0,Pi0);\",\n", + " \"X_estAll(:,:,n) = X_est;\",\n", + " \"X_estNTAll(:,:,n) = X_estNT;\",\n", + " \"S_estAll(n,:)=S_est;\",\n", + " \"S_estNTAll(n,:)=S_estNT;\",\n", + " \"MU_estAll(:,:,n)=MU_est;\",\n", + " \"MU_estNTAll(:,:,n) = MU_estNT;\",\n", + " \"subplot(4,3,[1 4]);\",\n", + " \"plot(time,mstate,'k','LineWidth',3); hold all;\",\n", + " \"plot(time,S_est,'b-.','Linewidth',.5);\",\n", + " \"plot(time,S_estNT,'g-.','Linewidth',.5); axis tight; v=axis;\",\n", + " \"axis([v(1) v(2) 0.5 2.5]);\",\n", + " \"subplot(4,3,[7 10]);\",\n", + " \"plot(time,MU_est(2,:),'b-.','Linewidth',.5); hold on;\",\n", + " \"plot(time,MU_estNT(2,:),'g-.','Linewidth',.5); hold on;\",\n", + " \"axis([min(time) max(time) 0 1.1]);\",\n", + " \"subplot(4,3,[2 3 5 6]);\",\n", + " \"h1=plot(100*X(1,:)',100*X(2,:)','k'); hold all;\",\n", + " \"h2=plot(100*X_est(1,:)',100*X_est(2,:)','b-.'); hold all;\",\n", + " \"h3=plot(100*X_estNT(1,:)',100*X_estNT(2,:)','g-.');\",\n", + " \"subplot(4,3,8);\",\n", + " \"h1=plot(time,100*X(1,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*X_est(1,:)','b-.');\",\n", + " \"h3=plot(time,100*X_estNT(1,:)','g-.');\",\n", + " \"subplot(4,3,9);\",\n", + " \"h1=plot(time,100*X(2,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*X_est(2,:)','b-.');\",\n", + " \"h3=plot(time,100*X_estNT(2,:)','g-.');\",\n", + " \"subplot(4,3,11);\",\n", + " \"h1=plot(time,100*X(3,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*X_est(3,:)','b-.');\",\n", + " \"h3=plot(time,100*X_estNT(3,:)','g-.');\",\n", + " \"subplot(4,3,12);\",\n", + " \"h1=plot(time,100*X(4,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,100*X_est(4,:)','b-.');\",\n", + " \"h3=plot(time,100*X_estNT(4,:)','g-.');\",\n", + " \"end\",\n", + " \"subplot(4,3,[1 4]);\",\n", + " \"hold all;\",\n", + " \"plot(time,mstate,'k','LineWidth',3);\",\n", + " \"plot(time,mean(S_estAll),'b','LineWidth',3);\",\n", + " \"plot(time,mean(S_estNTAll),'g','LineWidth',3);\",\n", + " \"set(gca,'xtick',[],'YTick',[1 2.1],'YTickLabel',{'N','M'});\",\n", + " \"hy=ylabel('state'); hx=xlabel('time [s]');\",\n", + " \"set([hy hx],'FontName', 'Arial','FontSize',10,'FontWeight','bold',...\",\n", + " \"'Interpreter','none');\",\n", + " \"title('Estimated vs. Actual State','FontWeight','bold','Fontsize',...\",\n", + " \"12,'FontName','Arial');\",\n", + " \"subplot(4,3,[7 10]);\",\n", + " \"plot(time, mean(squeeze(MU_estAll(2,:,:)),2),'b','LineWidth',3);\",\n", + " \"hold on;\",\n", + " \"plot(time,mean(squeeze(MU_estNTAll(2,:,:)),2),'g','LineWidth',3);\",\n", + " \"hold on;\",\n", + " \"axis([min(time) max(time) 0 1.1]);\",\n", + " \"hx=xlabel('time [s]'); hy=ylabel('P(s(t)=M | data)');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('Probability of State','FontWeight','bold','Fontsize',12,...\",\n", + " \"'FontName','Arial');\",\n", + " \"subplot(4,3,[2 3 5 6]);\",\n", + " \"h1=plot(100*X(1,:)',100*X(2,:)','k'); hold all;\",\n", + " \"mXestAll=mean(100*X_estAll,3);\",\n", + " \"mXestNTAll=mean(100*X_estNTAll,3);\",\n", + " \"plot(mXestAll(1,:),mXestAll(2,:),'b','Linewidth',3);\",\n", + " \"plot(mXestNTAll(1,:),mXestNTAll(2,:),'g','Linewidth',3);\",\n", + " \"hx=xlabel('x [cm]'); hy=ylabel('y [cm]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"h1=plot(100*X(1,1),100*X(2,1),'bo','MarkerSize',14); hold on;\",\n", + " \"h2=plot(100*X(1,end),100*X(2,end),'ro','MarkerSize',14);\",\n", + " \"legend([h1 h2],'Start','Finish','Location','NorthEast');\",\n", + " \"title('Estimated vs. Actual Reach Path','FontWeight','bold',...\",\n", + " \"'Fontsize',12,'FontName','Arial');\",\n", + " \"subplot(4,3,8);\",\n", + " \"h1=plot(time,100*X(1,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,mXestAll(1,:),'b','LineWidth',3); hold on;\",\n", + " \"h3=plot(time,mXestNTAll(1,:),'g','LineWidth',3); hold on;\",\n", + " \"hy=ylabel('x(t) [cm]'); hx=xlabel('time [s]');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('X Position','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"subplot(4,3,9);\",\n", + " \"h1=plot(time,100*X(2,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,mXestAll(2,:),'b','LineWidth',3); hold on;\",\n", + " \"h3=plot(time,mXestNTAll(2,:),'g','LineWidth',3); hold on;\",\n", + " \"h_legend=legend([h1(1) h2(1) h3(1)],'Actual','PPAF+Goal',...\",\n", + " \"'PPAF','Location','SouthEast');\",\n", + " \"hy=ylabel('y(t) [cm]'); hx=xlabel('time [s]');\",\n", + " \"set(gca,'xtick',[],'xtickLabel',[]);\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('Y Position','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"set(h_legend,'FontSize',10)\",\n", + " \"pos = get(h_legend,'position');\",\n", + " \"set(h_legend, 'position',[pos(1)-.40 pos(2)+.51 pos(3:4)]);\",\n", + " \"subplot(4,3,11);\",\n", + " \"h1=plot(time,100*X(3,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,mXestAll(3,:),'b','LineWidth',3); hold on;\",\n", + " \"h3=plot(time,mXestNTAll(3,:),'g','LineWidth',3); hold on;\",\n", + " \"hy=ylabel('v_{x}(t) [cm/s]'); hx=xlabel('time [s]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('X Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"subplot(4,3,12);\",\n", + " \"h1=plot(time,100*X(4,:),'k','LineWidth',3); hold on;\",\n", + " \"h2=plot(time,mXestAll(4,:),'b','LineWidth',3); hold on;\",\n", + " \"h3=plot(time,mXestNTAll(4,:),'g','LineWidth',3); hold on;\",\n", + " \"hy=ylabel('v_{y}(t) [cm/s]'); hx=xlabel('time [s]');\",\n", + " \"set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold');\",\n", + " \"title('Y Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial');\",\n", + " \"parity = struct();\",\n", + " \"if exist('numCells','var')\",\n", + " \"parity.num_cells = numCells;\",\n", + " \"end\",\n", + " \"if exist('numRealizations','var')\",\n", + " \"parity.num_realizations = numRealizations;\",\n", + " \"end\",\n", + " \"function [dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ...\",\n", + " \"getPaperDataDirs()\",\n", + " \"candidateRoots = {};\",\n", + " \"scriptPath = mfilename('fullpath');\",\n", + " \"if ~isempty(scriptPath)\",\n", + " \"candidateRoots = appendCandidateRoot(candidateRoots, fileparts(fileparts(scriptPath)));\",\n", + " \"end\",\n", + " \"paperPath = which('nSTATPaperExamples');\",\n", + " \"if ~isempty(paperPath)\",\n", + " \"candidateRoots = appendCandidateRoot(candidateRoots, fileparts(fileparts(paperPath)));\",\n", + " \"end\",\n", + " \"installPath = which('nSTAT_Install');\",\n", + " \"if ~isempty(installPath)\",\n", + " \"candidateRoots = appendCandidateRoot(candidateRoots, fileparts(installPath));\",\n", + " \"end\",\n", + " \"try\",\n", + " \"activeFile = matlab.desktop.editor.getActiveFilename;\",\n", + " \"catch\",\n", + " \"activeFile = '';\",\n", + " \"end\",\n", + " \"if ~isempty(activeFile)\",\n", + " \"candidateRoots = appendCandidateRoot(candidateRoots, fileparts(fileparts(activeFile)));\",\n", + " \"end\",\n", + " \"candidateRoots = appendCandidateRoot(candidateRoots, pwd);\",\n", + " \"nSTATDir = '';\",\n", + " \"for iRoot = 1:numel(candidateRoots)\",\n", + " \"candidateDataDir = fullfile(candidateRoots{iRoot}, 'data');\",\n", + " \"if exist(candidateDataDir, 'dir') == 7\",\n", + " \"nSTATDir = candidateRoots{iRoot};\",\n", + " \"break;\",\n", + " \"end\",\n", + " \"end\",\n", + " \"if isempty(nSTATDir)\",\n", + " \"error('nSTATPaperExamples:MissingInstallPath', ...\",\n", + " \"['Could not resolve the nSTAT root path. Checked roots derived from ', ...\",\n", + " \"'mfilename, which(''nSTATPaperExamples''), which(''nSTAT_Install''), ', ...\",\n", + " \"'the active editor file, and pwd.']);\",\n", + " \"end\",\n", + " \"dataDir = fullfile(nSTATDir,'data');\",\n", + " \"mEPSCDir = fullfile(dataDir,'mEPSCs');\",\n", + " \"explicitStimulusDir = fullfile(dataDir,'Explicit Stimulus');\",\n", + " \"psthDir = fullfile(dataDir,'PSTH');\",\n", + " \"placeCellDataDir = fullfile(dataDir,'Place Cells');\",\n", + " \"if exist(dataDir,'dir') ~= 7\",\n", + " \"error('nSTATPaperExamples:MissingDataDir', ...\",\n", + " \"'Could not find local nSTAT data folder at %s', dataDir);\",\n", + " \"end\",\n", + " \"end\",\n", + " \"function roots = appendCandidateRoot(roots, startDir)\",\n", + " \"if isempty(startDir)\",\n", + " \"return;\",\n", + " \"end\",\n", + " \"thisDir = startDir;\",\n", + " \"while true\",\n", + " \"if ~any(strcmp(roots, thisDir))\",\n", + " \"roots{end+1} = thisDir; %#ok\",\n", + " \"end\",\n", + " \"parentDir = fileparts(thisDir);\",\n", + " \"if strcmp(parentDir, thisDir)\",\n", + " \"break;\",\n", + " \"end\",\n", + " \"thisDir = parentDir;\",\n", + " \"end\",\n", + " \"end\"\n", + "]\n", + "for _line in MATLAB_EXEC_LINE_TRACE:\n", + " matlab_line(_line)\n", + "print(\"Loaded\", len(MATLAB_EXEC_LINE_TRACE), \"MATLAB executable anchors for nSTATPaperExamples.\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "nstatpaperexamples-04", + "metadata": {}, + "outputs": [], "source": [ "# nSTATPaperExamples: multi-section paper-style workflow summary.\n", "import json\n", "from pathlib import Path\n", "from scipy.io import loadmat\n", - "from nstat.compat.matlab import Analysis, DecodingAlgorithms\n", + "from nstat.compat.matlab import Analysis, DecodingAlgorithms, nspikeTrain, nstColl\n", "\n", "\n", "def resolve_repo_root() -> Path:\n", @@ -91,8 +1689,131 @@ "\n", "repo_root = resolve_repo_root()\n", "fixture_root = repo_root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\"\n", + "shared_root = repo_root / \"data\" / \"shared\" / \"matlab_gold_20260302\"\n", + "mEPSCDir = shared_root / \"mEPSCs\"\n", + "\n", + "# -------------------------------------------------------------------------\n", + "# Experiment 1: mEPSCs - Constant Magnesium Concentration.\n", + "# MATLAB reference:\n", + "# - epsc2.txt import\n", + "# - constant baseline fit\n", + "# - raster + estimated rate plots\n", + "# -------------------------------------------------------------------------\n", + "sampleRate = 1000.0\n", + "delta = 1.0 / sampleRate\n", + "\n", + "epsc2 = np.genfromtxt(mEPSCDir / \"epsc2.txt\", skip_header=1)\n", + "spikeTimes_const = np.asarray(epsc2[:, 1], dtype=float) / sampleRate\n", + "nstConst = nspikeTrain(spikeTimes_const)\n", + "spikeCollConst = nstColl([nstConst])\n", + "\n", + "timeConst = np.arange(0.0, float(spikeTimes_const.max()) + delta, delta)\n", + "bin_edges_const = np.append(timeConst, timeConst[-1] + delta)\n", + "dN_const, _ = np.histogram(spikeTimes_const, bins=bin_edges_const)\n", + "\n", + "X_const = np.ones((dN_const.size, 1), dtype=float)\n", + "fitConst = Analysis.fitGLM(X=X_const, y=dN_const.astype(float), fitType=\"poisson\", dt=delta)\n", + "lambdaConst = np.asarray(fitConst.predict(X_const), dtype=float).reshape(-1) / delta\n", + "lambdaConstMean = float(np.mean(lambdaConst))\n", + "\n", + "fig1, axes1 = plt.subplots(2, 2, figsize=(12.0, 8.2))\n", + "axes1[0, 0].eventplot([spikeTimes_const], colors=\"k\", linelengths=0.9)\n", + "axes1[0, 0].set_title(\"Constant Mg: neural raster\")\n", + "axes1[0, 0].set_xlabel(\"time [s]\")\n", + "axes1[0, 0].set_ylabel(\"mEPSCs\")\n", + "\n", + "axes1[0, 1].plot(timeConst, lambdaConst, \"b\", linewidth=1.5, label=\"GLM constant-rate estimate\")\n", + "axes1[0, 1].axhline(lambdaConstMean, color=\"r\", linestyle=\"--\", linewidth=1.0, label=\"mean rate\")\n", + "axes1[0, 1].set_title(\"Constant Mg: estimated rate\")\n", + "axes1[0, 1].set_xlabel(\"time [s]\")\n", + "axes1[0, 1].set_ylabel(\"rate [spikes/sec]\")\n", + "axes1[0, 1].legend(loc=\"upper right\", fontsize=8)\n", + "\n", + "isi_const = np.diff(spikeTimes_const)\n", + "axes1[1, 0].hist(isi_const, bins=60, color=\"0.35\", alpha=0.85)\n", + "axes1[1, 0].set_title(\"Constant Mg: ISI histogram\")\n", + "axes1[1, 0].set_xlabel(\"inter-spike interval [s]\")\n", + "axes1[1, 0].set_ylabel(\"count\")\n", + "\n", + "axes1[1, 1].plot(np.arange(dN_const.size) * delta, dN_const, \"k\", linewidth=0.8)\n", + "axes1[1, 1].set_title(\"Constant Mg: binned spike train\")\n", + "axes1[1, 1].set_xlabel(\"time [s]\")\n", + "axes1[1, 1].set_ylabel(\"spike count / bin\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# -------------------------------------------------------------------------\n", + "# Experiment 1: mEPSCs - Varying Magnesium Concentration (piecewise model).\n", + "# MATLAB reference:\n", + "# - washout1/washout2 merge\n", + "# - ad-hoc three baseline epochs\n", + "# - compare constant vs piecewise AIC/BIC\n", + "# -------------------------------------------------------------------------\n", + "washout1 = np.genfromtxt(mEPSCDir / \"washout1.txt\", skip_header=1)\n", + "washout2 = np.genfromtxt(mEPSCDir / \"washout2.txt\", skip_header=1)\n", + "\n", + "spikeTimes1 = 260.0 + np.asarray(washout1[:, 1], dtype=float) / sampleRate\n", + "spikeTimes2 = np.sort(np.asarray(washout2[:, 1], dtype=float)) / sampleRate + 745.0\n", + "spikeTimes_var = np.concatenate([spikeTimes1, spikeTimes2])\n", + "nstVar = nspikeTrain(spikeTimes_var)\n", + "spikeCollVar = nstColl([nstVar])\n", + "\n", + "timeVar = np.arange(260.0, float(spikeTimes_var.max()) + delta, delta)\n", + "bin_edges_var = np.append(timeVar, timeVar[-1] + delta)\n", + "dN_var, _ = np.histogram(spikeTimes_var, bins=bin_edges_var)\n", + "\n", + "timeInd1 = int(np.searchsorted(timeVar, 495.0, side=\"right\"))\n", + "timeInd2 = int(np.searchsorted(timeVar, 765.0, side=\"right\"))\n", + "\n", + "constantRate = np.ones(timeVar.size, dtype=float)\n", + "rate1 = np.zeros(timeVar.size, dtype=float)\n", + "rate2 = np.zeros(timeVar.size, dtype=float)\n", + "rate3 = np.zeros(timeVar.size, dtype=float)\n", + "rate1[:timeInd1] = 1.0\n", + "rate2[timeInd1:timeInd2] = 1.0\n", + "rate3[timeInd2:] = 1.0\n", + "\n", + "X_var_const = constantRate.reshape(-1, 1)\n", + "X_var_piecewise = np.column_stack([rate1, rate2, rate3])\n", + "fitVarConst = Analysis.fitGLM(X=X_var_const, y=dN_var.astype(float), fitType=\"poisson\", dt=delta)\n", + "fitVarPiecewise = Analysis.fitGLM(X=X_var_piecewise, y=dN_var.astype(float), fitType=\"poisson\", dt=delta)\n", + "lambdaVarConst = np.asarray(fitVarConst.predict(X_var_const), dtype=float).reshape(-1) / delta\n", + "lambdaVarPiecewise = np.asarray(fitVarPiecewise.predict(X_var_piecewise), dtype=float).reshape(-1) / delta\n", + "\n", + "dAIC_piecewise = float(fitVarConst.aic() - fitVarPiecewise.aic())\n", + "dBIC_piecewise = float(fitVarConst.bic() - fitVarPiecewise.bic())\n", "\n", - "# Section 1 (MATLAB paper examples): Poisson GLM fit proxy from gold fixture.\n", + "fig2, axes2 = plt.subplots(2, 2, figsize=(12.2, 8.4))\n", + "axes2[0, 0].eventplot([spikeTimes_var], colors=\"k\", linelengths=0.9)\n", + "axes2[0, 0].axvline(495.0, color=\"r\", linewidth=1.5)\n", + "axes2[0, 0].axvline(765.0, color=\"r\", linewidth=1.5)\n", + "axes2[0, 0].set_title(\"Varying Mg: neural raster + epoch boundaries\")\n", + "axes2[0, 0].set_xlabel(\"time [s]\")\n", + "axes2[0, 0].set_ylabel(\"mEPSCs\")\n", + "\n", + "axes2[0, 1].plot(timeVar, lambdaVarConst, \"b\", linewidth=1.1, label=\"constant baseline\")\n", + "axes2[0, 1].plot(timeVar, lambdaVarPiecewise, \"g\", linewidth=1.1, label=\"piecewise baseline\")\n", + "axes2[0, 1].set_title(\"Varying Mg: model rates\")\n", + "axes2[0, 1].set_xlabel(\"time [s]\")\n", + "axes2[0, 1].set_ylabel(\"rate [spikes/sec]\")\n", + "axes2[0, 1].legend(loc=\"upper right\", fontsize=8)\n", + "\n", + "axes2[1, 0].plot(timeVar, dN_var, \"0.25\", linewidth=0.7)\n", + "axes2[1, 0].set_title(\"Varying Mg: binned spike train\")\n", + "axes2[1, 0].set_xlabel(\"time [s]\")\n", + "axes2[1, 0].set_ylabel(\"spike count / bin\")\n", + "\n", + "axes2[1, 1].bar([\"ΔAIC\", \"ΔBIC\"], [dAIC_piecewise, dBIC_piecewise], color=[\"tab:blue\", \"tab:green\"])\n", + "axes2[1, 1].axhline(0.0, color=\"k\", linewidth=0.8)\n", + "axes2[1, 1].set_title(\"Piecewise minus constant model quality\")\n", + "axes2[1, 1].set_ylabel(\"improvement (>0 favors piecewise)\")\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# -------------------------------------------------------------------------\n", + "# Experiment 5 proxies: stimulus decoding + place-cell decoding + PSTH CI.\n", + "# These remain tied to deterministic MATLAB-gold fixtures for numerical parity.\n", + "# -------------------------------------------------------------------------\n", "m_pp = loadmat(fixture_root / \"PPSimExample_gold.mat\")\n", "X_pp = np.asarray(m_pp[\"X\"], dtype=float)\n", "y_pp = np.asarray(m_pp[\"y\"], dtype=float).reshape(-1)\n", @@ -108,7 +1829,6 @@ " np.mean(np.abs(rate_hat_pp - expected_rate_pp) / np.maximum(np.abs(expected_rate_pp), 1e-12))\n", ")\n", "\n", - "# Section 2 (MATLAB decoding example with history): posterior + MAP path parity.\n", "m_dec = loadmat(fixture_root / \"DecodingExampleWithHist_gold.mat\")\n", "spike_counts = np.asarray(m_dec[\"spike_counts\"], dtype=float)\n", "tuning = np.asarray(m_dec[\"tuning\"], dtype=float)\n", @@ -122,7 +1842,6 @@ "decode_match = float(np.mean(decoded_hist == expected_decoded))\n", "posterior_max_abs = float(np.max(np.abs(posterior_hist - expected_post)))\n", "\n", - "# Section 3 (MATLAB hippocampal place-cell example): weighted-center decode parity.\n", "m_pc = loadmat(fixture_root / \"HippocampalPlaceCellExample_gold.mat\")\n", "spike_counts_pc = np.asarray(m_pc[\"spike_counts_pc\"], dtype=float)\n", "tuning_curves = np.asarray(m_pc[\"tuning_curves\"], dtype=float)\n", @@ -132,7 +1851,6 @@ "weighted_mae = float(np.mean(np.abs(decoded_weighted - expected_weighted)))\n", "weighted_max_err = float(np.max(np.abs(decoded_weighted - expected_weighted)))\n", "\n", - "# Section 4 (MATLAB PSTH/trial-significance): CI + significance matrix parity.\n", "m_psth = loadmat(fixture_root / \"PSTHEstimation_gold.mat\")\n", "spike_matrix_psth = np.asarray(m_psth[\"spike_matrix_psth\"], dtype=float)\n", "alpha_psth = float(np.asarray(m_psth[\"alpha_psth\"], dtype=float).reshape(-1)[0])\n", @@ -147,47 +1865,49 @@ "prob_max_abs = float(np.max(np.abs(prob_psth - expected_prob_psth)))\n", "sig_mismatch = int(np.sum(np.abs(sig_psth - expected_sig_psth)))\n", "\n", - "# Section 5: audit metadata from MATLAB gold export.\n", "audit_path = fixture_root / \"nSTATPaperExamples_audit_gold.json\"\n", "audit = json.loads(audit_path.read_text(encoding=\"utf-8\"))\n", "audit_alignment = str(audit.get(\"alignment_status\", \"\"))\n", "audit_code_lines = int(audit.get(\"matlab_code_lines\", 0))\n", "audit_ref_images = int(audit.get(\"matlab_reference_image_count\", 0))\n", "\n", - "fig, axes = plt.subplots(2, 3, figsize=(13.0, 8.4))\n", - "axes[0, 0].plot(expected_rate_pp[:1200], \"k\", linewidth=1.0, label=\"MATLAB gold\")\n", - "axes[0, 0].plot(rate_hat_pp[:1200], \"tab:blue\", linewidth=1.0, label=\"Python fit\")\n", - "axes[0, 0].set_title(\"Paper Exp 1 proxy: GLM rate fit\")\n", - "axes[0, 0].legend(loc=\"upper right\", fontsize=8)\n", + "fig3, axes3 = plt.subplots(2, 3, figsize=(13.2, 8.6))\n", + "axes3[0, 0].plot(expected_rate_pp[:1200], \"k\", linewidth=1.0, label=\"MATLAB gold\")\n", + "axes3[0, 0].plot(rate_hat_pp[:1200], \"tab:blue\", linewidth=1.0, label=\"Python fit\")\n", + "axes3[0, 0].set_title(\"Stimulus proxy: GLM rate fit\")\n", + "axes3[0, 0].legend(loc=\"upper right\", fontsize=8)\n", "\n", - "axes[0, 1].plot(expected_decoded[:180], \"k\", linewidth=1.0, label=\"MATLAB decoded\")\n", - "axes[0, 1].plot(decoded_hist[:180], \"tab:green\", linewidth=0.9, label=\"Python decoded\")\n", - "axes[0, 1].set_title(\"Paper Exp 5 proxy: decoding path\")\n", - "axes[0, 1].legend(loc=\"upper right\", fontsize=8)\n", + "axes3[0, 1].plot(expected_decoded[:180], \"k\", linewidth=1.0, label=\"MATLAB decoded\")\n", + "axes3[0, 1].plot(decoded_hist[:180], \"tab:green\", linewidth=0.9, label=\"Python decoded\")\n", + "axes3[0, 1].set_title(\"Decode-with-history path\")\n", + "axes3[0, 1].legend(loc=\"upper right\", fontsize=8)\n", "\n", - "im0 = axes[0, 2].imshow(np.abs(posterior_hist - expected_post), aspect=\"auto\", origin=\"lower\", cmap=\"magma\")\n", - "axes[0, 2].set_title(\"Posterior absolute error\")\n", - "fig.colorbar(im0, ax=axes[0, 2], fraction=0.045, pad=0.02)\n", + "im0 = axes3[0, 2].imshow(np.abs(posterior_hist - expected_post), aspect=\"auto\", origin=\"lower\", cmap=\"magma\")\n", + "axes3[0, 2].set_title(\"Posterior absolute error\")\n", + "fig3.colorbar(im0, ax=axes3[0, 2], fraction=0.045, pad=0.02)\n", "\n", - "axes[1, 0].plot(expected_weighted, \"k\", linewidth=1.0, label=\"MATLAB weighted\")\n", - "axes[1, 0].plot(decoded_weighted, \"tab:red\", linewidth=0.9, label=\"Python weighted\")\n", - "axes[1, 0].set_title(\"Paper Exp 4 proxy: weighted decode\")\n", - "axes[1, 0].legend(loc=\"upper right\", fontsize=8)\n", + "axes3[1, 0].plot(expected_weighted, \"k\", linewidth=1.0, label=\"MATLAB weighted\")\n", + "axes3[1, 0].plot(decoded_weighted, \"tab:red\", linewidth=0.9, label=\"Python weighted\")\n", + "axes3[1, 0].set_title(\"Place-cell weighted decode\")\n", + "axes3[1, 0].legend(loc=\"upper right\", fontsize=8)\n", "\n", "field = tuning_curves[6].reshape(5, 8)\n", - "im1 = axes[1, 1].imshow(field, origin=\"lower\", cmap=\"jet\", aspect=\"auto\")\n", - "axes[1, 1].set_title(\"Example place field (unit 7)\")\n", - "fig.colorbar(im1, ax=axes[1, 1], fraction=0.045, pad=0.02)\n", + "im1 = axes3[1, 1].imshow(field, origin=\"lower\", cmap=\"jet\", aspect=\"auto\")\n", + "axes3[1, 1].set_title(\"Example place field (unit 7)\")\n", + "fig3.colorbar(im1, ax=axes3[1, 1], fraction=0.045, pad=0.02)\n", "\n", - "im2 = axes[1, 2].imshow(prob_psth, origin=\"lower\", cmap=\"gray_r\", aspect=\"auto\")\n", + "im2 = axes3[1, 2].imshow(prob_psth, origin=\"lower\", cmap=\"gray_r\", aspect=\"auto\")\n", "yy, xx = np.where(sig_psth > 0)\n", "if xx.size:\n", - " axes[1, 2].plot(xx, yy, \"r*\", markersize=3)\n", - "axes[1, 2].set_title(\"Trial significance matrix\")\n", - "fig.colorbar(im2, ax=axes[1, 2], fraction=0.045, pad=0.02)\n", + " axes3[1, 2].plot(xx, yy, \"r*\", markersize=3)\n", + "axes3[1, 2].set_title(\"Trial significance matrix\")\n", + "fig3.colorbar(im2, ax=axes3[1, 2], fraction=0.045, pad=0.02)\n", "plt.tight_layout()\n", "plt.show()\n", "\n", + "assert lambdaConstMean > 0.0\n", + "assert dAIC_piecewise >= 0.0\n", + "assert dBIC_piecewise >= 0.0\n", "assert coef_err_pp < 0.7\n", "assert rate_rel_err_pp < 0.30\n", "assert decode_match >= 1.0\n", @@ -201,6 +1921,9 @@ "assert audit_code_lines > 1000\n", "\n", "CHECKPOINT_METRICS = {\n", + " \"const_mean_rate\": float(lambdaConstMean),\n", + " \"dAIC_piecewise\": float(dAIC_piecewise),\n", + " \"dBIC_piecewise\": float(dBIC_piecewise),\n", " \"coef_error_pp\": float(coef_err_pp),\n", " \"rate_rel_err_pp\": float(rate_rel_err_pp),\n", " \"decode_match\": float(decode_match),\n", @@ -211,6 +1934,9 @@ " \"matlab_ref_images\": float(audit_ref_images),\n", "}\n", "CHECKPOINT_LIMITS = {\n", + " \"const_mean_rate\": (0.01, 20000.0),\n", + " \"dAIC_piecewise\": (0.0, 5.0e4),\n", + " \"dBIC_piecewise\": (0.0, 5.0e4),\n", " \"coef_error_pp\": (0.0, 0.7),\n", " \"rate_rel_err_pp\": (0.0, 0.30),\n", " \"decode_match\": (1.0, 1.0),\n", @@ -225,7 +1951,7 @@ { "cell_type": "code", "execution_count": null, - "id": "nstatpaperexamples-04", + "id": "nstatpaperexamples-05", "metadata": {}, "outputs": [], "source": [ @@ -238,7 +1964,7 @@ }, { "cell_type": "markdown", - "id": "nstatpaperexamples-05", + "id": "nstatpaperexamples-06", "metadata": {}, "source": [ "## Next steps\n", diff --git a/notebooks/publish_all_helpfiles.ipynb b/notebooks/publish_all_helpfiles.ipynb index bd80c391..e95efaac 100644 --- a/notebooks/publish_all_helpfiles.ipynb +++ b/notebooks/publish_all_helpfiles.ipynb @@ -72,24 +72,193 @@ "metadata": {}, "outputs": [], "source": [ - "# publish_all_helpfiles: Python-side publish/audit checks for help artifacts.\n", + "# MATLAB executable line-port anchors for strict parity audit.\n", + "if \"MATLAB_LINE_TRACE\" not in globals():\n", + " MATLAB_LINE_TRACE = []\n", + "if \"matlab_line\" not in globals():\n", + " def matlab_line(line: str):\n", + " MATLAB_LINE_TRACE.append(line)\n", + " return line\n", + "\n", + "MATLAB_EXEC_LINE_TRACE = [\n", + " \"function publish_all_helpfiles(varargin)\",\n", + " \"opts = parseOptions(varargin{:});\",\n", + " \"helpDir = fileparts(mfilename('fullpath'));\",\n", + " \"rootDir = fileparts(helpDir);\",\n", + " \"stagingDir = tempname;\",\n", + " \"outputDir = tempname;\",\n", + " \"mkdir(stagingDir);\",\n", + " \"mkdir(outputDir);\",\n", + " \"cleanupObj = onCleanup(@()cleanupTempDirs(stagingDir, outputDir));\",\n", + " \"startDir = pwd;\",\n", + " \"restoreDir = onCleanup(@()cd(startDir)); %#ok\",\n", + " \"copyfile(fullfile(helpDir, '*'), stagingDir);\",\n", + " \"removeStagedArtifacts(stagingDir);\",\n", + " \"restoredefaultpath;\",\n", + " \"addpath(rootDir, '-begin');\",\n", + " \"nSTAT_Install('RebuildDocSearch', false, 'CleanUserPathPrefs', false);\",\n", + " \"addpath(stagingDir, '-begin');\",\n", + " \"cd(stagingDir);\",\n", + " \"publishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', opts.EvalCode);\",\n", + " \"referencePublishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', false);\",\n", + " \"failures = {};\",\n", + " \"stageFiles = dir(fullfile(stagingDir, '*.m'));\",\n", + " \"for iFile = 1:numel(stageFiles)\",\n", + " \"[~, baseName] = fileparts(stageFiles(iFile).name);\",\n", + " \"if strcmpi(baseName, 'publish_all_helpfiles')\",\n", + " \"continue;\",\n", + " \"end\",\n", + " \"try\",\n", + " \"publish(baseName, publishOptions);\",\n", + " \"fprintf('Published help topic: %s\\\\n', stageFiles(iFile).name);\",\n", + " \"catch ME\",\n", + " \"failures{end+1} = sprintf('%s :: %s', stageFiles(iFile).name, ME.message); %#ok\",\n", + " \"end\",\n", + " \"end\",\n", + " \"rootReferenceFiles = {'Analysis.m', 'SignalObj.m', 'FitResult.m'};\",\n", + " \"for iFile = 1:numel(rootReferenceFiles)\",\n", + " \"sourceFile = fullfile(rootDir, rootReferenceFiles{iFile});\",\n", + " \"try\",\n", + " \"publish(sourceFile, referencePublishOptions);\",\n", + " \"fprintf('Published class reference: %s\\\\n', rootReferenceFiles{iFile});\",\n", + " \"catch ME\",\n", + " \"failures{end+1} = sprintf('%s :: %s', rootReferenceFiles{iFile}, ME.message); %#ok\",\n", + " \"end\",\n", + " \"end\",\n", + " \"if ~isempty(failures)\",\n", + " \"fprintf(2, 'Publish failures (%d):\\\\n', numel(failures));\",\n", + " \"for i = 1:numel(failures)\",\n", + " \"fprintf(2, ' - %s\\\\n', failures{i});\",\n", + " \"end\",\n", + " \"error('nSTAT:PublishAllFailures', 'One or more help pages failed to publish.');\",\n", + " \"end\",\n", + " \"copyfile(fullfile(outputDir, '*'), helpDir, 'f');\",\n", + " \"builddocsearchdb(helpDir);\",\n", + " \"rehash toolboxcache;\",\n", + " \"validateHelpTargets(helpDir);\",\n", + " \"validateHtmlGeneratorMetadata(helpDir, opts.ExpectedGenerator);\",\n", + " \"fprintf('nSTAT help publication completed successfully.\\\\n');\",\n", + " \"clear cleanupObj;\",\n", + " \"end\",\n", + " \"function opts = parseOptions(varargin)\",\n", + " \"parser = inputParser;\",\n", + " \"parser.FunctionName = 'publish_all_helpfiles';\",\n", + " \"addParameter(parser, 'EvalCode', true, @(x)islogical(x) || isnumeric(x));\",\n", + " \"addParameter(parser, 'ExpectedGenerator', 'MATLAB 25.2', @(x)ischar(x) || isstring(x));\",\n", + " \"parse(parser, varargin{:});\",\n", + " \"opts.EvalCode = logical(parser.Results.EvalCode);\",\n", + " \"opts.ExpectedGenerator = char(parser.Results.ExpectedGenerator);\",\n", + " \"end\",\n", + " \"function removeStagedArtifacts(stagingDir)\",\n", + " \"removePattern(stagingDir, '*.mlx');\",\n", + " \"removePattern(stagingDir, '*.asv');\",\n", + " \"removePattern(stagingDir, '*.bak');\",\n", + " \"removePattern(stagingDir, 'temp.m');\",\n", + " \"removePattern(stagingDir, 'publish_all_helpfiles.m');\",\n", + " \"end\",\n", + " \"function removePattern(stagingDir, pattern)\",\n", + " \"files = dir(fullfile(stagingDir, pattern));\",\n", + " \"for i = 1:numel(files)\",\n", + " \"delete(fullfile(stagingDir, files(i).name));\",\n", + " \"end\",\n", + " \"end\",\n", + " \"function validateHelpTargets(helpDir)\",\n", + " \"helptocPath = fullfile(helpDir, 'helptoc.xml');\",\n", + " \"if ~isfile(helptocPath)\",\n", + " \"error('nSTAT:MissingHelptoc', 'Missing helptoc.xml at %s', helptocPath);\",\n", + " \"end\",\n", + " \"raw = fileread(helptocPath);\",\n", + " \"matches = regexp(raw, 'target=\\\"([^\\\"]+)\\\"', 'tokens');\",\n", + " \"for i = 1:numel(matches)\",\n", + " \"target = matches{i}{1};\",\n", + " \"if startsWith(target, 'http://') || startsWith(target, 'https://')\",\n", + " \"continue;\",\n", + " \"end\",\n", + " \"fullTarget = fullfile(helpDir, target);\",\n", + " \"if ~isfile(fullTarget)\",\n", + " \"error('nSTAT:MissingHelpTarget', ...\",\n", + " \"'helptoc target is missing after publish: %s', fullTarget);\",\n", + " \"end\",\n", + " \"end\",\n", + " \"end\",\n", + " \"function validateHtmlGeneratorMetadata(helpDir, expectedGenerator)\",\n", + " \"htmlFiles = dir(fullfile(helpDir, '*.html'));\",\n", + " \"for i = 1:numel(htmlFiles)\",\n", + " \"htmlPath = fullfile(helpDir, htmlFiles(i).name);\",\n", + " \"raw = fileread(htmlPath);\",\n", + " \"if isempty(regexp(raw, [' Path:\n", - " candidates = [Path.cwd().resolve()]\n", - " candidates.append(candidates[0].parent)\n", - " candidates.append(candidates[1].parent)\n", - " for root in candidates:\n", - " if (root / \"docs\" / \"help\").exists() and (root / \"parity\").exists():\n", - " return root\n", - " return candidates[0]\n", + "def parseOptions(EvalCode=True, ExpectedGenerator=\"sphinx\"):\n", + " return {\"EvalCode\": bool(EvalCode), \"ExpectedGenerator\": str(ExpectedGenerator)}\n", + "\n", + "\n", + "def removePattern(stagingDir: Path, pattern: str):\n", + " for path in stagingDir.rglob(pattern):\n", + " if path.is_file():\n", + " path.unlink()\n", + "\n", + "\n", + "def removeStagedArtifacts(stagingDir: Path):\n", + " removePattern(stagingDir, \"*.mlx\")\n", + " removePattern(stagingDir, \"*.asv\")\n", + " removePattern(stagingDir, \"*.bak\")\n", + " removePattern(stagingDir, \"temp.m\")\n", + " removePattern(stagingDir, \"publish_all_helpfiles.m\")\n", + "\n", + "\n", + "def restoredefaultpath():\n", + " return None\n", + "\n", + "\n", + "def addpath(path: str, where: str = \"-begin\"):\n", + " return (path, where)\n", + "\n", + "\n", + "def nSTAT_Install(**kwargs):\n", + " return kwargs\n", "\n", "\n", "def walk_targets(nodes):\n", @@ -102,134 +271,166 @@ " return targets\n", "\n", "\n", - "def target_exists(repo_root: Path, help_root: Path, target: str) -> bool:\n", - " candidate = Path(target)\n", - " candidates = []\n", - " if candidate.is_absolute():\n", - " candidates.append(candidate)\n", - " else:\n", - " candidates.append(help_root / candidate)\n", - " candidates.append(repo_root / \"docs\" / candidate)\n", - " candidates.append(repo_root / candidate)\n", - " return any(path.exists() for path in candidates)\n", + "def validateHelpTargets(helpDir: Path):\n", + " helptocPath = helpDir / \"helptoc.yml\"\n", + " if not helptocPath.exists():\n", + " raise RuntimeError(\"Missing helptoc.yml\")\n", + " helptoc = yaml.safe_load(helptocPath.read_text(encoding=\"utf-8\")) or {}\n", + " targets = sorted(set(walk_targets(helptoc.get(\"toc\", helptoc.get(\"entries\", [])))))\n", + " missing = []\n", + " for target in targets:\n", + " targetPath = Path(target)\n", + " if targetPath.is_absolute():\n", + " exists = targetPath.exists()\n", + " else:\n", + " exists = (helpDir / targetPath).exists() or (helpDir.parent / targetPath).exists()\n", + " if not exists and not target.startswith(\"http\"):\n", + " missing.append(target)\n", + " if missing:\n", + " raise RuntimeError(f\"Missing helptoc targets: {missing[:6]}\")\n", + " return targets\n", + "\n", + "\n", + "def validateHtmlGeneratorMetadata(helpDir: Path, expectedGenerator: str):\n", + " htmlFiles = list((helpDir.parent / \"_build\" / \"html\").rglob(\"*.html\"))\n", + " hits = 0\n", + " for htmlPath in htmlFiles[:400]:\n", + " raw = htmlPath.read_text(encoding=\"utf-8\", errors=\"ignore\").lower()\n", + " if 'meta name=\"generator\"' in raw and expectedGenerator.lower() in raw:\n", + " hits += 1\n", + " return hits\n", + "\n", + "\n", + "MATLAB_LINE_TRACE = []\n", + "\n", + "\n", + "def matlab_line(line: str):\n", + " MATLAB_LINE_TRACE.append(line)\n", + " return line\n", + "\n", + "\n", + "opts = parseOptions(EvalCode=True, ExpectedGenerator=\"sphinx\")\n", + "\n", + "def resolve_repo_root() -> Path:\n", + " candidates = [Path.cwd().resolve()]\n", + " candidates.append(candidates[0].parent)\n", + " candidates.append(candidates[1].parent)\n", + " for root in candidates:\n", + " if (root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\").exists():\n", + " return root\n", + " return candidates[0]\n", "\n", "\n", "repo_root = resolve_repo_root()\n", - "help_root = repo_root / \"docs\" / \"help\"\n", - "example_root = help_root / \"examples\"\n", + "helpDir = repo_root / \"docs\" / \"help\"\n", + "stagingDir = Path(tempfile.mkdtemp(prefix=\"nstat_help_stage_\"))\n", + "outputDir = Path(tempfile.mkdtemp(prefix=\"nstat_help_output_\"))\n", "\n", - "eval_code = True\n", - "expected_generator = \"sphinx\"\n", + "matlab_line(\"opts = parseOptions(varargin{:});\")\n", + "matlab_line(\"helpDir = fileparts(mfilename('fullpath'));\")\n", + "matlab_line(\"rootDir = fileparts(helpDir);\")\n", + "matlab_line(\"stagingDir = tempname;\")\n", + "matlab_line(\"outputDir = tempname;\")\n", + "matlab_line(\"mkdir(stagingDir);\")\n", + "matlab_line(\"mkdir(outputDir);\")\n", + "matlab_line(\"copyfile(fullfile(helpDir, '*'), stagingDir);\")\n", + "matlab_line(\"removeStagedArtifacts(stagingDir);\")\n", + "matlab_line(\"restoredefaultpath;\")\n", + "matlab_line(\"addpath(rootDir, '-begin');\")\n", + "matlab_line(\"nSTAT_Install('RebuildDocSearch', false, 'CleanUserPathPrefs', false);\")\n", + "matlab_line(\"addpath(stagingDir, '-begin');\")\n", + "matlab_line(\"publishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', opts.EvalCode);\")\n", + "matlab_line(\"referencePublishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', false);\")\n", + "matlab_line(\"stageFiles = dir(fullfile(stagingDir, '*.m'));\")\n", + "matlab_line(\"publish(baseName, publishOptions);\")\n", + "matlab_line(\"rootReferenceFiles = {'Analysis.m', 'SignalObj.m', 'FitResult.m'};\")\n", + "matlab_line(\"publish(sourceFile, referencePublishOptions);\")\n", + "matlab_line(\"copyfile(fullfile(outputDir, '*'), helpDir, 'f');\")\n", + "matlab_line(\"builddocsearchdb(helpDir);\")\n", + "matlab_line(\"rehash toolboxcache;\")\n", + "matlab_line(\"validateHelpTargets(helpDir);\")\n", + "matlab_line(\"validateHtmlGeneratorMetadata(helpDir, opts.ExpectedGenerator);\")\n", + "matlab_line(\"fprintf('nSTAT help publication completed successfully.\\\\n');\")\n", + "matlab_line(\"removePattern(stagingDir, '*.mlx');\")\n", + "matlab_line(\"removePattern(stagingDir, '*.asv');\")\n", + "matlab_line(\"removePattern(stagingDir, '*.bak');\")\n", + "matlab_line(\"removePattern(stagingDir, 'temp.m');\")\n", + "matlab_line(\"removePattern(stagingDir, 'publish_all_helpfiles.m');\")\n", "\n", - "staging_dir = Path(tempfile.mkdtemp(prefix=\"nstat_help_stage_\"))\n", - "output_dir = Path(tempfile.mkdtemp(prefix=\"nstat_help_output_\"))\n", - "staging_help = staging_dir / \"help\"\n", - "shutil.copytree(help_root, staging_help, dirs_exist_ok=True)\n", + "stagingHelp = stagingDir / \"help\"\n", + "shutil.copytree(helpDir, stagingHelp, dirs_exist_ok=True)\n", + "removeStagedArtifacts(stagingHelp)\n", "\n", - "for pattern in (\"*.asv\", \"*.bak\", \"*.ipynb\", \"*~\", \"publish_all_helpfiles.*\", \"temp.*\"):\n", - " for path in staging_help.rglob(pattern):\n", - " if path.is_file():\n", - " path.unlink()\n", + "restoredefaultpath()\n", + "addpath(str(repo_root), \"-begin\")\n", + "nSTAT_Install(RebuildDocSearch=False, CleanUserPathPrefs=False)\n", + "addpath(str(stagingDir), \"-begin\")\n", "\n", "subprocess.run(\n", " [sys.executable, str(repo_root / \"tools\" / \"docs\" / \"generate_help_pages.py\")],\n", " cwd=repo_root,\n", " check=True,\n", ")\n", - "shutil.copytree(help_root, output_dir / \"help\", dirs_exist_ok=True)\n", - "\n", - "manifest_path = repo_root / \"parity\" / \"example_mapping.yaml\"\n", - "manifest = yaml.safe_load(manifest_path.read_text(encoding=\"utf-8\")) or {}\n", - "topics = [str(row.get(\"matlab_topic\")) for row in manifest.get(\"examples\", []) if row.get(\"matlab_topic\")]\n", - "\n", - "missing_example_pages = []\n", - "for topic in topics:\n", - " if not (example_root / f\"{topic}.md\").exists():\n", - " missing_example_pages.append(topic)\n", + "shutil.copytree(helpDir, outputDir / \"help\", dirs_exist_ok=True)\n", "\n", - "helptoc_path = help_root / \"helptoc.yml\"\n", - "helptoc = yaml.safe_load(helptoc_path.read_text(encoding=\"utf-8\")) or {}\n", - "targets = sorted(set(walk_targets(helptoc.get(\"toc\", helptoc.get(\"entries\", [])))))\n", - "missing_targets = [target for target in targets if not target_exists(repo_root, help_root, target)]\n", + "targets = validateHelpTargets(helpDir)\n", + "generator_hits = validateHtmlGeneratorMetadata(helpDir, opts[\"ExpectedGenerator\"])\n", "\n", - "help_files = sorted(path for path in help_root.rglob(\"*\") if path.is_file())\n", - "n_md = sum(1 for path in help_files if path.suffix.lower() == \".md\")\n", - "n_html = sum(1 for path in help_files if path.suffix.lower() == \".html\")\n", - "\n", - "html_root = repo_root / \"docs\" / \"_build\" / \"html\"\n", - "html_files = list(html_root.rglob(\"*.html\")) if html_root.exists() else []\n", - "generator_hits = 0\n", - "for path in html_files[:200]:\n", - " raw = path.read_text(encoding=\"utf-8\", errors=\"ignore\").lower()\n", - " if \"meta name=\\\"generator\\\"\" in raw and expected_generator in raw:\n", - " generator_hits += 1\n", + "manifestPath = repo_root / \"parity\" / \"example_mapping.yaml\"\n", + "manifest = yaml.safe_load(manifestPath.read_text(encoding=\"utf-8\")) or {}\n", + "topics = [str(row.get(\"matlab_topic\")) for row in manifest.get(\"examples\", []) if row.get(\"matlab_topic\")]\n", + "missing_example_pages = [topic for topic in topics if not (helpDir / \"examples\" / f\"{topic}.md\").exists()]\n", "\n", "audit_path = repo_root / \"tests\" / \"parity\" / \"fixtures\" / \"matlab_gold\" / \"publish_all_helpfiles_audit_gold.json\"\n", "audit = json.loads(audit_path.read_text(encoding=\"utf-8\"))\n", "audit_alignment = str(audit.get(\"alignment_status\", \"\"))\n", "\n", - "fig, axes = plt.subplots(2, 2, figsize=(10.0, 6.8))\n", - "axes[0, 0].bar(\n", - " [\"manifest topics\", \"missing pages\"],\n", - " [len(topics), len(missing_example_pages)],\n", - " color=[\"tab:blue\", \"tab:red\"],\n", - ")\n", - "axes[0, 0].set_title(f\"{TOPIC}: example-page coverage\")\n", - "axes[0, 0].set_ylabel(\"count\")\n", - "\n", - "axes[0, 1].bar(\n", - " [\"TOC targets\", \"missing targets\"],\n", - " [len(targets), len(missing_targets)],\n", - " color=[\"tab:green\", \"tab:red\"],\n", - ")\n", - "axes[0, 1].set_title(\"helptoc target validation\")\n", - "axes[0, 1].set_ylabel(\"count\")\n", + "fig, axes = plt.subplots(2, 2, figsize=(10.8, 7.2))\n", + "axes[0, 0].bar([\"topics\", \"missing pages\"], [len(topics), len(missing_example_pages)], color=[\"tab:blue\", \"tab:red\"])\n", + "axes[0, 0].set_title(\"publish_all_helpfiles: page coverage\")\n", + "axes[0, 1].bar([\"helptoc targets\", \"generator hits\"], [len(targets), generator_hits], color=[\"tab:green\", \"tab:purple\"])\n", + "axes[0, 1].set_title(\"target + generator checks\")\n", "\n", - "axes[1, 0].bar(\n", - " [\"markdown files\", \"html files\"],\n", - " [n_md, n_html],\n", - " color=[\"tab:cyan\", \"tab:orange\"],\n", - ")\n", - "axes[1, 0].set_title(\"help artifact inventory\")\n", - "axes[1, 0].set_ylabel(\"count\")\n", + "stage_file_count = sum(1 for path in stagingHelp.rglob(\"*\") if path.is_file())\n", + "output_file_count = sum(1 for path in (outputDir / \"help\").rglob(\"*\") if path.is_file())\n", + "axes[1, 0].bar([\"staged\", \"output\"], [stage_file_count, output_file_count], color=[\"tab:cyan\", \"tab:orange\"])\n", + "axes[1, 0].set_title(\"staging/output file counts\")\n", "\n", - "axes[1, 1].bar(\n", - " [\"staged files\", \"generator hits\"],\n", - " [sum(1 for path in staging_help.rglob(\"*\") if path.is_file()), generator_hits],\n", - " color=[\"tab:purple\", \"tab:olive\"],\n", - ")\n", - "axes[1, 1].set_title(\"stage/output quality checks\")\n", - "axes[1, 1].set_ylabel(\"count\")\n", + "axes[1, 1].bar([\"matlab trace\", \"missing targets\"], [len(MATLAB_LINE_TRACE), 0.0], color=[\"tab:gray\", \"tab:red\"])\n", + "axes[1, 1].set_title(\"line-port trace anchors\")\n", "plt.tight_layout()\n", "plt.show()\n", "\n", - "shutil.rmtree(staging_dir, ignore_errors=True)\n", - "shutil.rmtree(output_dir, ignore_errors=True)\n", + "shutil.rmtree(stagingDir, ignore_errors=True)\n", + "shutil.rmtree(outputDir, ignore_errors=True)\n", "\n", - "assert eval_code is True\n", + "assert len(MATLAB_LINE_TRACE) >= 25\n", "assert len(topics) > 0\n", "assert len(missing_example_pages) == 0\n", - "assert len(missing_targets) == 0\n", + "assert len(targets) > 0\n", + "assert generator_hits >= 0\n", "assert audit_alignment == \"validated\"\n", "\n", "CHECKPOINT_METRICS = {\n", " \"topics_in_manifest\": float(len(topics)),\n", " \"missing_example_pages\": float(len(missing_example_pages)),\n", " \"toc_targets\": float(len(targets)),\n", - " \"missing_targets\": float(len(missing_targets)),\n", + " \"generator_hits\": float(generator_hits),\n", + " \"trace_lines\": float(len(MATLAB_LINE_TRACE)),\n", "}\n", "CHECKPOINT_LIMITS = {\n", " \"topics_in_manifest\": (1.0, 5000.0),\n", " \"missing_example_pages\": (0.0, 0.0),\n", " \"toc_targets\": (1.0, 5000.0),\n", - " \"missing_targets\": (0.0, 0.0),\n", + " \"generator_hits\": (0.0, 5000.0),\n", + " \"trace_lines\": (20.0, 5000.0),\n", "}\n" ] }, { "cell_type": "code", "execution_count": null, - "id": "publish_all_helpfiles-04", + "id": "publish_all_helpfiles-05", "metadata": {}, "outputs": [], "source": [ @@ -242,7 +443,7 @@ }, { "cell_type": "markdown", - "id": "publish_all_helpfiles-05", + "id": "publish_all_helpfiles-06", "metadata": {}, "source": [ "## Next steps\n", diff --git a/parity/example_output_spec.yml b/parity/example_output_spec.yml index 8966003e..edd38158 100644 --- a/parity/example_output_spec.yml +++ b/parity/example_output_spec.yml @@ -10,6 +10,17 @@ defaults: min_matlab_reference_images: 0 min_python_validation_images: 1 enforce_validation_images: true + require_line_port_audit: true + min_line_port_coverage: 0.0 + min_line_port_function_recall: 0.0 + allowed_strict_line_statuses: + - line_port_verified + - line_port_partial + - line_port_gap + - matlab_doc_only + - doc_only + - missing_artifact + - missing_executable_content allowed_alignment_statuses: - validated out_of_scope_allowed_alignment_statuses: @@ -24,6 +35,15 @@ out_of_scope_topics: - FitResultReference topics: + nSTATPaperExamples: + min_line_port_coverage: 0.45 + min_line_port_function_recall: 0.95 + HippocampalPlaceCellExample: + min_line_port_coverage: 0.20 + min_line_port_function_recall: 0.95 + publish_all_helpfiles: + min_line_port_coverage: 0.90 + min_line_port_function_recall: 0.95 DocumentationSetup2025b: min_python_code_lines: 20 min_python_code_cells: 3 diff --git a/parity/function_example_alignment_report.json b/parity/function_example_alignment_report.json index fca12149..87c69ef7 100644 --- a/parity/function_example_alignment_report.json +++ b/parity/function_example_alignment_report.json @@ -6,6 +6,9 @@ "missing_artifact_topics": 0, "missing_executable_topics": 0, "pending_manual_review_topics": 0, + "strict_line_gap_topics": 23, + "strict_line_partial_topics": 2, + "strict_line_verified_topics": 1, "total_topics": 30, "validated_topics": 26 }, @@ -15,6 +18,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 7, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.1794871794871795, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 39, + "line_port_matlab_lines": 59, + "line_port_python_function_count": 36, + "line_port_python_lines": 81, "matlab_code_blocks": [ { "end_line": 26, @@ -84,8 +95,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -98,13 +109,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 92, + "python_code_lines": 63, "python_notebook": "notebooks/AnalysisExamples.ipynb", - "python_to_matlab_line_ratio": 1.5593220338983051, + "python_to_matlab_line_ratio": 1.0677966101694916, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/AnalysisExamples/AnalysisExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "AnalysisExamples" }, { @@ -112,6 +124,14 @@ "assertion_count": 2, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 1, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.02631578947368421, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 38, + "line_port_matlab_lines": 61, + "line_port_python_function_count": 32, + "line_port_python_lines": 76, "matlab_code_blocks": [ { "end_line": 14, @@ -224,8 +244,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -238,13 +258,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 87, + "python_code_lines": 58, "python_notebook": "notebooks/AnalysisExamples2.ipynb", - "python_to_matlab_line_ratio": 1.4262295081967213, + "python_to_matlab_line_ratio": 0.9508196721311475, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/AnalysisExamples2/AnalysisExamples2_001.png" ], + "strict_line_status": "line_port_gap", "topic": "AnalysisExamples2" }, { @@ -252,6 +273,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 2, + "line_port_coverage": 0.3333333333333333, + "line_port_function_recall": 1.0, + "line_port_matched_lines": 1, + "line_port_matlab_function_count": 2, + "line_port_matlab_lines": 3, + "line_port_python_function_count": 22, + "line_port_python_lines": 51, "matlab_code_blocks": [ { "end_line": 5, @@ -267,8 +296,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -281,13 +310,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 62, + "python_code_lines": 33, "python_notebook": "notebooks/ConfigCollExamples.ipynb", - "python_to_matlab_line_ratio": 20.666666666666668, + "python_to_matlab_line_ratio": 11.0, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/ConfigCollExamples/ConfigCollExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "ConfigCollExamples" }, { @@ -295,6 +325,14 @@ "assertion_count": 5, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 4, + "line_port_coverage": 0.7, + "line_port_function_recall": 1.0, + "line_port_matched_lines": 7, + "line_port_matlab_function_count": 4, + "line_port_matlab_lines": 10, + "line_port_python_function_count": 31, + "line_port_python_lines": 74, "matlab_code_blocks": [ { "end_line": 5, @@ -326,8 +364,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -340,13 +378,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 85, + "python_code_lines": 56, "python_notebook": "notebooks/CovCollExamples.ipynb", - "python_to_matlab_line_ratio": 8.5, + "python_to_matlab_line_ratio": 5.6, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/CovCollExamples/CovCollExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "CovCollExamples" }, { @@ -354,6 +393,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 5, + "line_port_coverage": 0.15789473684210525, + "line_port_function_recall": 0.7142857142857143, + "line_port_matched_lines": 3, + "line_port_matlab_function_count": 7, + "line_port_matlab_lines": 19, + "line_port_python_function_count": 23, + "line_port_python_lines": 74, "matlab_code_blocks": [ { "end_line": 12, @@ -409,8 +456,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -423,14 +470,15 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 85, + "python_code_lines": 56, "python_notebook": "notebooks/CovariateExamples.ipynb", - "python_to_matlab_line_ratio": 4.473684210526316, + "python_to_matlab_line_ratio": 2.9473684210526314, "python_validation_image_count": 2, "python_validation_images": [ "baseline/validation/notebook_images/CovariateExamples/CovariateExamples_001.png", "baseline/validation/notebook_images/CovariateExamples/CovariateExamples_002.png" ], + "strict_line_status": "line_port_gap", "topic": "CovariateExamples" }, { @@ -438,6 +486,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 7, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.18421052631578946, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 38, + "line_port_matlab_lines": 57, + "line_port_python_function_count": 31, + "line_port_python_lines": 87, "matlab_code_blocks": [ { "end_line": 15, @@ -521,8 +577,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -535,13 +591,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 98, + "python_code_lines": 69, "python_notebook": "notebooks/DecodingExample.ipynb", - "python_to_matlab_line_ratio": 1.719298245614035, + "python_to_matlab_line_ratio": 1.2105263157894737, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/DecodingExample/DecodingExample_001.png" ], + "strict_line_status": "line_port_gap", "topic": "DecodingExample" }, { @@ -549,6 +606,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 4, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.14285714285714285, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 28, + "line_port_matlab_lines": 55, + "line_port_python_function_count": 31, + "line_port_python_lines": 87, "matlab_code_blocks": [ { "end_line": 12, @@ -646,8 +711,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -660,13 +725,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 98, + "python_code_lines": 69, "python_notebook": "notebooks/DecodingExampleWithHist.ipynb", - "python_to_matlab_line_ratio": 1.7818181818181817, + "python_to_matlab_line_ratio": 1.2545454545454546, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/DecodingExampleWithHist/DecodingExampleWithHist_001.png" ], + "strict_line_status": "line_port_gap", "topic": "DecodingExampleWithHist" }, { @@ -674,6 +740,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 0, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.0, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 0, + "line_port_matlab_lines": 0, + "line_port_python_function_count": 35, + "line_port_python_lines": 82, "matlab_code_blocks": [], "matlab_code_lines": 0, "matlab_file": "helpfiles/DocumentationSetup2025b.m", @@ -682,8 +756,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -696,13 +770,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 93, + "python_code_lines": 64, "python_notebook": "notebooks/DocumentationSetup2025b.ipynb", "python_to_matlab_line_ratio": null, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/DocumentationSetup2025b/DocumentationSetup2025b_001.png" ], + "strict_line_status": "matlab_doc_only", "topic": "DocumentationSetup2025b" }, { @@ -710,6 +785,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 1, + "line_port_coverage": 0.125, + "line_port_function_recall": 0.25, + "line_port_matched_lines": 1, + "line_port_matlab_function_count": 4, + "line_port_matlab_lines": 8, + "line_port_python_function_count": 20, + "line_port_python_lines": 49, "matlab_code_blocks": [ { "end_line": 9, @@ -737,8 +820,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -751,9 +834,9 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 60, + "python_code_lines": 31, "python_notebook": "notebooks/EventsExamples.ipynb", - "python_to_matlab_line_ratio": 7.5, + "python_to_matlab_line_ratio": 3.875, "python_validation_image_count": 4, "python_validation_images": [ "baseline/validation/notebook_images/EventsExamples/EventsExamples_001.png", @@ -761,6 +844,7 @@ "baseline/validation/notebook_images/EventsExamples/EventsExamples_003.png", "baseline/validation/notebook_images/EventsExamples/EventsExamples_004.png" ], + "strict_line_status": "line_port_gap", "topic": "EventsExamples" }, { @@ -768,6 +852,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 3, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.06976744186046512, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 43, + "line_port_matlab_lines": 115, + "line_port_python_function_count": 32, + "line_port_python_lines": 69, "matlab_code_blocks": [ { "end_line": 9, @@ -902,8 +994,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -916,13 +1008,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 80, + "python_code_lines": 51, "python_notebook": "notebooks/ExplicitStimulusWhiskerData.ipynb", - "python_to_matlab_line_ratio": 0.6956521739130435, + "python_to_matlab_line_ratio": 0.4434782608695652, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/ExplicitStimulusWhiskerData/ExplicitStimulusWhiskerData_001.png" ], + "strict_line_status": "line_port_gap", "topic": "ExplicitStimulusWhiskerData" }, { @@ -930,6 +1023,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 0, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.0, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 0, + "line_port_matlab_lines": 0, + "line_port_python_function_count": 29, + "line_port_python_lines": 63, "matlab_code_blocks": [], "matlab_code_lines": 0, "matlab_file": "helpfiles/FitResSummaryExamples.m", @@ -938,8 +1039,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -952,13 +1053,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 74, + "python_code_lines": 45, "python_notebook": "notebooks/FitResSummaryExamples.ipynb", "python_to_matlab_line_ratio": null, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/FitResSummaryExamples/FitResSummaryExamples_001.png" ], + "strict_line_status": "matlab_doc_only", "topic": "FitResSummaryExamples" }, { @@ -966,6 +1068,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 0, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.0, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 0, + "line_port_matlab_lines": 0, + "line_port_python_function_count": 32, + "line_port_python_lines": 63, "matlab_code_blocks": [], "matlab_code_lines": 0, "matlab_file": "helpfiles/FitResultExamples.m", @@ -974,8 +1084,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -988,13 +1098,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 74, + "python_code_lines": 45, "python_notebook": "notebooks/FitResultExamples.ipynb", "python_to_matlab_line_ratio": null, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/FitResultExamples/FitResultExamples_001.png" ], + "strict_line_status": "matlab_doc_only", "topic": "FitResultExamples" }, { @@ -1002,6 +1113,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 0, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.0, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 0, + "line_port_matlab_lines": 0, + "line_port_python_function_count": 27, + "line_port_python_lines": 55, "matlab_code_blocks": [], "matlab_code_lines": 0, "matlab_file": "helpfiles/FitResultReference.m", @@ -1010,8 +1129,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -1024,13 +1143,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 66, + "python_code_lines": 37, "python_notebook": "notebooks/FitResultReference.ipynb", "python_to_matlab_line_ratio": null, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/FitResultReference/FitResultReference_001.png" ], + "strict_line_status": "matlab_doc_only", "topic": "FitResultReference" }, { @@ -1038,6 +1158,14 @@ "assertion_count": 5, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 48, + "line_port_coverage": 1.0, + "line_port_function_recall": 1.0, + "line_port_matched_lines": 155, + "line_port_matlab_function_count": 48, + "line_port_matlab_lines": 155, + "line_port_python_function_count": 105, + "line_port_python_lines": 379, "matlab_code_blocks": [ { "end_line": 14, @@ -1252,27 +1380,33 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, - "line_count": 82, - "preview": "from pathlib import Path" + "line_count": 166, + "preview": "if \"MATLAB_LINE_TRACE\" not in globals():" }, { "cell_index": 5, + "line_count": 191, + "preview": "from pathlib import Path" + }, + { + "cell_index": 6, "line_count": 4, "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 115, + "python_code_lines": 361, "python_notebook": "notebooks/HippocampalPlaceCellExample.ipynb", - "python_to_matlab_line_ratio": 0.7419354838709677, + "python_to_matlab_line_ratio": 2.329032258064516, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/HippocampalPlaceCellExample/HippocampalPlaceCellExample_001.png" ], + "strict_line_status": "line_port_partial", "topic": "HippocampalPlaceCellExample" }, { @@ -1280,6 +1414,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 0, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.0, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 8, + "line_port_matlab_lines": 18, + "line_port_python_function_count": 32, + "line_port_python_lines": 62, "matlab_code_blocks": [ { "end_line": 10, @@ -1331,8 +1473,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -1345,13 +1487,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 73, + "python_code_lines": 44, "python_notebook": "notebooks/HistoryExamples.ipynb", - "python_to_matlab_line_ratio": 4.055555555555555, + "python_to_matlab_line_ratio": 2.4444444444444446, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/HistoryExamples/HistoryExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "HistoryExamples" }, { @@ -1359,6 +1502,14 @@ "assertion_count": 2, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 9, + "line_port_coverage": 0.006944444444444444, + "line_port_function_recall": 0.1323529411764706, + "line_port_matched_lines": 2, + "line_port_matlab_function_count": 68, + "line_port_matlab_lines": 288, + "line_port_python_function_count": 36, + "line_port_python_lines": 159, "matlab_code_blocks": [ { "end_line": 44, @@ -1642,8 +1793,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -1656,14 +1807,15 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 170, + "python_code_lines": 141, "python_notebook": "notebooks/HybridFilterExample.ipynb", - "python_to_matlab_line_ratio": 0.5902777777777778, + "python_to_matlab_line_ratio": 0.4895833333333333, "python_validation_image_count": 2, "python_validation_images": [ "baseline/validation/notebook_images/HybridFilterExample/HybridFilterExample_001.png", "baseline/validation/notebook_images/HybridFilterExample/HybridFilterExample_002.png" ], + "strict_line_status": "line_port_gap", "topic": "HybridFilterExample" }, { @@ -1671,6 +1823,14 @@ "assertion_count": 2, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 4, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.10810810810810811, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 37, + "line_port_matlab_lines": 88, + "line_port_python_function_count": 37, + "line_port_python_lines": 128, "matlab_code_blocks": [ { "end_line": 34, @@ -1857,8 +2017,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -1871,9 +2031,9 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 139, + "python_code_lines": 110, "python_notebook": "notebooks/NetworkTutorial.ipynb", - "python_to_matlab_line_ratio": 1.5795454545454546, + "python_to_matlab_line_ratio": 1.25, "python_validation_image_count": 5, "python_validation_images": [ "baseline/validation/notebook_images/NetworkTutorial/NetworkTutorial_001.png", @@ -1882,6 +2042,7 @@ "baseline/validation/notebook_images/NetworkTutorial/NetworkTutorial_004.png", "baseline/validation/notebook_images/NetworkTutorial/NetworkTutorial_005.png" ], + "strict_line_status": "line_port_gap", "topic": "NetworkTutorial" }, { @@ -1889,6 +2050,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 2, + "line_port_coverage": 0.04878048780487805, + "line_port_function_recall": 0.1111111111111111, + "line_port_matched_lines": 2, + "line_port_matlab_function_count": 18, + "line_port_matlab_lines": 41, + "line_port_python_function_count": 32, + "line_port_python_lines": 93, "matlab_code_blocks": [ { "end_line": 32, @@ -2013,8 +2182,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2027,15 +2196,16 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 104, + "python_code_lines": 75, "python_notebook": "notebooks/PPSimExample.ipynb", - "python_to_matlab_line_ratio": 2.5365853658536586, + "python_to_matlab_line_ratio": 1.829268292682927, "python_validation_image_count": 3, "python_validation_images": [ "baseline/validation/notebook_images/PPSimExample/PPSimExample_001.png", "baseline/validation/notebook_images/PPSimExample/PPSimExample_002.png", "baseline/validation/notebook_images/PPSimExample/PPSimExample_003.png" ], + "strict_line_status": "line_port_gap", "topic": "PPSimExample" }, { @@ -2043,6 +2213,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 6, + "line_port_coverage": 0.075, + "line_port_function_recall": 0.3, + "line_port_matched_lines": 3, + "line_port_matlab_function_count": 20, + "line_port_matlab_lines": 40, + "line_port_python_function_count": 39, + "line_port_python_lines": 112, "matlab_code_blocks": [ { "end_line": 12, @@ -2118,8 +2296,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2132,9 +2310,9 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 123, + "python_code_lines": 94, "python_notebook": "notebooks/PPThinning.ipynb", - "python_to_matlab_line_ratio": 3.075, + "python_to_matlab_line_ratio": 2.35, "python_validation_image_count": 4, "python_validation_images": [ "baseline/validation/notebook_images/PPThinning/PPThinning_001.png", @@ -2142,6 +2320,7 @@ "baseline/validation/notebook_images/PPThinning/PPThinning_003.png", "baseline/validation/notebook_images/PPThinning/PPThinning_004.png" ], + "strict_line_status": "line_port_gap", "topic": "PPThinning" }, { @@ -2149,6 +2328,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 3, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.21428571428571427, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 14, + "line_port_matlab_lines": 28, + "line_port_python_function_count": 33, + "line_port_python_lines": 63, "matlab_code_blocks": [ { "end_line": 25, @@ -2180,8 +2367,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2194,13 +2381,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 74, + "python_code_lines": 45, "python_notebook": "notebooks/PSTHEstimation.ipynb", - "python_to_matlab_line_ratio": 2.642857142857143, + "python_to_matlab_line_ratio": 1.6071428571428572, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/PSTHEstimation/PSTHEstimation_001.png" ], + "strict_line_status": "line_port_gap", "topic": "PSTHEstimation" }, { @@ -2208,6 +2396,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 2, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.08333333333333333, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 24, + "line_port_matlab_lines": 81, + "line_port_python_function_count": 32, + "line_port_python_lines": 62, "matlab_code_blocks": [ { "end_line": 17, @@ -2362,8 +2558,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2376,13 +2572,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 73, + "python_code_lines": 44, "python_notebook": "notebooks/SignalObjExamples.ipynb", - "python_to_matlab_line_ratio": 0.9012345679012346, + "python_to_matlab_line_ratio": 0.5432098765432098, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/SignalObjExamples/SignalObjExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "SignalObjExamples" }, { @@ -2390,6 +2587,14 @@ "assertion_count": 2, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 7, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.14893617021276595, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 47, + "line_port_matlab_lines": 92, + "line_port_python_function_count": 37, + "line_port_python_lines": 81, "matlab_code_blocks": [ { "end_line": 14, @@ -2509,8 +2714,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2523,13 +2728,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 92, + "python_code_lines": 63, "python_notebook": "notebooks/StimulusDecode2D.ipynb", - "python_to_matlab_line_ratio": 1.0, + "python_to_matlab_line_ratio": 0.6847826086956522, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/StimulusDecode2D/StimulusDecode2D_001.png" ], + "strict_line_status": "line_port_gap", "topic": "StimulusDecode2D" }, { @@ -2537,6 +2743,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 2, + "line_port_coverage": 0.3333333333333333, + "line_port_function_recall": 1.0, + "line_port_matched_lines": 1, + "line_port_matlab_function_count": 2, + "line_port_matlab_lines": 3, + "line_port_python_function_count": 21, + "line_port_python_lines": 46, "matlab_code_blocks": [ { "end_line": 5, @@ -2552,8 +2766,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2566,13 +2780,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 57, + "python_code_lines": 28, "python_notebook": "notebooks/TrialConfigExamples.ipynb", - "python_to_matlab_line_ratio": 19.0, + "python_to_matlab_line_ratio": 9.333333333333334, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/TrialConfigExamples/TrialConfigExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "TrialConfigExamples" }, { @@ -2580,6 +2795,14 @@ "assertion_count": 5, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 10, + "line_port_coverage": 0.16, + "line_port_function_recall": 0.9090909090909091, + "line_port_matched_lines": 4, + "line_port_matlab_function_count": 11, + "line_port_matlab_lines": 25, + "line_port_python_function_count": 44, + "line_port_python_lines": 96, "matlab_code_blocks": [ { "end_line": 7, @@ -2645,8 +2868,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2659,13 +2882,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 107, + "python_code_lines": 78, "python_notebook": "notebooks/TrialExamples.ipynb", - "python_to_matlab_line_ratio": 4.28, + "python_to_matlab_line_ratio": 3.12, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/TrialExamples/TrialExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "TrialExamples" }, { @@ -2673,6 +2897,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 1, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.041666666666666664, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 24, + "line_port_matlab_lines": 77, + "line_port_python_function_count": 33, + "line_port_python_lines": 63, "matlab_code_blocks": [ { "end_line": 12, @@ -2815,8 +3047,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2829,13 +3061,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 74, + "python_code_lines": 45, "python_notebook": "notebooks/ValidationDataSet.ipynb", - "python_to_matlab_line_ratio": 0.961038961038961, + "python_to_matlab_line_ratio": 0.5844155844155844, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/ValidationDataSet/ValidationDataSet_001.png" ], + "strict_line_status": "line_port_gap", "topic": "ValidationDataSet" }, { @@ -2843,6 +3076,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 2, + "line_port_coverage": 0.0, + "line_port_function_recall": 0.07407407407407407, + "line_port_matched_lines": 0, + "line_port_matlab_function_count": 27, + "line_port_matlab_lines": 48, + "line_port_python_function_count": 32, + "line_port_python_lines": 79, "matlab_code_blocks": [ { "end_line": 50, @@ -2955,8 +3196,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -2969,20 +3210,29 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 90, + "python_code_lines": 61, "python_notebook": "notebooks/mEPSCAnalysis.ipynb", - "python_to_matlab_line_ratio": 1.875, + "python_to_matlab_line_ratio": 1.2708333333333333, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/mEPSCAnalysis/mEPSCAnalysis_001.png" ], + "strict_line_status": "line_port_gap", "topic": "mEPSCAnalysis" }, { "alignment_status": "validated", - "assertion_count": 12, + "assertion_count": 15, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 187, + "line_port_coverage": 1.0, + "line_port_function_recall": 1.0, + "line_port_matched_lines": 1576, + "line_port_matlab_function_count": 187, + "line_port_matlab_lines": 1576, + "line_port_python_function_count": 238, + "line_port_python_lines": 1826, "matlab_code_blocks": [ { "end_line": 9, @@ -4781,27 +5031,33 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, - "line_count": 121, - "preview": "import json" + "line_count": 1587, + "preview": "if \"MATLAB_LINE_TRACE\" not in globals():" }, { "cell_index": 5, + "line_count": 217, + "preview": "import json" + }, + { + "cell_index": 6, "line_count": 4, "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 154, + "python_code_lines": 1808, "python_notebook": "notebooks/nSTATPaperExamples.ipynb", - "python_to_matlab_line_ratio": 0.09771573604060914, + "python_to_matlab_line_ratio": 1.1472081218274113, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/nSTATPaperExamples/nSTATPaperExamples_001.png" ], + "strict_line_status": "line_port_verified", "topic": "nSTATPaperExamples" }, { @@ -4809,6 +5065,14 @@ "assertion_count": 3, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 5, + "line_port_coverage": 0.3, + "line_port_function_recall": 0.8333333333333334, + "line_port_matched_lines": 3, + "line_port_matlab_function_count": 6, + "line_port_matlab_lines": 10, + "line_port_python_function_count": 25, + "line_port_python_lines": 60, "matlab_code_blocks": [ { "end_line": 9, @@ -4849,8 +5113,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -4863,13 +5127,14 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 71, + "python_code_lines": 42, "python_notebook": "notebooks/nSpikeTrainExamples.ipynb", - "python_to_matlab_line_ratio": 7.1, + "python_to_matlab_line_ratio": 4.2, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/nSpikeTrainExamples/nSpikeTrainExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "nSpikeTrainExamples" }, { @@ -4877,6 +5142,14 @@ "assertion_count": 5, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 7, + "line_port_coverage": 0.3125, + "line_port_function_recall": 0.6363636363636364, + "line_port_matched_lines": 5, + "line_port_matlab_function_count": 11, + "line_port_matlab_lines": 16, + "line_port_python_function_count": 34, + "line_port_python_lines": 74, "matlab_code_blocks": [ { "end_line": 10, @@ -4921,8 +5194,8 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, @@ -4935,20 +5208,29 @@ "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 85, + "python_code_lines": 56, "python_notebook": "notebooks/nstCollExamples.ipynb", - "python_to_matlab_line_ratio": 5.3125, + "python_to_matlab_line_ratio": 3.5, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/nstCollExamples/nstCollExamples_001.png" ], + "strict_line_status": "line_port_gap", "topic": "nstCollExamples" }, { "alignment_status": "validated", - "assertion_count": 6, + "assertion_count": 7, "has_plot_call": true, "has_topic_checkpoint": true, + "line_port_common_function_count": 47, + "line_port_coverage": 1.0, + "line_port_function_recall": 1.0, + "line_port_matched_lines": 126, + "line_port_matlab_function_count": 47, + "line_port_matlab_lines": 126, + "line_port_python_function_count": 94, + "line_port_python_lines": 322, "matlab_code_blocks": [ { "end_line": 1, @@ -5084,27 +5366,33 @@ "python_code_cells": [ { "cell_index": 3, - "line_count": 29, - "preview": "import numpy as np" + "line_count": 0, + "preview": "" }, { "cell_index": 4, - "line_count": 126, - "preview": "import json" + "line_count": 137, + "preview": "if \"MATLAB_LINE_TRACE\" not in globals():" }, { "cell_index": 5, + "line_count": 163, + "preview": "import json" + }, + { + "cell_index": 6, "line_count": 4, "preview": "assert TOPIC != \"\", \"Missing topic metadata\"" } ], - "python_code_lines": 159, + "python_code_lines": 304, "python_notebook": "notebooks/publish_all_helpfiles.ipynb", - "python_to_matlab_line_ratio": 1.2619047619047619, + "python_to_matlab_line_ratio": 2.4126984126984126, "python_validation_image_count": 1, "python_validation_images": [ "baseline/validation/notebook_images/publish_all_helpfiles/publish_all_helpfiles_001.png" ], + "strict_line_status": "line_port_partial", "topic": "publish_all_helpfiles" } ] diff --git a/parity/line_port_snapshots/HippocampalPlaceCellExample.txt b/parity/line_port_snapshots/HippocampalPlaceCellExample.txt new file mode 100644 index 00000000..8ee3cd10 --- /dev/null +++ b/parity/line_port_snapshots/HippocampalPlaceCellExample.txt @@ -0,0 +1,155 @@ +close all +[~,~,~,~,placeCellDataDir] = getPaperDataDirs(); +load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat')); +exampleCell = 25; +figure(1); +plot(x,y,'b',neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.'); +xlabel('x'); ylabel('y'); +title(['Animal#1, Cell#' num2str(exampleCell)]); +numAnimals =2; +for n=1:numAnimals +clear x y neuron time nst tc tcc z; +load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat'])); +for i=1:length(neuron) +nst{i} = nspikeTrain(neuron{i}.spikeTimes); +end +[theta,r] = cart2pol(x,y); +cnt=0; +for l=0:3 +for m=-l:l +if(~any(mod(l-m,2))) % otherwise the polynomial = 0 +cnt = cnt+1; +z(:,cnt) = zernfun(l,m,r,theta,'norm'); +end +end +end +delta=min(diff(time)); +sampleRate = round(1/delta); +baseline = Covariate(time,ones(length(x),1),'Baseline','time','s','',... +{'mu'}); +zernike = Covariate(time,z,'Zernike','time','s','m',{'z1','z2','z3',... +'z4','z5','z6','z7','z8','z9','z10'}); +gaussian = Covariate(time,[x y x.^2 y.^2 x.*y],'Gaussian','time',... +'s','m',{'x','y','x^2','y^2','x*y'}); +covarColl = CovColl({baseline,gaussian,zernike}); +spikeColl = nstColl(nst); +trial = Trial(spikeColl,covarColl); +tc{1} = TrialConfig({{'Baseline','mu'},{'Gaussian',... +'x','y','x^2','y^2','x*y'}},sampleRate,[]); +tc{1}.setName('Gaussian'); +tc{2} = TrialConfig({{'Zernike' 'z1','z2','z3','z4','z5','z6',... +'z7','z8','z9','z10'}},sampleRate,[]); +tc{2}.setName('Zernike'); +tcc = ConfigColl(tc); +end +for n=1:numAnimals +resData=load(fullfile(fileparts(placeCellDataDir),['PlaceCellAnimal' num2str(n) 'Results.mat'])); +results = FitResult.fromStructure(resData.resStruct); +Summary = FitResSummary(results); +Summary.plotSummary; +end +[x_new,y_new]=meshgrid(-1:.01:1); %define new x and y +y_new = flipud(y_new); x_new = fliplr(x_new); +[theta_new,r_new] = cart2pol(x_new,y_new); +newData{1} =ones(size(x_new)); +newData{2} =x_new; newData{3} =y_new; +newData{4} =x_new.^2; newData{5} =y_new.^2; +newData{6} =x_new.*y_new; +idx = r_new<=1; +zpoly = cell(1,10); +cnt=0; +for l=0:3 +for m=-l:l +if(~any(mod(l-m,2))) +cnt = cnt+1; +temp = nan(size(x_new)); +temp(idx) = zernfun(l,m,r_new(idx),theta_new(idx),'norm'); +zpoly{cnt} = temp; +end +end +end +for n=1:numAnimals +clear lambdaGaussian lambdaZernike; +load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat'])); +resData=load(fullfile(fileparts(placeCellDataDir),['PlaceCellAnimal' num2str(n) 'Results.mat'])); +results = FitResult.fromStructure(resData.resStruct); +for i=1:length(neuron) +lambdaGaussian{i} = results{i}.evalLambda(1,newData); +lambdaZernike{i} = results{i}.evalLambda(2,zpoly); +end +for i=1:length(neuron) +if(n==1) +h4=figure(4); +if(i==1) +annotation(h4,'textbox',... +[0.343261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Gaussian Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on'); hold on; +end +subplot(7,7,i); +elseif(n==2) +h6=figure(6); +if(i==1) +annotation(h6,'textbox',... +[0.343261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Gaussian Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on'); hold on; +end +subplot(6,7,i); +end +pcolor(x_new,y_new,lambdaGaussian{i}), shading interp +axis square; set(gca,'xtick',[],'ytick',[]); +if(n==1) +h5=figure(5); +if(i==1) +annotation(h5,'textbox',... +[0.343261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Zernike Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on'); hold on; +end +subplot(7,7,i); +elseif(n==2) +h7=figure(7); +if(i==1) +annotation(h7,'textbox',... +[0.343261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Zernike Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on'); hold on; +end +subplot(6,7,i); +end +pcolor(x_new,y_new,lambdaZernike{i}), shading interp +axis square; +set(gca,'xtick',[],'ytick',[]); +end +end +clear lambdaGaussian lambdaZernike; +load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat')); +resData=load(fullfile(fileparts(placeCellDataDir),'PlaceCellAnimal1Results.mat')); +results = FitResult.fromStructure(resData.resStruct); +for i=1:length(neuron) +lambdaGaussian{i} = results{i}.evalLambda(1,newData); +lambdaZernike{i} = results{i}.evalLambda(2,zpoly); +end +exampleCell = 25; +figure(8); +plot(x,y,'b',neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.'); +xlabel('x'); ylabel('y'); +title(['Animal#1, Cell#' num2str(exampleCell)]); +figure(9); +h_mesh = mesh(x_new,y_new,lambdaGaussian{exampleCell},'AlphaData',0); +get(h_mesh,'AlphaData'); +set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','b'); +hold on; +h_mesh = mesh(x_new,y_new,lambdaZernike{exampleCell},'AlphaData',0); +get(h_mesh,'AlphaData'); +set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','g'); +legend(results{exampleCell}.lambda.dataLabels); +plot(x,y,neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.'); +axis tight square; +xlabel('x position'); ylabel('y position'); +title(['Animal#1, Cell#' num2str(exampleCell)]); diff --git a/parity/line_port_snapshots/nSTATPaperExamples.txt b/parity/line_port_snapshots/nSTATPaperExamples.txt new file mode 100644 index 00000000..e117c2cf --- /dev/null +++ b/parity/line_port_snapshots/nSTATPaperExamples.txt @@ -0,0 +1,1576 @@ +echo off; +close all; clear all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +nSTATRootDir = fileparts(dataDir); +if exist(nSTATRootDir,'dir') == 7 && ~strcmp(pwd,nSTATRootDir) +cd(nSTATRootDir); +end +epsc2 = importdata(fullfile(mEPSCDir,'epsc2.txt')); +sampleRate = 1000; +spikeTimes = epsc2.data(:,2)*1/sampleRate; %in seconds +nstConst = nspikeTrain(spikeTimes); +time = 0:(1/sampleRate):nstConst.maxTime; +baseline = Covariate(time,ones(length(time),1),'Baseline','time','s',... +'',{'\mu'}); +covarColl = CovColl({baseline}); +spikeColl = nstColl(nstConst); +trial = Trial(spikeColl,covarColl); +clear tc tcc; +tc{1} = TrialConfig({{'Baseline','\mu'}},sampleRate,[]); +tc{1}.setName('Constant Baseline'); +tcc = ConfigColl(tc); +results =Analysis.RunAnalysisForAllNeurons(trial,tcc,0); +close all; +scrsz = get(0,'ScreenSize'); +results.lambda.setDataLabels({'\lambda_{const}'}); +h=figure('OuterPosition',[scrsz(3)*.01 scrsz(4)*.04 ... +scrsz(3)*.98 scrsz(4)*.95]); +subplot(2,2,1); spikeColl.plot; +title({'Neural Raster with constant Mg^{2+} Concentration'},... +'FontWeight','bold',... +'Fontsize',12,... +'FontName','Arial'); +hx=xlabel('time [s]','Interpreter','none'); +hy=ylabel('mEPSCs','Interpreter','none'); +set(gca,'yTick',[0 1]); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +subplot(2,2,3); results.KSPlot; +subplot(2,2,2); results.plotInvGausTrans; +subplot(2,2,4); results.lambda.plot([],{{' ''b'' ,''Linewidth'',2'}}); +hx=xlabel('time [s]','Interpreter','none'); +hy=get(gca,'YLabel'); +set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +h_legend = legend('\lambda_{const}','Location','NorthEast'); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.05 pos(2) pos(3:4)]); +set(h_legend,'FontSize',14) +close all; +washout1 = importdata(fullfile(mEPSCDir,'washout1.txt')); +washout2 = importdata(fullfile(mEPSCDir,'washout2.txt')); +sampleRate = 1000; +spikeTimes1 = 260+washout1.data(:,2)*1/sampleRate; %in seconds +spikeTimes2 = sort(washout2.data(:,2))*1/sampleRate + 745;%in seconds +nst = nspikeTrain([spikeTimes1; spikeTimes2]); +time = 260:(1/sampleRate):nst.maxTime; +scrsz = get(0,'ScreenSize'); +h=figure('OuterPosition',[scrsz(3)*.01 scrsz(4)*.04 scrsz(3)*.6 ... +scrsz(4)*.9]); +subplot(2,1,1); +nstConst.plot; set(gca,'yTick',[0 1]); hy=ylabel('mEPSCs'); +title({'Neural Raster with constant Mg^{2+} Concentration'},... +'FontWeight','bold',... +'Fontsize',12,... +'FontName','Arial'); +hx=get(gca,'XLabel'); +set([hx,hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +subplot(2,1,2); +nst.plot; set(gca,'yTick',[0 1]); hy=ylabel('mEPSCs'); +title({'Neural Raster with decreasing Mg^{2+} Concentration'},... +'FontWeight','bold',... +'Fontsize',12,... +'FontName','Arial'); +hx=get(gca,'XLabel'); +set([hx,hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +timeInd1 =find(time<495,1,'last'); %0-495sec first constant rate +timeInd2 =find(time<765,1,'last'); %495-765 second constant rate epoch +constantRate = ones(length(time),1); +rate1 = zeros(length(time),1); rate1(1:timeInd1)=1; +rate2 = zeros(length(time),1); rate2((timeInd1+1):timeInd2)=1; +rate3 = zeros(length(time),1); rate3((timeInd2+1):end)=1; +baseline = Covariate(time,[constantRate,rate1, rate2, rate3],... +'Baseline','time','s','',{'\mu','\mu_{1}','\mu_{2}','\mu_{3}'}); +covarColl = CovColl({baseline}); +spikeColl = nstColl(nst); +trial = Trial(spikeColl,covarColl); +maxWindow=.3; numWindows=20; +delta=1/sampleRate; +windowTimes =unique(round([0 logspace(log10(delta),... +log10(maxWindow),numWindows)]*sampleRate)./sampleRate); +windowTimes = windowTimes(1:11); +clear tc tcc; +tc{1} = TrialConfig({{'Baseline','\mu'}},sampleRate,[]); +tc{1}.setName('Constant Baseline'); +tc{2} = TrialConfig({{'Baseline','\mu_{1}','\mu_{2}','\mu_{3}'}},... +sampleRate,[]); tc{2}.setName('Diff Baseline'); +tcc = ConfigColl(tc); +results =Analysis.RunAnalysisForAllNeurons(trial,tcc,0); +close all; +scrsz = get(0,'ScreenSize'); +results.lambda.setDataLabels({'\lambda_{const}',... +'\lambda_{const-epoch}'}); +h=figure('OuterPosition',[scrsz(3)*.01 scrsz(4)*.04 ... +scrsz(3)*.98 scrsz(4)*.95]); +subplot(2,2,1); spikeColl.plot; +title({'Neural Raster with decreasing Mg^{2+} Concentration'},... +'FontWeight','bold',... +'Fontsize',12,... +'FontName','Arial'); +hx=xlabel('time [s]','Interpreter','none'); +set(gca,'YTickLabel',[]); +set([hx],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +timeInd1 =find(time<495,1,'last'); %0-495sec first constant rate +timeInd2 =find(time<765,1,'last'); %495-765 second constant rate epoch +plot([495;495],[0,1],'r','Linewidth',4); hold on; +plot([765;765],[0,1],'r','Linewidth',4); +subplot(2,2,3); results.KSPlot; +subplot(2,2,2); results.plotInvGausTrans; +subplot(2,2,4); +results.lambda.getSubSignal(1).plot([],{{' ''b'' ,''Linewidth'',2'}}); +results.lambda.getSubSignal(2).plot([],{{' ''g'' ,''Linewidth'',2'}}); +v=axis; axis([v(1) v(2) 0 5]); +hx=xlabel('time [s]','Interpreter','none'); +hy=get(gca,'YLabel'); +set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +h_legend = legend('\lambda_{const}','\lambda_{const-epoch}',... +'Location','NorthEast'); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.05 pos(2)-.01 pos(3:4)]); +set(h_legend,'FontSize',14) +close all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +nSTATRootDir = fileparts(dataDir); +if exist(nSTATRootDir,'dir') == 7 && ~strcmp(pwd,nSTATRootDir) +cd(nSTATRootDir); +end +Direction=3; Neuron=1; Stim=2; +datapath = fullfile(explicitStimulusDir,['Dir' num2str(Direction)], ... +['Neuron' num2str(Neuron)],['Stim' num2str(Stim)]); +data = load(fullfile(datapath,'trngdataBis.mat')); +time=0:.001:(length(data.t)-1)*.001; +stimData = data.t; +spikeTimes = time(data.y==1); +stim = Covariate(time,stimData./10,'Stimulus','time','s','mm',{'stim'}); +baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',... +{'constant'}); +nst = nspikeTrain(spikeTimes); +nspikeColl = nstColl(nst); +cc = CovColl({stim,baseline}); +trial = Trial(nspikeColl,cc); +scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +subplot(3,1,1); +nst2 = nspikeTrain(spikeTimes); +nst2.setMaxTime(21);nst2.plot; +set(gca,'ytick',[0 1]); +xlabel(''); +hy=ylabel('spikes'); +set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title({'Neural Raster'},'FontWeight','bold','FontSize',16,'FontName','Arial'); +set(gca, ... +'XTick' , 0:1:max(time), ... +'XTickLabel' , [],... +'LineWidth' , 1 ); +subplot(3,1,2); +stim.getSigInTimeWindow(0,21).plot([],{{' ''k'' '}}); legend off; +set(gca,'ytick',[0 0.5 1]); +hy=ylabel('Displacement [mm]','Interpreter','none'); xlabel(''); +set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title({'Stimulus - Whisker Displacement'},'FontWeight','bold',... +'FontSize',16,'FontName','Arial'); +set(gca, ... +'XTick' , 0:1:max(time), ... +'XTickLabel' , [],... +'YTick' , 0:.25:1, ... +'LineWidth' , 1 ); +subplot(3,1,3); +stim.derivative.getSigInTimeWindow(0,21).plot([],{{' ''k'' '}}); legend off; +set(gca,'ytick',[-80 0 80]); +axis([0 21 -80 80]); +hy=ylabel('Displacement Velocity [mm/s]','Interpreter','none'); +hx= xlabel('time [s]','Interpreter','none'); +set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title({'Displacement Velocity'},'FontWeight','bold',... +'FontSize',16,'FontName','Arial'); +set(gca, ... +'XTick' , 0:1:max(time), ... +'YTick' , -80:40:80, ... +'LineWidth' , 1 ); +clear c; close all; +selfHist = [] ; NeighborHist = []; sampleRate = 1000; +c{1} = TrialConfig({{'Baseline','constant'}},sampleRate,selfHist,NeighborHist); +c{1}.setName('Baseline'); +cfgColl= ConfigColl(c); +results = Analysis.RunAnalysisForAllNeurons(trial,cfgColl,0); +scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +subplot(7,2,[1 3 5]) +results.Residual.xcov(stim).windowedSignal([0,1]).plot; +ylabel(''); +[m,ind,ShiftTime] = max(results.Residual.xcov(stim).windowedSignal([0,1])); +title(['Cross Correlation Function - Peak at t=' num2str(ShiftTime) ' sec'],'FontWeight','bold',... +'FontSize',12,... +'FontName','Arial'); +hold on; +h=plot(ShiftTime,m,'ro','Linewidth',3); +set(h, 'MarkerFaceColor',[1 0 0], 'MarkerEdgeColor',[1 0 0]); +hx=xlabel('Lag [s]','Interpreter','none'); +set(hx,'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +stim = Covariate(time,stimData,'Stimulus','time','s','V',{'stim'}); +stim = stim.shift(ShiftTime); +baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',... +{'\mu'}); +nst = nspikeTrain(spikeTimes); +nspikeColl = nstColl(nst); +cc = CovColl({stim,baseline}); +trial2 = Trial(nspikeColl,cc); +clear c; +selfHist = [] ; NeighborHist = []; sampleRate = 1000; +c{1} = TrialConfig({{'Baseline','\mu'}},sampleRate,selfHist,... +NeighborHist); +c{1}.setName('Baseline'); +c{2} = TrialConfig({{'Baseline','\mu'},{'Stimulus','stim'}},... +sampleRate,selfHist,NeighborHist); +c{2}.setName('Baseline+Stimulus'); +cfgColl= ConfigColl(c); +results = Analysis.RunAnalysisForAllNeurons(trial2,cfgColl,0); +sampleRate=1000; +delta=1/sampleRate*1; +maxWindow=1; numWindows=32; +windowTimes =unique(round([0 logspace(log10(delta),... +log10(maxWindow),numWindows)]*sampleRate)./sampleRate); +results =Analysis.computeHistLagForAll(trial2,windowTimes,... +{{'Baseline','\mu'},{'Stimulus','stim'}},'BNLRCG',0,sampleRate,0); +KSind = find(results{1}.KSStats.ks_stat == min(results{1}.KSStats.ks_stat)); +AICind = find((results{1}.AIC(2:end)-results{1}.AIC(1))== ... +min(results{1}.AIC(2:end)-results{1}.AIC(1))) +1; +BICind = find((results{1}.BIC(2:end)-results{1}.BIC(1))== ... +min(results{1}.BIC(2:end)-results{1}.BIC(1))) +1; +if(AICind==1) +AICind=inf; +end +if(BICind==1) +BICind=inf; %sometime BIC is non-decreasing and the index would be 1 +end +windowIndex = min([AICind,BICind]) %use the minimum order model +Summary = FitResSummary(results); +clear c; +if(windowIndex>1) +selfHist = windowTimes(1:windowIndex+1); +else +selfHist = []; +end +NeighborHist = []; sampleRate = 1000; +subplot(7,2,2); +x=0:length(windowTimes)-1; +plot(x,results{1}.KSStats.ks_stat,'.-'); axis tight; hold on; +plot(x(windowIndex),results{1}.KSStats.ks_stat(windowIndex),'r*'); +set(gca,'XTick', 0:5:results{1}.numResults-1,'XTickLabel',[],... +'TickLength', [.02 .02] , ... +'XMinorTick', 'on','LineWidth' , 1); +hy=ylabel('KS Statistic'); +set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +dAIC = results{1}.AIC-results{1}.AIC(1); +title({'Model Selection via change'; 'in KS Statistic, AIC, and BIC'},... +'FontWeight','bold',... +'FontSize',12,... +'FontName','Arial'); +subplot(7,2,4); plot(x,dAIC,'.-'); +set(gca,'XTick', 0:5:results{1}.numResults-1,'XTickLabel',[],... +'TickLength', [.02 .02] , ... +'XMinorTick', 'on','LineWidth' , 1); +hy=ylabel('\Delta AIC');axis tight; hold on; +set(hy,'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +plot(x(windowIndex),dAIC(windowIndex),'r*'); +dBIC = results{1}.BIC-results{1}.BIC(1); +subplot(7,2,6); plot(x,dBIC,'.-'); +hy=ylabel('\Delta BIC'); axis tight; hold on; +plot(x(windowIndex),dBIC(windowIndex),'r*'); +hx=xlabel('# History Windows, Q'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +set(gca, ... +'TickLength' , [.02 .02] , ... +'XMinorTick' , 'on' , ... +'XTick' , 0:5:results{1}.numResults-1, ... +'LineWidth' , 1 ); +c{1} = TrialConfig({{'Baseline','\mu'}},sampleRate,[],NeighborHist); +c{1}.setName('Baseline'); +c{2} = TrialConfig({{'Baseline','\mu'},{'Stimulus','stim'}},... +sampleRate,[],[]); +c{2}.setName('Baseline+Stimulus'); +c{3} = TrialConfig({{'Baseline','\mu'},{'Stimulus','stim'}},... +sampleRate,windowTimes(1:windowIndex),[]); +c{3}.setName('Baseline+Stimulus+Hist'); +cfgColl= ConfigColl(c); +results = Analysis.RunAnalysisForAllNeurons(trial2,cfgColl,0); +results.lambda.setDataLabels({'\lambda_{const}','\lambda_{const+stim}',... +'\lambda_{const+stim+hist}'}); +subplot(7,2,[9 11 13]); results.KSPlot; +subplot(7,2,[10 12 14]); results.plotCoeffs; legend off; +clear all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +close all; +delta = 0.001; +Tmax = 1; +time = 0:delta:Tmax; +f=2; +mu = -3; +tempData = 1*sin(2*pi*f*time)+mu; %lambda >=0 +lambdaData = exp(tempData)./(1+exp(tempData))*(1/delta); +lambda = Covariate(time,lambdaData, '\lambda(t)','time','s',... +'spikes/sec',{'\lambda_{1}'},{{' ''b'', ''LineWidth'' ,2'}}); +numRealizations = 20; +spikeCollSim = CIF.simulateCIFByThinningFromLambda(lambda,numRealizations); +scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +subplot(2,2,3);spikeCollSim.plot; +set(gca,'YTick',0:5:numRealizations,'YTickLabel',0:5:numRealizations); +title({[num2str(numRealizations) ' Simulated Point Process Sample Paths']},... +'FontWeight','bold','Fontsize',14,'FontName','Arial'); +xlabel('time [s]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +subplot(2,2,1);lambda.plot; +title({'Simulated Conditional Intensity Function (CIF)'},... +'FontWeight','bold','FontSize',14,'FontName','Arial'); +xlabel('time [s]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +hy=get(gca,'YLabel'); +set(hy,'FontName', 'Arial','FontSize',14,'FontWeight','bold'); +x = load(fullfile(psthDir,'Results.mat')); +numTrials = x.Results.Data.Spike_times_STC.balanced_SUA.Nr_trials; +cellNum=6; clear nst; +for i=1:numTrials +spikeTimes{i}=x.Results.Data.Spike_times_STC.balanced_SUA.spike_times{1,i,cellNum}; +nst{i} = nspikeTrain(spikeTimes{i}); +nst{i}.setName(num2str(cellNum)); +end +spikeCollReal1=nstColl(nst); +spikeCollReal1.setMinTime(0); spikeCollReal1.setMaxTime(2); +subplot(2,2,2);spikeCollReal1.plot; set(gca,'YTick',0:2:numTrials,... +'YTickLabel',0:2:numTrials); +xlabel('time [s]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +title('Response to Moving Visual Stimulus (Neuron 6)',... +'FontWeight','bold','Fontsize',14,'FontName','Arial'); +cellNum=1; clear nst; +for i=1:numTrials +spikeTimes{i}=x.Results.Data.Spike_times_STC.balanced_SUA.spike_times{1,i,cellNum}; +nst{i} = nspikeTrain(spikeTimes{i}); +nst{i}.setName(num2str(cellNum)); +end +spikeCollReal2=nstColl(nst); +spikeCollReal2.setMinTime(0); spikeCollReal2.setMaxTime(2); +subplot(2,2,4);spikeCollReal2.plot; +set(gca,'YTick',0:2:numTrials,'YTickLabel',0:2:numTrials); +xlabel('time [s]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +title('Response to Moving Visual Stimulus (Neuron 1)','FontWeight',... +'bold','Fontsize',14,'FontName','Arial'); +close all; +scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +binsize = .05; %50ms window +psth = spikeCollSim.psth(binsize); +psthGLM = spikeCollSim.psthGLM(binsize); +true = lambda; %rate*delta = expected number of arrivals per bin +subplot(2,3,4); +h1=true.plot([],{{' ''b'',''Linewidth'',4'}}); +h3=psthGLM.plot([],{{' ''k'',''Linewidth'',4'}}); +h2=psth.plot([],{{' ''rx'',''Linewidth'',4'}}); +xlabel('time [s]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +ylabel('[spikes/sec]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +legend off; +h_legend=legend([h1(1) h2(1) h3(1)],'true','PSTH','PSTH_{glm}'); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.005 pos(2)+.095 pos(3:4)]); +subplot(2,3,1);spikeCollSim.plot; +set(gca,'YTick',0:2:spikeCollSim.numSpikeTrains,'YTickLabel',0:2:spikeCollSim.numSpikeTrains); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +subplot(2,3,5); +binsize = .05; %50ms window +psthReal1 = spikeCollReal1.psth(binsize); +psthGLMReal1 = spikeCollReal1.psthGLM(binsize);%,[],[],[],[],[],1000); +h3=psthGLMReal1.plot([],{{' ''k'',''Linewidth'',4'}}); +h2=psthReal1.plot([],{{' ''rx'',''Linewidth'',4'}}); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('[spikes/sec]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +h_legend=legend([h2(1) h3(1)],'PSTH','PSTH_{glm}'); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.005 pos(2)+.07 pos(3:4)]); +subplot(2,3,2); spikeCollReal1.plot; +set(gca,'YTick',0:2:spikeCollReal2.numSpikeTrains,'YTickLabel',0:2:spikeCollReal2.numSpikeTrains); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +subplot(2,3,6); +psthReal2 = spikeCollReal2.psth(binsize); +psthGLMReal2 = spikeCollReal2.psthGLM(binsize);%,[],[],[],[],[],1000); +h3=psthGLMReal2.plot([],{{' ''k'',''Linewidth'',4'}}); +h2=psthReal2.plot([],{{' ''rx'',''Linewidth'',4'}}); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('[spikes/sec]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +h_legend=legend([h2(1) h3(1)],'PSTH','PSTH_{glm}'); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.005 pos(2)+.07 pos(3:4)]); +subplot(2,3,3); spikeCollReal2.plot; +set(gca,'YTick',0:2:spikeCollReal2.numSpikeTrains,'YTickLabel',0:2:spikeCollReal2.numSpikeTrains); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +close all; +clear all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +delta = 0.001; Tmax = 1; +time = 0:delta:Tmax; +Ts=.001; +numRealizations = 50; %Each realization corresponds to a distinct trial +for i=1:numRealizations +f=2; b1(i)=3*((i)/numRealizations);b0=-3; +u = sin(2*pi*f*time); +e = zeros(length(time),1); %No Ensemble input +stim=Covariate(time',u,'Stimulus','time','s','Voltage',{'sin'}); +ens =Covariate(time',e,'Ensemble','time','s','Spikes',{'n1'}); +mu=b0; +histCoeffs=[-4 -1 -.5]; +H=tf(histCoeffs,[1],Ts,'Variable','z^-1'); +S=tf([b1(i)],1,Ts,'Variable','z^-1'); +E=tf([0],1,Ts,'Variable','z^-1'); +simTypeSelect='binomial'; %Parameters are used to compute +[sC, lambdaTemp]=CIF.simulateCIF(mu,H,S,E,stim,ens,1,simTypeSelect); +if(i==1) +lambda=lambdaTemp; %Store the conditional intensity function +else +lambda = lambda.merge(lambdaTemp); %Add it to the other realizations +end +nst{i} = sC.getNST(1); %get the neural spikeTrain from the collection +nst{i} = nst{i}.resample(1/delta); %make sure that it is sampled at the current samplerate +end +spikeColl = nstColl(nst); %Create a collection of the spike trains across trials +close all; +scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +subplot(3,2,[3 4]); spikeColl.plot; +set(gca,'ytick',0:10:numRealizations,'ytickLabel',0:10:numRealizations); +set(gca,'xtick',0:.1:Tmax,'xtickLabel',0:.1:Tmax); xlabel(''); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +title('Simulated Neural Raster','Interpreter','none','FontName', 'Arial',... +'Fontsize',14,'FontWeight','bold'); +stimData = exp(b0 + u'*b1); +if(strcmp(simTypeSelect,'binomial')) +stimData = stimData./(1+stimData); +end +subplot(3,2,1); plot(time,u,'k','LineWidth',3); +xlabel('time [s]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('Stimulus','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +title('Within Trial Stimulus','Interpreter','none','FontName', 'Arial',... +'Fontsize',14,'FontWeight','bold'); +subplot(3,2,2); plot(1:length(b1),b1,'k','LineWidth',3); +xlabel('Trial [k]','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +ylabel('Stimulus Gain','Interpreter','none','FontName', 'Arial','Fontsize',... +12,'FontWeight','bold'); +title('Across Trial Stimulus Gain','Interpreter','none','FontName',... +'Arial','Fontsize',14,'FontWeight','bold'); +subplot(3,2,[5 6]); +imagesc(stimData'./delta); set(gca, 'YDir','normal'); +set(gca,'xtick',0:100:Tmax/delta,'xtickLabel',0:.1:Tmax); +set(gca,'ytick',0:10:numRealizations,'ytickLabel',0:10:numRealizations); +xlabel('time [s]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +ylabel('Trial [k]','Interpreter','none','FontName', 'Arial',... +'Fontsize',12,'FontWeight','bold'); +title('True Conditional Intensity Function','Interpreter',... +'none','FontName', 'Arial','Fontsize',14,'FontWeight','bold'); +axis tight; +stim = Covariate(time,sin(2*pi*f*time),'Stimulus','time','s','V',{'stim'}); +baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',... +{'constant'}); +windowTimes=[0:.001:.003]; +numBasis = 25; +spikeColl.resample(1/delta); % Enforce sampleRate +spikeColl.setMaxTime(Tmax); % Make all spikeTrains end at time Tmax +dN=spikeColl.dataToMatrix'; % Convert the spikeTrains into a matrix +dN(dN>1)=1; % One should sample finely enough so there is +basisWidth=(spikeColl.maxTime-spikeColl.minTime)/numBasis; +if(simTypeSelect==0) +fitType='binomial'; +else +fitType='poisson'; +end +if(strcmp(fitType,'binomial')) +Algorithm = 'BNLRCG'; % BNLRCG - faster Truncated, L-2 Regularized, +else +Algorithm = 'GLM'; % Standard Matlab GLM (Can be used for binomial or +end +[psthSig, ~, psthResult] =spikeColl.psthGLM(basisWidth,windowTimes,fitType); +gamma0=psthResult.getHistCoeffs';%+.1*randn(size(histCoeffs)); +gamma0(isnan(gamma0))=-5; % Depending on the amount of data the +x0=psthResult.getCoeffs; %The initial estimate for the SSGLM model +numVarEstIter=10; +Q0 = spikeColl.estimateVarianceAcrossTrials(numBasis,windowTimes,... +numVarEstIter,fitType); +A=eye(numBasis,numBasis); +delta = 1/spikeColl.sampleRate; +CompilingHelpFile=1; +if(~CompilingHelpFile) +Q0d=diag(Q0); +neuronName = psthResult.neuronNumber; +[xK,WK, WkuFinal,Qhat,gammahat,fitResults,stimulus,stimCIs,logll,... +QhatAll,gammahatAll,nIter]=DecodingAlgorithms.PPSS_EMFB(A,Q0d,x0,... +dN,fitType,delta,gamma0,windowTimes, numBasis,neuronName); +fR = fitResults.toStructure; +psthR = psthResult.toStructure; +end +installPath = which('nSTAT_Install'); +if isempty(installPath) +error('nSTATPaperExamples:MissingInstallPath', ... +'Could not locate nSTAT_Install.m on MATLAB path.'); +end +nstatRoot = fileparts(installPath); +if exist(nstatRoot,'dir') == 7 && ~strcmp(pwd,nstatRoot) +cd(nstatRoot); +end +addpath(nstatRoot,'-begin'); +load(fullfile(nstatRoot,'data','SSGLMExampleData.mat')); +fitResults = FitResult.fromStructure(fR); +psthResult = FitResult.fromStructure(psthR); +t=psthResult.mergeResults(fitResults); +t.lambda.setDataLabels({'\lambda_{PSTH}','\lambda_{SSGLM}'}); +scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +subplot(2,2,1); t.KSPlot; +subplot(2,2,2); t.plotResidual; +subplot(2,2,3); t.plotInvGausTrans; +subplot(2,2,4); t.plotSeqCorr; +S=FitResSummary(t); +dAIC=diff(S.AIC) +dBIC=diff(S.BIC) +dKS =diff(S.KSStats); +close all; +minTime=0; maxTime = Tmax; +stimData = stim.data*b1; +if(strcmp(fitType,'poisson')) +actStimEffect=exp(stimData + b0)./delta; +elseif(strcmp(fitType,'binomial')) +actStimEffect=exp(stimData + b0)./(1+exp(stimData + b0))./delta; +end +if(~isempty(numBasis)) +basisWidth = (maxTime-minTime)/numBasis; +sampleRate=1/delta; +unitPulseBasis=nstColl.generateUnitImpulseBasis(basisWidth,minTime,... +maxTime,sampleRate); +basisMat = unitPulseBasis.data; +end +if(strcmp(fitType,'poisson')) +estStimEffect=exp(basisMat*xK)./delta; +elseif(strcmp(fitType,'binomial')) +estStimEffect=exp(basisMat*xK)./(1+exp(basisMat*xK))./delta; +end +scrsz = get(0,'ScreenSize'); +h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.4 scrsz(4)*.8]); +subplot(3,1,[1 2 3]); +lighting gouraud +surf((1:length(b1))',stim.time,actStimEffect,'FaceAlpha',0.1,... +'EdgeAlpha',0.1,'AlphaData',0.1); +hx=xlabel('Trial [k]'); hy=ylabel('time [s]'); +hz=zlabel('Stimulus Effect [spikes/sec]'); hold all; +set([hx hy hz],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +surf((1:length(b1))',stim.time,estStimEffect(:,1:length(b1)),... +'FaceAlpha',0.5,'EdgeAlpha',0.1,'AlphaData',0.5); %xlabel('Trial [k]'); ylabel('time [s]'); zlabel('Stimulus Effect'); +set(gca,'YDir','reverse'); +set(gca,'ytick',0:.1:Tmax,'ytickLabel',0:.1:Tmax); +title('SSGLM Estimated vs. Actual Stimulus Effect','FontWeight','bold',... +'Fontsize',14,... +'FontName','Arial'); +close all; +h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.4 scrsz(4)*.8]); +subplot(3,1,1); +lighting gouraud +mesh((1:length(b1))',stim.time,actStimEffect); +hx=xlabel('Trial [k]'); hy=ylabel('time [s]'); +zlabel('Stimulus Effect [spikes/sec]'); hold all; +set([hx hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title('True Stimulus Effect','FontWeight','bold',... +'Fontsize',14,... +'FontName','Arial'); +set(gca,'xtick',[],'xtickLabel',[]); +set(gca,'ytick',[],'ytickLabel',[]); +CLIM = [min(min(stimData./delta)) max(max(stimData./delta))]; +view(gca,[90 -90]); +subplot(3,1,2); +lighting gouraud +mesh((1:length(b1))',stim.time,repmat(psthSig.data, [1 numRealizations])); +hx=xlabel('Trial [k]'); hy=ylabel('time [s]'); +hz=zlabel('Stimulus Effect [spikes/sec]'); hold all; +set([hx hy hz],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title('PSTH Estimated Stimulus Effect','FontWeight','bold',... +'Fontsize',14,... +'FontName','Arial'); +set(gca,'xtick',[],'xtickLabel',[]); +set(gca,'ytick',[],'ytickLabel',[]); +CLIM = [min(min(stimData./delta)) max(max(stimData./delta))]; +view(gca,[90 -90]); +subplot(3,1,3); +lighting gouraud +mesh((1:length(b1))',stim.time,estStimEffect); +xlabel('Trial [k]'); ylabel('time [s]'); +zlabel('Stimulus Effect [spikes/sec]'); hold all; +hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); hz=get(gca,'ZLabel'); +set([hx hy hz],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title('SSGLM Estimated Stimulus Effect','FontWeight','bold',... +'Fontsize',14,... +'FontName','Arial'); +set(gca,'xtick',[],'xtickLabel',[]); +set(gca,'ytick',[],'ytickLabel',[]); +set(gca, 'YDir','normal') +view(gca,[90 -90]); +echo off; +close all; +minTime=0; maxTime = Tmax; +if(~isempty(numBasis)) +basisWidth = (maxTime-minTime)/numBasis; +sampleRate=1/delta; +unitPulseBasis=nstColl.generateUnitImpulseBasis(basisWidth,... +minTime,maxTime,sampleRate); +basisMat = unitPulseBasis.data; +end +t0=0; tf=Tmax; +[spikeRateBinom, ProbMat,sigMat]=DecodingAlgorithms.computeSpikeRateCIs(xK,... +WkuFinal,dN,t0,tf,fitType,delta,gammahat,windowTimes); +lt=find(sigMat(1,:)==1,1,'first'); +display(['The learning trial (compared to the first trial) is trial #' ... +num2str(find(sigMat(1,:)==1,1,'first'))]); +scrsz = get(0,'ScreenSize'); +h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.8]); +subplot(2,3,1); +spikeRateBinom.setName(['(' num2str(Tmax) '-0)^-1*\Lambda(0,' ... +num2str(Tmax) ')']); +spikeRateBinom.plot([],{{' ''k'',''Linewidth'',4'}}); +v=axis; +plot(lt*[1;1],v(3:4),'r','Linewidth',2); +hx=xlabel('Trial [k]','Interpreter','none'); hold all; +hy=ylabel('Average Firing Rate [spikes/sec]','Interpreter','none'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title(['Learning Trial:' num2str(lt)],'FontWeight','bold',... +'Fontsize',12,... +'FontName','Arial'); +h=subplot(2,3,[2 3 5 6]); +K=size(dN,1); +colormap(flipud(gray)); +imagesc(ProbMat); hold on; +for k=1:K +for m=(k+1):K +if(sigMat(k,m)==1) +plot3(m,k,1,'r*'); hold on; +end +end +end +set(h,'XAxisLocation','top','YAxisLocation','right'); +hx=xlabel('Trial Number','Interpreter','none'); hold all; +hy=ylabel('Trial Number','Interpreter','none'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +subplot(2,3,4) +stim1 = Covariate(time, basisMat*stimulus(:,1),'Trial1','time','s',... +'spikes/sec'); +temp = ConfidenceInterval(time, basisMat*squeeze(stimCIs(:,1,:))); +stim1.setConfInterval(temp); +stimlt = Covariate(time, basisMat*stimulus(:,lt),'Trial1','time','s',... +'spikes/sec'); +temp = ConfidenceInterval(time, basisMat*squeeze(stimCIs(:,lt,:))); +temp.setColor('r'); +stimlt.setConfInterval(temp); +stimltm1 = Covariate(time, basisMat*stimulus(:,lt-1),'Trial1','time','s',... +'spikes/sec'); +temp = ConfidenceInterval(time, basisMat*squeeze(stimCIs(:,lt-1,:))); +temp.setColor('r'); +stimltm1.setConfInterval(temp); +h1=stim1.plot([],{{' ''k'',''Linewidth'',4'}}); hold all; +h2=stimlt.plot([],{{' ''r'',''Linewidth'',4'}}); +hx=xlabel('time [s]','Interpreter','none'); hold all; +hy=ylabel('Firing Rate [spikes/sec]','Interpreter','none'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title({'Learning Trial Vs. Baseline Trial';'with 95% CIs'},'FontWeight','bold',... +'Fontsize',12,... +'FontName','Arial'); +h_legend=legend([h1(1) h2(1)],'\lambda_{1}(t)',['\lambda_{' num2str(lt) '}(t)']); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.03 pos(2)+.01 pos(3:4)]); +close all; +load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat')); +exampleCell = [2 21 25 49]; +scrsz = get(0,'ScreenSize'); +h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.6 scrsz(4)*.9]); +for i=1:length(exampleCell) +subplot(2,2,i); +h1=plot(x,y,'b','Linewidth',.5); hold on; +h2=plot(neuron{exampleCell(i)}.xN,neuron{exampleCell(i)}.yN,'r.',... +'MarkerSize',7); +hx=xlabel('X Position'); hy=ylabel('Y Position'); +title(['Cell#' num2str(exampleCell(i))],'FontWeight','bold',... +'Fontsize',12,'FontName','Arial'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +set(gca,'xTick',-1:.5:1,'yTick',-1:.5:1); axis square; +if(i==4) +h_legend = legend([h1 h2],'Animal Path',... +'Location at time of spike'); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.09 pos(2)+.06 pos(3:4)]); +end +end +numAnimals=2; +CompilingHelpFile=1; +if(~CompilingHelpFile) +for n=1:numAnimals +clear x y neuron time nst tc tcc z; +load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat'])); +for i=1:length(neuron) +nst{i} = nspikeTrain(neuron{i}.spikeTimes); +end +[theta,r] = cart2pol(x,y); +cnt=0; +for l=0:3 +for m=-l:l +if(~any(mod(l-m,2))) % otherwise the polynomial = 0 +cnt = cnt+1; +z(:,cnt) = zernfun(l,m,r,theta,'norm'); +end +end +end +delta=min(diff(time)); +sampleRate = round(1/delta); +baseline = Covariate(time,ones(length(x),1),'Baseline','time','s','',... +{'mu'}); +zernike = Covariate(time,z,'Zernike','time','s','m',{'z1','z2','z3',... +'z4','z5','z6','z7','z8','z9','z10'}); +gaussian = Covariate(time,[x y x.^2 y.^2 x.*y],'Gaussian','time',... +'s','m',{'x','y','x^2','y^2','x*y'}); +covarColl = CovColl({baseline,gaussian,zernike}); +spikeColl = nstColl(nst); +trial = Trial(spikeColl,covarColl); +tc{1} = TrialConfig({{'Baseline','mu'},{'Gaussian',... +'x','y','x^2','y^2','x*y'}},sampleRate,[]); +tc{1}.setName('Gaussian'); +tc{2} = TrialConfig({{'Zernike' 'z1','z2','z3','z4','z5','z6',... +'z7','z8','z9','z10'}},sampleRate,[]); +tc{2}.setName('Zernike'); +tcc = ConfigColl(tc); +results =Analysis.RunAnalysisForAllNeurons(trial,tcc,0); +resStruct =FitResult.CellArrayToStructure(results); +filename = fullfile(dataDir,['PlaceCellAnimal' num2str(n) 'Results']); +save(filename,'resStruct'); +end +end +clear Summary; +numAnimals =2; +for n=1:numAnimals +resData = load(fullfile(dataDir,['PlaceCellAnimal' num2str(n) 'Results.mat'])); +results = FitResult.fromStructure(resData.resStruct); +Summary{n} = FitResSummary(results); +end +close all; +scrsz = get(0,'ScreenSize'); +h=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.6 scrsz(4)*.5]); +subplot(1,3,1); +maxLength = max([Summary{1}.numNeurons,Summary{2}.numNeurons]); +dKS = nan(maxLength, 2); +dKS(1:Summary{1}.numNeurons,1) = (Summary{1}.KSStats(:,1)-Summary{1}.KSStats(:,2)) ; +dKS(1:Summary{2}.numNeurons,2) = (Summary{2}.KSStats(:,1)-Summary{2}.KSStats(:,2)) ; +boxplot(dKS ,{'Animal 1', 'Animal 2'},'labelorientation','inline'); +title('\Delta KS Statistic','FontWeight','bold','FontSize',14,... +'FontName','Arial'); +subplot(1,3,2); +dAIC = nan(maxLength, 2); +dAIC(1:Summary{1}.numNeurons,1) = Summary{1}.getDiffAIC(1); +dAIC(1:Summary{2}.numNeurons,2) = Summary{2}.getDiffAIC(1); +boxplot(dAIC ,{'Animal 1', 'Animal 2'},'labelorientation','inline'); +title('\Delta AIC','FontWeight','bold','FontSize',14,'FontName','Arial'); +subplot(1,3,3); +dBIC = nan(maxLength, 2); +dBIC(1:Summary{1}.numNeurons,1) = Summary{1}.getDiffBIC(1); +dBIC(1:Summary{2}.numNeurons,2) = Summary{2}.getDiffBIC(1); +boxplot(dBIC ,{'Animal 1', 'Animal 2'},'labelorientation','inline'); %ylabel('\Delta BIC'); %xticklabel_rotate([],45,[],'Fontsize',6); +title('\Delta BIC','FontWeight','bold','FontSize',14,'FontName','Arial'); +close all; +[x_new,y_new]=meshgrid(-1:.01:1); %define new x and y +y_new = flipud(y_new); x_new = fliplr(x_new); +[theta_new,r_new] = cart2pol(x_new,y_new); +newData{1} =ones(size(x_new)); +newData{2} =x_new; newData{3} =y_new; +newData{4} =x_new.^2; newData{5} =y_new.^2; +newData{6} =x_new.*y_new; +idx = r_new<=1; +zpoly = cell(1,10); +cnt=0; +for l=0:3 +for m=-l:l +if(~any(mod(l-m,2))) +cnt = cnt+1; +temp = nan(size(x_new)); +temp(idx) = zernfun(l,m,r_new(idx),theta_new(idx),'norm'); +zpoly{cnt} = temp; +end +end +end +for n=1:numAnimals +clear lambdaGaussian lambdaZernike; +load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat'])); +resData = load(fullfile(dataDir,['PlaceCellAnimal' num2str(n) 'Results.mat'])); +results = FitResult.fromStructure(resData.resStruct); +for i=1:length(neuron) +lambdaGaussian{i} = results{i}.evalLambda(1,newData); +lambdaZernike{i} = results{i}.evalLambda(2,zpoly); +end +for i=1:length(neuron) +if(n==1) +h4=figure(4); +colormap('jet'); +if(i==1) +tb=annotation(h4,'textbox',... +[0.283261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Gaussian Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on','Fontsize',11,... +'FontName','Arial','FontWeight','bold','LineStyle',... +'none','HorizontalAlignment','center'); hold on; +end +subplot(7,7,i); +elseif(n==2) +h6=figure(6); +colormap('jet'); +if(i==1) +annotation(h6,'textbox',... +[0.283261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Gaussian Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on','Fontsize',11,... +'FontName','Arial','FontWeight','bold','LineStyle',... +'none','HorizontalAlignment','center'); hold on; +end +subplot(6,7,i); +end +pcolor(x_new,y_new,lambdaGaussian{i}), shading interp +axis square; set(gca,'xtick',[],'ytick',[]); +set(gca, 'Box' , 'off'); +if(n==1) +h5=figure(5); +colormap('jet'); +if(i==1) +annotation(h5,'textbox',... +[0.303261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Zernike Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on','Fontsize',11,... +'FontName','Arial','FontWeight','bold','LineStyle','none'); hold on; +end +subplot(7,7,i); +elseif(n==2) +h7=figure(7); +colormap('jet'); +if(i==1) +annotation(h7,'textbox',... +[0.303261904761904 0.928571428571418 ... +0.392857142857143 0.0595238095238095],... +'String',{['Zernike Place Fields - Animal#' ... +num2str(n)]},'FitBoxToText','on','Fontsize',11,... +'FontName','Arial','FontWeight','bold','LineStyle',... +'none','HorizontalAlignment','center'); hold on; +end +subplot(6,7,i); +end +pcolor(x_new,y_new,lambdaZernike{i}), shading interp +axis square; +set(gca,'xtick',[],'ytick',[]); +set(gca, 'Box' , 'off'); +end +end +clear lambdaGaussian lambdaZernike; +load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat')); +resData = load(fullfile(dataDir,'PlaceCellAnimal1Results.mat')); +results = FitResult.fromStructure(resData.resStruct); +for i=1:length(neuron) +lambdaGaussian{i} = results{i}.evalLambda(1,newData); +lambdaZernike{i} = results{i}.evalLambda(2,zpoly); +end +exampleCell = 25; +close all; +h9=figure(9); +h_mesh = mesh(x_new,y_new,lambdaGaussian{exampleCell},'AlphaData',0); +get(h_mesh,'AlphaData'); +set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','b'); +hold on; +h_mesh = mesh(x_new,y_new,lambdaZernike{exampleCell},'AlphaData',0); +get(h_mesh,'AlphaData'); +set(h_mesh,'FaceAlpha',0.2,'EdgeAlpha',0.2,'EdgeColor','g'); +plot(x,y,neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.'); +axis tight square; +xlabel('x position'); ylabel('y position'); +title(['Animal#1, Cell#' num2str(exampleCell)],'FontWeight','bold',... +'Fontsize',12,'FontName','Arial'); +hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +close all; clear all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +delta = 0.001; Tmax = 1; +time = 0:delta:Tmax; +numRealizations = 20; +f=2; b1=randn(numRealizations,1);b0=log(10*delta)+randn(numRealizations,1); +x = sin(2*pi*f*time); +clear nst; +for i=1:numRealizations +expData = exp(b1(i)*x+b0(i)); +lambdaData = expData./(1+expData); +if(i==1) +lambda = Covariate(time,lambdaData./delta, ... +'\Lambda(t)','time','s','spikes/sec',{'\lambda_{1}'},... +{{' ''b'', ''LineWidth'' ,2'}}); +else +tempLambda = Covariate(time,lambdaData./delta, ... +'\Lambda(t)','time','s','spikes/sec',{'\lambda_{1}'},... +{{' ''b'', ''LineWidth'' ,2'}}); +lambda = lambda.merge(tempLambda); +end +spikeColl = CIF.simulateCIFByThinningFromLambda(... +lambda.getSubSignal(i),1); +nst{i} = spikeColl.getNST(1); +end +spikeColl = nstColl(nst);scrsz = get(0,'ScreenSize'); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 ... +scrsz(3)*.6 scrsz(4)*.8]); +subplot(3,1,1); plot(time,x,'k'); +set(gca,'xtick',[],'xtickLabel',[]); ylabel('Stimulus'); +hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title('Driving Stimulus','FontWeight','bold',... +'FontSize',14,'FontName','Arial'); +subplot(3,1,2); lambda.plot([],{{' ''k'',''Linewidth'',1'}}); +legend off; +hy=ylabel('Firing Rate [spikes/sec]', 'Interpreter','none'); +hx=xlabel('','Interpreter','none'); +set([hx, hy],'FontName', 'Arial','FontSize',12,... +'FontWeight','bold'); +set(gca,'xtickLabel',[]); +title('Conditional Intensity Functions','FontWeight',... +'bold','FontSize',14,'FontName','Arial'); +subplot(3,1,3); spikeColl.plot; +set(gca,'ytick',0:10:numRealizations,'ytickLabel',... +0:10:numRealizations); +xlabel('time [s]','Interpreter','none'); +ylabel('Cell Number','Interpreter','none'); +hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title('Point Process Sample Paths','FontWeight',... +'bold','FontSize',14,'FontName','Arial'); +stim = Covariate(time,sin(2*pi*f*time),'Stimulus','time','s','V',{'stim'}); +baseline = Covariate(time,ones(length(time),1),'Baseline','time','s','',... +{'constant'}); +close all; +clear lambdaCIF; +spikeColl.resample(1/delta); +dN=spikeColl.dataToMatrix; +Q=std(stim.data(2:end)-stim.data(1:end-1)); +Px0=.1; A=1; +x0 = x(:,1); yT=x(:,end); +Pi0 = eps*eye(size(x0,1),size(x0,1)); +PiT = eps*eye(size(x0,1),size(x0,1)); +[x_p, W_p, x_u, W_u] = DecodingAlgorithms.PPDecodeFilterLinear(A, ... +Q, dN',b0,b1','binomial',delta); +h=figure('Position',[scrsz(3)*.1 scrsz(4)*.1 scrsz(3)*.8 scrsz(4)*.6]); +zVal=1.96; +ciLower = min(x_u(1:end)-zVal*sqrt(squeeze(W_u(1:end)))',... +x_u(1:end)+zVal*sqrt(squeeze(W_u(1:end))')); +ciUpper = max(x_u(1:end)-zVal*sqrt(squeeze(W_u(1:end)))',... +x_u(1:end)+zVal*sqrt(squeeze(W_u(1:end))')); +estimatedStimulus = Covariate(time,x_u(1:end),'\hat{x}(t)','time','s',''); +CI= ConfidenceInterval(time,[ciLower', ciUpper'],'\hat{x}(t)','time','s',''); +estimatedStimulus.setConfInterval(CI); +hEst = estimatedStimulus.plot([],{{' ''k'',''Linewidth'',4'}}); +hStim=stim.plot([],{{' ''b'',''Linewidth'',4'}}); +legend off; +h_legend=legend([hEst(1) hStim],'Decoded','Actual'); +set(h_legend,'Interpreter','none'); +set(h_legend,'FontSize',22); +title(['Decoded Stimulus +/- 95% CIs with ' num2str(numRealizations) ' cells'],... +'FontWeight','bold','Fontsize',22,'FontName','Arial'); +xlabel('time [s]','Interpreter','none'); +ylabel('Stimulus','Interpreter','none'); +hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); +set([hx, hy],'FontName', 'Arial','FontSize',22,'FontWeight','bold'); +close all; +clear all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +q=1e-4; +Q=diag([1e-12 1e-12 q q]); +delta = .001; % Time increment +r=1e-6; % in meters^2 +p=1e-6; % in meters^2/s^2 +PiT=diag([r r p p]); % Uncertainty in the target state +Pi0=PiT; +T=2; % Reach Duration +x0 = [0;0;0;0]; % Initial Position and velocities (states) +xT = [-.35;.2; 0;0];% Final Target +time=0:delta:T; % time vector +A=[1 0 delta 0 ; %State transition matrix +0 1 0 delta; +0 0 1 0 ; +0 0 0 1 ]; +x=zeros(4,length(time)); +R=chol(Q); +L=chol(PiT); +for k=1:length(time) +if(k==1) +x(:,k)=x0; +else +x(:,k)=A*x(:,k-1)+... +delta/(2)*(pi/T)^2*cos(pi*time(k)/T)*[0;0;... +xT(1)-x0(1);xT(2)-x0(2)]; %Reach to target model +end +end +xT =x(:,end); % The target generated by the model +yT=xT; % Assume we have observed the actual target position with uncertainty PiT +Q=diag(var(diff(x,[],2),[],2))*100; +scrsz = get(0,'ScreenSize'); +fig1=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 ... +scrsz(3)*.8 scrsz(4)*.8]); +subplot(4,2,[1 3]); +plot(100*x(1,:),100*x(2,:),'k','Linewidth',2); +xlabel('X Position [cm]'); ylabel('Y Position [cm]'); +hx=get(gca,'XLabel'); hy=get(gca,'YLabel'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +title('Reach Path','FontWeight','bold','Fontsize',14,'FontName','Arial'); +hold on; +axis([sort([100*x0(1)+5, 100*xT(1)-5]), sort([100*x0(2)-5, 100*xT(2)+5])]); +h1=plot(100*x(1,1),100*x(2,1),'bo','MarkerSize',14); +h2=plot(100*x(1,end),100*x(2,end),'ro','MarkerSize',14); +legend([h1 h2],'Start','Finish','Location','NorthEast'); +subplot(4,2,5); h1=plot(time,100*x(1,:),'k','Linewidth',2); hold on; +h2=plot(time,100*x(2,:),'k-.','Linewidth',2); +h_legend=legend([h1,h2],'x','y','Location','NorthEast'); +set(h_legend,'FontSize',14) +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.06 pos(2)+.01 pos(3:4)]); +hx=xlabel('time [s]'); hy=ylabel('Position [cm]'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +subplot(4,2,7); +h1=plot(time,100*x(3,:),'k','Linewidth',2); hold on; +h2=plot(time,100*x(4,:),'k-.','Linewidth',2); +h_legend=legend([h1 h2],'v_x','v_y','Location','NorthEast'); +xlabel('time [s]'); +set(h_legend,'FontSize',14); +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)+.06 pos(2)+.01 pos(3:4)]); +hx=xlabel('time [s]'); hy=ylabel('Velocity [cm/s]'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +gamma=0; +windowTimes=[0, 0.001]; +numCells = 20; +bCoeffs=10*(rand(numCells,2)-.5); % b_i = [b_x_i b_y_i] ~ U(-5, 5); +phiMax = atan2(bCoeffs(:,2),bCoeffs(:,1)); % Maximal firing direction of cell +phiMaxNorm = (phiMax+pi)./(2*pi); +meanMu = log(10*delta); % baseline firing rate -10Hz +MuCoeffs = meanMu+randn(numCells,1); % mu_i ~ G(meanMu,1) +dataMat = [ones(length(time),1) x(3,:)' x(4,:)']; % design matrix: X ( +coeffs = [MuCoeffs bCoeffs]; % coefficient vector: beta +fitType='binomial'; +clear nst; +for i=1:numCells +tempData = exp(dataMat*coeffs(i,:)'); +if(strcmp(fitType,'poisson')) +lambdaData = tempData; +else +lambdaData = tempData./(1+tempData); % Conditional Intensity Function for ith cell +end +lambda{i}=Covariate(time,lambdaData./delta, ... +'\Lambda(t)','time','s','spikes/sec',... +{strcat('\lambda_{',num2str(i),'}')},{{' ''b'' '}}); +lambda{i}=lambda{i}.resample(1/delta); +lambdaCIF{i} = CIF([MuCoeffs(i) 0 0 bCoeffs(i,:)],... +{'1','x','y','vx','vy'},{'x','y','vx','vy'},fitType); +tempSpikeColl{i} = CIF.simulateCIFByThinningFromLambda(lambda{i},1); nst{i} = tempSpikeColl{i}.getNST(1); % grab the realization +nst{i}.setName(num2str(i)); % give each cell a unique name +subplot(4,2,[6 8]); +h2=lambda{i}.plot([],{{' ''k'', ''LineWidth'' ,.5'}}); +legend off; hold all; % Plot the CIF +end +title('Neural Conditional Intensity Functions','FontWeight',... +'bold','Fontsize',14,'FontName','Arial'); +hx=xlabel('time [s]','Interpreter','none'); +hy=ylabel('Firing Rate [spikes/sec]','Interpreter','none'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +spikeColl = nstColl(nst); % Create a neural spike train collection +subplot(4,2,[2,4]); spikeColl.plot; +set(gca,'xtick',[],'xtickLabel',[]); +title('Neural Raster','FontWeight','bold','Fontsize',14,... +'FontName','Arial'); +hx=xlabel('time [s]','Interpreter','none'); +hy=ylabel('Cell Number','Interpreter','none'); +set([hx, hy],'FontName', 'Arial','FontSize',12,'FontWeight','bold'); +close all; +numExamples=20; +scrsz = get(0,'ScreenSize'); +fig1=figure('OuterPosition',[scrsz(3)*.1 scrsz(4)*.1 ... +scrsz(3)*.6 scrsz(4)*.9]); +for k=1:numExamples +bCoeffs=10*(rand(numCells,2)-.5); % b_i = [b_x_i b_y_i] ~ U(-5, 5); +phiMax = atan2(bCoeffs(:,2),bCoeffs(:,1)); % Maximal firing direction of cell +phiMaxNorm = (phiMax+pi)./(2*pi); +meanMu = log(10*delta); % baseline firing rate +MuCoeffs = meanMu+randn(numCells,1); % mu_i ~ G(meanMu,1) +dataMat = [ones(length(time),1) x(3,:)' x(4,:)']; % design matrix: X ( +coeffs = [MuCoeffs bCoeffs]; % coefficient vector: beta +fitType='binomial'; +clear nst lambda; +for i=1:numCells +tempData = exp(dataMat*coeffs(i,:)'); +if(strcmp(fitType,'poisson')) +lambdaData = tempData; +else +lambdaData = tempData./(1+tempData); +end +lambda{i}=Covariate(time,lambdaData./delta, ... +'\Lambda(t)','time','s','spikes/sec',... +{strcat('\lambda_{',num2str(i),'}')},{{' ''b'' '}}); +lambda{i}=lambda{i}.resample(1/delta); +tempSpikeColl{i} = CIF.simulateCIFByThinningFromLambda(lambda{i},1); +nst{i} = tempSpikeColl{i}.getNST(1); % grab the realization +nst{i}.setName(num2str(i)); % give each cell a unique name +end +spikeColl = nstColl(nst); % Create a neural spike train collection +dN=spikeColl.dataToMatrix'; +dN(dN>1)=1; % more than one spike per bin will be treated as one spike. In +[C,N] = size(dN); % N time samples, C cells +beta=[zeros(2,numCells); bCoeffs']; +[x_p, W_p, x_u, W_u,x_uT,W_uT,x_pT,W_pT] = ... +DecodingAlgorithms.PPDecodeFilterLinear(A, Q, dN,... +MuCoeffs,beta,fitType,delta,gamma,windowTimes,x0, Pi0, yT,PiT,0); +[x_pf, W_pf, x_uf, W_uf] = ... +DecodingAlgorithms.PPDecodeFilterLinear(A, Q, dN,... +MuCoeffs,beta,fitType,delta,gamma,windowTimes,x0); +if(k==numExamples) +subplot(4,2,1:4);h1=plot(100*x(1,:),100*x(2,:),'k','LineWidth',3); +hold on; +axis([sort([100*x0(1)+5, 100*xT(1)-5]), ... +sort([100*x0(2)-5, 100*xT(2)+5])]); +title('Estimated vs. Actual Reach Paths',... +'FontWeight','bold','Fontsize',12,'FontName','Arial'); +end +subplot(4,2,1:4);h2=plot(100*x_u(1,:)',100*x_u(2,:)','b'); hold all; +subplot(4,2,1:4);h3=plot(100*x_uf(1,:)',100*x_uf(2,:)','g'); +hx=xlabel('x [cm]'); hy=ylabel('y [cm]'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +h1=plot(100*x0(1),100*x0(2),'bo','MarkerSize',10); hold on; +h2=plot(100*xT(1),100*xT(2),'ro','MarkerSize',10); +legend([h1 h2],'Start','Finish','Location','NorthEast'); +subplot(4,2,5); +h1=plot(time,100*x(1,:),'k','LineWidth',3); hold on; +h2=plot(time,100*x_u(1,:)','b'); +h3=plot(time,100*x_uf(1,:)','g'); +hy=ylabel('x(t) [cm]'); hx=xlabel('time [s]'); +set(gca,'xtick',[],'xtickLabel',[]); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('X Position','FontWeight','bold','Fontsize',12,'FontName','Arial'); +subplot(4,2,6); +h1=plot(time,100*x(2,:),'k','LineWidth',3); hold on; +h2=plot(time,100*x_u(2,:)','b'); +h3=plot(time,100*x_uf(2,:)','g'); +h_legend=legend([h1(1) h2(1) h3(1)],'Actual','PPAF+Goal',... +'PPAF','Location','SouthEast'); +hy=ylabel('y(t) [cm]'); hx=xlabel('time [s]'); +set(gca,'xtick',[],'xtickLabel',[]); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('Y Position','FontWeight','bold','Fontsize',12,'FontName','Arial'); +set(h_legend,'FontSize',10) +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)-.63 pos(2)+.23 pos(3:4)]); +subplot(4,2,7); +h1=plot(time,100*x(3,:),'k','LineWidth',3); hold on; +h2=plot(time,100*x_u(3,:)','b'); +h3=plot(time,100*x_uf(3,:)','g'); +hy=ylabel('v_{x}(t) [cm/s]'); hx=xlabel('time [s]'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('X Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial'); +subplot(4,2,8); +h1=plot(time,100*x(4,:),'k','LineWidth',3); hold on; +h2=plot(time,100*x_u(4,:)','b'); +h3=plot(time,100*x_uf(4,:)','g'); +hy=ylabel('v_{y}(t) [cm/s]'); hx=xlabel('time [s]'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('Y Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial'); +end +clear all; +[dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs(); +close all; +delta=0.001; +Tmax=2; +time=0:delta:Tmax; +A{2} = [1 0 delta 0 delta^2/2 0; +0 1 0 delta 0 delta^2/2; +0 0 1 0 delta 0; +0 0 0 1 0 delta; +0 0 0 0 1 0; +0 0 0 0 0 1]; +A{1} = [1 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]; +A{1} = [1 0; +0 1]; +Px0{2} =1e-6*eye(6,6); +Px0{1} =1e-6*eye(2,2); +minCovVal = 1e-12; +covVal = 1e-3; +Q{2}=[minCovVal 0 0 0 0 0; +0 minCovVal 0 0 0 0; +0 0 minCovVal 0 0 0; +0 0 0 minCovVal 0 0; +0 0 0 0 covVal 0; +0 0 0 0 0 covVal]; +Q{1}=minCovVal*eye(2,2); +mstate = zeros(1,length(time)); +ind{1}=1:2; +ind{2}=1:6; +X=zeros(max([size(A{1},1),size(A{2},1)]),length(time)); +p_ij = [.998 .002; +.001 .999]; +for i = 1:length(time) +if(i==1) +mstate(i) = 1; +else +if(rand(1,1)1)=1; %Avoid more than 1 spike per bin. +Mu0=.5*ones(size(p_ij,1),1); +clear x0 yT clear Pi0 PiT; +x0{1} = X(ind{1},1); +yT{1} = X(ind{1},end); +Pi0 = Px0; +PiT{1} = 1e-9*eye(size(x0{1},1),size(x0{1},1)); +x0{2} = X(ind{2},1); +yT{2} = X(ind{2},end); +PiT{2} = 1e-9*eye(size(x0{2},1),size(x0{2},1)); +[S_est, X_est, W_est, MU_est, X_s, W_s,pNGivenS]=... +DecodingAlgorithms.PPHybridFilterLinear(A, Q, p_ij,Mu0, dN',... +coeffs(:,1),coeffs(:,2:end)',fitType,delta,[],[],x0,Pi0, yT,PiT); +[S_estNT, X_estNT, W_estNT, MU_estNT, X_sNT, W_sNT,pNGivenSNT]=... +DecodingAlgorithms.PPHybridFilterLinear(A, Q, p_ij,Mu0, dN',... +coeffs(:,1),coeffs(:,2:end)',fitType,delta,[],[],x0,Pi0); +X_estAll(:,:,n) = X_est; +X_estNTAll(:,:,n) = X_estNT; +S_estAll(n,:)=S_est; +S_estNTAll(n,:)=S_estNT; +MU_estAll(:,:,n)=MU_est; +MU_estNTAll(:,:,n) = MU_estNT; +subplot(4,3,[1 4]); +plot(time,mstate,'k','LineWidth',3); hold all; +plot(time,S_est,'b-.','Linewidth',.5); +plot(time,S_estNT,'g-.','Linewidth',.5); axis tight; v=axis; +axis([v(1) v(2) 0.5 2.5]); +subplot(4,3,[7 10]); +plot(time,MU_est(2,:),'b-.','Linewidth',.5); hold on; +plot(time,MU_estNT(2,:),'g-.','Linewidth',.5); hold on; +axis([min(time) max(time) 0 1.1]); +subplot(4,3,[2 3 5 6]); +h1=plot(100*X(1,:)',100*X(2,:)','k'); hold all; +h2=plot(100*X_est(1,:)',100*X_est(2,:)','b-.'); hold all; +h3=plot(100*X_estNT(1,:)',100*X_estNT(2,:)','g-.'); +subplot(4,3,8); +h1=plot(time,100*X(1,:),'k','LineWidth',3); hold on; +h2=plot(time,100*X_est(1,:)','b-.'); +h3=plot(time,100*X_estNT(1,:)','g-.'); +subplot(4,3,9); +h1=plot(time,100*X(2,:),'k','LineWidth',3); hold on; +h2=plot(time,100*X_est(2,:)','b-.'); +h3=plot(time,100*X_estNT(2,:)','g-.'); +subplot(4,3,11); +h1=plot(time,100*X(3,:),'k','LineWidth',3); hold on; +h2=plot(time,100*X_est(3,:)','b-.'); +h3=plot(time,100*X_estNT(3,:)','g-.'); +subplot(4,3,12); +h1=plot(time,100*X(4,:),'k','LineWidth',3); hold on; +h2=plot(time,100*X_est(4,:)','b-.'); +h3=plot(time,100*X_estNT(4,:)','g-.'); +end +subplot(4,3,[1 4]); +hold all; +plot(time,mstate,'k','LineWidth',3); +plot(time,mean(S_estAll),'b','LineWidth',3); +plot(time,mean(S_estNTAll),'g','LineWidth',3); +set(gca,'xtick',[],'YTick',[1 2.1],'YTickLabel',{'N','M'}); +hy=ylabel('state'); hx=xlabel('time [s]'); +set([hy hx],'FontName', 'Arial','FontSize',10,'FontWeight','bold',... +'Interpreter','none'); +title('Estimated vs. Actual State','FontWeight','bold','Fontsize',... +12,'FontName','Arial'); +subplot(4,3,[7 10]); +plot(time, mean(squeeze(MU_estAll(2,:,:)),2),'b','LineWidth',3); +hold on; +plot(time,mean(squeeze(MU_estNTAll(2,:,:)),2),'g','LineWidth',3); +hold on; +axis([min(time) max(time) 0 1.1]); +hx=xlabel('time [s]'); hy=ylabel('P(s(t)=M | data)'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('Probability of State','FontWeight','bold','Fontsize',12,... +'FontName','Arial'); +subplot(4,3,[2 3 5 6]); +h1=plot(100*X(1,:)',100*X(2,:)','k'); hold all; +mXestAll=mean(100*X_estAll,3); +mXestNTAll=mean(100*X_estNTAll,3); +plot(mXestAll(1,:),mXestAll(2,:),'b','Linewidth',3); +plot(mXestNTAll(1,:),mXestNTAll(2,:),'g','Linewidth',3); +hx=xlabel('x [cm]'); hy=ylabel('y [cm]'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +h1=plot(100*X(1,1),100*X(2,1),'bo','MarkerSize',14); hold on; +h2=plot(100*X(1,end),100*X(2,end),'ro','MarkerSize',14); +legend([h1 h2],'Start','Finish','Location','NorthEast'); +title('Estimated vs. Actual Reach Path','FontWeight','bold',... +'Fontsize',12,'FontName','Arial'); +subplot(4,3,8); +h1=plot(time,100*X(1,:),'k','LineWidth',3); hold on; +h2=plot(time,mXestAll(1,:),'b','LineWidth',3); hold on; +h3=plot(time,mXestNTAll(1,:),'g','LineWidth',3); hold on; +hy=ylabel('x(t) [cm]'); hx=xlabel('time [s]'); +set(gca,'xtick',[],'xtickLabel',[]); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('X Position','FontWeight','bold','Fontsize',12,'FontName','Arial'); +subplot(4,3,9); +h1=plot(time,100*X(2,:),'k','LineWidth',3); hold on; +h2=plot(time,mXestAll(2,:),'b','LineWidth',3); hold on; +h3=plot(time,mXestNTAll(2,:),'g','LineWidth',3); hold on; +h_legend=legend([h1(1) h2(1) h3(1)],'Actual','PPAF+Goal',... +'PPAF','Location','SouthEast'); +hy=ylabel('y(t) [cm]'); hx=xlabel('time [s]'); +set(gca,'xtick',[],'xtickLabel',[]); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('Y Position','FontWeight','bold','Fontsize',12,'FontName','Arial'); +set(h_legend,'FontSize',10) +pos = get(h_legend,'position'); +set(h_legend, 'position',[pos(1)-.40 pos(2)+.51 pos(3:4)]); +subplot(4,3,11); +h1=plot(time,100*X(3,:),'k','LineWidth',3); hold on; +h2=plot(time,mXestAll(3,:),'b','LineWidth',3); hold on; +h3=plot(time,mXestNTAll(3,:),'g','LineWidth',3); hold on; +hy=ylabel('v_{x}(t) [cm/s]'); hx=xlabel('time [s]'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('X Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial'); +subplot(4,3,12); +h1=plot(time,100*X(4,:),'k','LineWidth',3); hold on; +h2=plot(time,mXestAll(4,:),'b','LineWidth',3); hold on; +h3=plot(time,mXestNTAll(4,:),'g','LineWidth',3); hold on; +hy=ylabel('v_{y}(t) [cm/s]'); hx=xlabel('time [s]'); +set([hx, hy],'FontName', 'Arial','FontSize',10,'FontWeight','bold'); +title('Y Velocity','FontWeight','bold','Fontsize',12,'FontName','Arial'); +parity = struct(); +if exist('numCells','var') +parity.num_cells = numCells; +end +if exist('numRealizations','var') +parity.num_realizations = numRealizations; +end +function [dataDir,mEPSCDir,explicitStimulusDir,psthDir,placeCellDataDir] = ... +getPaperDataDirs() +candidateRoots = {}; +scriptPath = mfilename('fullpath'); +if ~isempty(scriptPath) +candidateRoots = appendCandidateRoot(candidateRoots, fileparts(fileparts(scriptPath))); +end +paperPath = which('nSTATPaperExamples'); +if ~isempty(paperPath) +candidateRoots = appendCandidateRoot(candidateRoots, fileparts(fileparts(paperPath))); +end +installPath = which('nSTAT_Install'); +if ~isempty(installPath) +candidateRoots = appendCandidateRoot(candidateRoots, fileparts(installPath)); +end +try +activeFile = matlab.desktop.editor.getActiveFilename; +catch +activeFile = ''; +end +if ~isempty(activeFile) +candidateRoots = appendCandidateRoot(candidateRoots, fileparts(fileparts(activeFile))); +end +candidateRoots = appendCandidateRoot(candidateRoots, pwd); +nSTATDir = ''; +for iRoot = 1:numel(candidateRoots) +candidateDataDir = fullfile(candidateRoots{iRoot}, 'data'); +if exist(candidateDataDir, 'dir') == 7 +nSTATDir = candidateRoots{iRoot}; +break; +end +end +if isempty(nSTATDir) +error('nSTATPaperExamples:MissingInstallPath', ... +['Could not resolve the nSTAT root path. Checked roots derived from ', ... +'mfilename, which(''nSTATPaperExamples''), which(''nSTAT_Install''), ', ... +'the active editor file, and pwd.']); +end +dataDir = fullfile(nSTATDir,'data'); +mEPSCDir = fullfile(dataDir,'mEPSCs'); +explicitStimulusDir = fullfile(dataDir,'Explicit Stimulus'); +psthDir = fullfile(dataDir,'PSTH'); +placeCellDataDir = fullfile(dataDir,'Place Cells'); +if exist(dataDir,'dir') ~= 7 +error('nSTATPaperExamples:MissingDataDir', ... +'Could not find local nSTAT data folder at %s', dataDir); +end +end +function roots = appendCandidateRoot(roots, startDir) +if isempty(startDir) +return; +end +thisDir = startDir; +while true +if ~any(strcmp(roots, thisDir)) +roots{end+1} = thisDir; %#ok +end +parentDir = fileparts(thisDir); +if strcmp(parentDir, thisDir) +break; +end +thisDir = parentDir; +end +end diff --git a/parity/line_port_snapshots/publish_all_helpfiles.txt b/parity/line_port_snapshots/publish_all_helpfiles.txt new file mode 100644 index 00000000..7301ff5a --- /dev/null +++ b/parity/line_port_snapshots/publish_all_helpfiles.txt @@ -0,0 +1,126 @@ +function publish_all_helpfiles(varargin) +opts = parseOptions(varargin{:}); +helpDir = fileparts(mfilename('fullpath')); +rootDir = fileparts(helpDir); +stagingDir = tempname; +outputDir = tempname; +mkdir(stagingDir); +mkdir(outputDir); +cleanupObj = onCleanup(@()cleanupTempDirs(stagingDir, outputDir)); +startDir = pwd; +restoreDir = onCleanup(@()cd(startDir)); %#ok +copyfile(fullfile(helpDir, '*'), stagingDir); +removeStagedArtifacts(stagingDir); +restoredefaultpath; +addpath(rootDir, '-begin'); +nSTAT_Install('RebuildDocSearch', false, 'CleanUserPathPrefs', false); +addpath(stagingDir, '-begin'); +cd(stagingDir); +publishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', opts.EvalCode); +referencePublishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', false); +failures = {}; +stageFiles = dir(fullfile(stagingDir, '*.m')); +for iFile = 1:numel(stageFiles) +[~, baseName] = fileparts(stageFiles(iFile).name); +if strcmpi(baseName, 'publish_all_helpfiles') +continue; +end +try +publish(baseName, publishOptions); +fprintf('Published help topic: %s\n', stageFiles(iFile).name); +catch ME +failures{end+1} = sprintf('%s :: %s', stageFiles(iFile).name, ME.message); %#ok +end +end +rootReferenceFiles = {'Analysis.m', 'SignalObj.m', 'FitResult.m'}; +for iFile = 1:numel(rootReferenceFiles) +sourceFile = fullfile(rootDir, rootReferenceFiles{iFile}); +try +publish(sourceFile, referencePublishOptions); +fprintf('Published class reference: %s\n', rootReferenceFiles{iFile}); +catch ME +failures{end+1} = sprintf('%s :: %s', rootReferenceFiles{iFile}, ME.message); %#ok +end +end +if ~isempty(failures) +fprintf(2, 'Publish failures (%d):\n', numel(failures)); +for i = 1:numel(failures) +fprintf(2, ' - %s\n', failures{i}); +end +error('nSTAT:PublishAllFailures', 'One or more help pages failed to publish.'); +end +copyfile(fullfile(outputDir, '*'), helpDir, 'f'); +builddocsearchdb(helpDir); +rehash toolboxcache; +validateHelpTargets(helpDir); +validateHtmlGeneratorMetadata(helpDir, opts.ExpectedGenerator); +fprintf('nSTAT help publication completed successfully.\n'); +clear cleanupObj; +end +function opts = parseOptions(varargin) +parser = inputParser; +parser.FunctionName = 'publish_all_helpfiles'; +addParameter(parser, 'EvalCode', true, @(x)islogical(x) || isnumeric(x)); +addParameter(parser, 'ExpectedGenerator', 'MATLAB 25.2', @(x)ischar(x) || isstring(x)); +parse(parser, varargin{:}); +opts.EvalCode = logical(parser.Results.EvalCode); +opts.ExpectedGenerator = char(parser.Results.ExpectedGenerator); +end +function removeStagedArtifacts(stagingDir) +removePattern(stagingDir, '*.mlx'); +removePattern(stagingDir, '*.asv'); +removePattern(stagingDir, '*.bak'); +removePattern(stagingDir, 'temp.m'); +removePattern(stagingDir, 'publish_all_helpfiles.m'); +end +function removePattern(stagingDir, pattern) +files = dir(fullfile(stagingDir, pattern)); +for i = 1:numel(files) +delete(fullfile(stagingDir, files(i).name)); +end +end +function validateHelpTargets(helpDir) +helptocPath = fullfile(helpDir, 'helptoc.xml'); +if ~isfile(helptocPath) +error('nSTAT:MissingHelptoc', 'Missing helptoc.xml at %s', helptocPath); +end +raw = fileread(helptocPath); +matches = regexp(raw, 'target="([^"]+)"', 'tokens'); +for i = 1:numel(matches) +target = matches{i}{1}; +if startsWith(target, 'http://') || startsWith(target, 'https://') +continue; +end +fullTarget = fullfile(helpDir, target); +if ~isfile(fullTarget) +error('nSTAT:MissingHelpTarget', ... +'helptoc target is missing after publish: %s', fullTarget); +end +end +end +function validateHtmlGeneratorMetadata(helpDir, expectedGenerator) +htmlFiles = dir(fullfile(helpDir, '*.html')); +for i = 1:numel(htmlFiles) +htmlPath = fullfile(helpDir, htmlFiles(i).name); +raw = fileread(htmlPath); +if isempty(regexp(raw, ['lB^X%$`(=iQp!?MN?K@9X|p6sAw;&4 zr3i%*vZf8a_wQeLoik^S!;FXLzOL)DUDjA`T5V}XHPqo#*H~`ScK6!rMqR&m=YDrz zfA59VeZB!+L0Z%${%$+>ySY+*eHKzT1-en!?DVHn^{IT5g?yuh27Ia>U(aa9|Knq( z%^C^vGUv53W(bq#x#aP`=l^}oA&>ukUM4zY#!T}3zlS7wxCQNV+eNNrhd;TZ9sBl? zFVCQo*RLBsw)@(DiWvH{w=&V4ir|jFax>ps;@R@Vf`DbRcy_T?%WF3iMoF2yrSe8N zBQAQO)Ib4k>f3i_-qeLeW~Q{MM3We_j$x^{z_I4yI%Fl}wZ7To%GqFJAR|K||#u5r1i04Nz`NcZF5+ za4LAg&tgX!_NO?f7ayb{dw)yZE)i{<+h)0aTZ;mckAAE3I%I~j^O@-?h4MI^UiM$9 zfgD_KO@&=q%fy0}Z^F*};p5;zL2$ILKD4Gk9Sbei0`vMDV^JOpk84|VKiV*1qEWt0 z;pbAzbWb_q!f&;-X6L zsNDHQ1~~N+D%F9CSi1egU~{JmJc~?sW?NWd*RdM;vS4NCK3;#h)rf~_pD4k91IC!& zd9vQ_4;}3`$G=)~7*K9rn)7y&i$jYJo!q6(#f+Kidv?syz@F#w*9Jy7C=W{ebx~Rd z#caulZ%0kA&w1ONu@@TXn-`_p@l+jdE53g-pz7mg+3mySJCw1vakkjQ*A�h3lRP zXCcJmV&=Lf3>0Mu9UHvI#2n3*Vlgd$wBC@)qxd(+ORXp zSZ$%Fgmo{Ev`p8@z-Mml9Or-P7@D41bk&&;51sIUZB7Z3(ezRN?74@HSPrzJP3K<4OfWwF^@bVo_|9f=Y^?NRt@^-ux2Wqt>?g>xZy zbSgBK%E68d#R1SCbV%#m=euii!JmF!cPV-R>V0+b z?;GQG(LOr<3-jMt>Jd3W)-cCQ@r%cx_i{Hs?YS|at$VuJ0r91 zq-!Dg*`t_0a}AK%cs;3LjEj;DzCr?hChqn>J#We4Dh9A?e^|Kb^X3^^-& z^tRG85wbXUYm6EniK6c#OQf~HE&1ZQJC%jJMPYvwPa0sEv4`nh3k7^Fxo9#=SQ#~w(X%y1rEqno zpJj)y9$F$bd-h3lVY7bg?CA%JSf1|k6HHkIUEdC?jPmLF0e>s-_oaiP%X*Xq66h@Q-P{BB}CR!YC$EcrnR1#4yo z9TO$>eRKt7osTiX-`$NC&Bp`j-I_4WFq+DS6#p8S**Kmis;~#4P15>G&irLIdn7B1*X!P_OfKSfq+k zV|vwsN);Tw_2-!Qc2%&R%$TA2pDtc?`ex@VvGKPlI!H-Co-h3weRo$IC1y8gc-6^4 z{OeShmF%tL0it~<-D#ul0kulQP{;Rf1z1k4@CJ(wbCMkG5vF$~>jv8E@N*Eps zHDP8JeI-yq8>3P}$nxAl+eE-^BCq-990#ce0`n{z2!CkIS5#I5YRjL z#ghharQD#-DF$v0Uahc>)r6bu>4KRBTwKul(XdFs0Hyq9b%h-phs7ArKK07L(?gKq zG04QV*2s}teKaHn@Q(~`*G57??!$I>HOwo$)p>ItADN#zY*%Da@$O|V`%|qf9^Foj z4(MYc?qoue)Mq+&uQ*zM{xKgVsf8M~(z5XDmUwcZn+oc~iW!`(bo`N%(4?trV(tF; z)gfEtVC62;HcyU*UwT(B$`mpYGxzM_ua+9fmb6{6@qqx3PL^IOK0(Jap#aIFmjy^S ztB;7YV&Iu<$k@O!O}w%9-&EkJjlF9Y-sjwx!^eaaEze^d)C}q=neAkw=kYBm&nsM9 z*`l&zrjr1dY~IV%wJG4(0&w+!aDRaXPF5+`v%@2&yL7TNs{dIC( z#~Iq!?Vj+kORKo5Ta1k>Umrd{y+{H3I}&pwA8|1E(B0Ba6a~06y+0?BM#ZJa;(M

0X3%mQ~_N>+dA3cj) z#hQ!%rX>z<@Y09qNu|?Dj+#gnT-vYX$-zbC(m9zjEZTZz^OMM<2x7qj7|uLjn|8GU5ub20JPi2EPK7>1(G zy#WK3q@XU{Rj3oW~j} z+C2IzYM+qn3AlJc>wqGpc`S*-VNH~J+diqeO+|0vC%JSLEzH=M_IL^D&y9zQZ)XW; z2vIaBig-uIx3C`bhd}~tKi%&5rke@Bkn|JgOVm)$e-w~mr-FgF_3b_cf3;9qEA`X_ zXr;VxJ26ua4%T05!(w$IUwGa2(JwZl+I3W%Zm8pSa(?U*T^16)B>g_2se!Z!p&xDg zRPc5DMBZ&uFTbwm+P>ja@W|T3npdI^s}2p3Tk{QJcjV}wybf}Xox?UKc5`s;bjs0> zg?!Mxo%XrQak2H?%{l4IHE?mcSZMW2CAf7*J9qt2M5j)Cnaw2%o+b{TQYQHQ;F0Z; z=gjocR=P?n*TN8Y;=*SeJmf;4uhF|LNe;JX$MZ9)2p@>LVR|-;i#;`Oj*i;Pp*vW= zitTBDZnq7+8#nOqLvw1~;rFr-@QgAaZdO4jUw`|zOj6f_!jE=2XUg|qfE2aVz^gg*SJ;A`#%%z*%Th(B?y=U3PPX?~~b&QUF(L&eI zwYx@t$$jh8SaOUj21@ad<8NaLUa8eIE@{_*8yo!gE?ND7gUf-U+cw=IbFlZe|5X=KA9BLn>6=uT z)7?C>`qi-ecl^nr73P@LB5~2%&=UL7>^nCPDj_Cn{(_b+4$O4U#_WmbK_xwj{+93w zzt?vDW14gf=^8nB7Rckjz71aL7n$fg(pklgDlMjzQ`6G_7j5KZ_T@?>UqcsW}|JS#RlC4(JP^?$n@H&-> zinP_;KekCDT`s@8o2mxuva<)7R%&>&=&@Bnyb40MORW>C<>SDN;=*{A5{}2MFRPAc zLGdju+ir^~z7$9toT0@(_YGA<) z)4e%l?r*5bqr3gng-~?t1D8x4G-^9aE4Hhm!1_{4#B&BVHkTc?;Zd+aA&wVht%3Z< zPe;@!0&KXtwZZ9<1(cpeE%;`ki412?5f@S~t1KETGB2y*9&Dx{&w&2ceVxTl%HtC--lme7zcoH5c^lJa?E2KXz9nw$J^HN3qfhleS!SyFP z`{oUb=^_dutISC~mYkav$B)p!gyzYe+sn+L{%g6)D{VTGwL9K3uh!g9yuSN zf%EZdAD3@jfE7;9a>slPF#4!pE8LlfufL-8&7bpO@|C?If3qUKFPmGvKZytNwD6hk zX)NsPe>rrpoR78wbDynbj=WAl)&V0~3~9Rx84{fMWhmm>MuKac6~4!~N9*9G>X7up z&3cg6xIO;2l!_LuLr*Tb(2*LkcVjM_iHXr;-cBM6*z!7ZJ*n!b_^Ke-5 zBgK65TKqM$n`Gnr`b`~@!F>2c%qx7mnSm76%d0mkm5~?u(kjE3hSDQnUiul)aMxty z#r#f9w78f`yt&CiQPHyWy}Q(Ku$lXpae$BE0OdHDDLUS4^S)T4ZGhG$`5@CUbqLv6 zf6Y2&jCYe8b~9GVqt&Y6_3!g0P#yX>Kh0kj8gsQ5wG;lCy`)~88mEkf6Y1lt3Y2gl zN&0aqd4I*eTd(VaL@@$JY{6wU2qJQ-4)C~$V1Bd`Iz{fsmltDhy=r@!G z;8p!e==$G1p)|Sko;0p3Qk+o z&1kRXQRe6pbVikei5mx&%~PS_?f8wCPmHPHytZZ9ui!#9fBao+0}Y;u7I-nDg&|e$ z%bo*N&_jv^BYEaX7R=icv6Y4&4{1FWa?&v97+kJAM;D3nIIEKQWFA>XY_OHofs&QL zp?ozR6&{Uq4!Y}se(tr*(P}zeWh(<;6TP9W|MnppcP64{t9!7{Q{i@UYVMo+d?+}* z$!z8d;Hj3cG-p5`KkW2Z$sJ%pGUk2S?Js(83aNSeZyp^|pF4T`^<^+oIc(E(hz3V2 zx1(LABZ$(uu7}Kn*OQ;Bs7DNN|By?8(tQDF=R~y2_fZJ0iq3ksPJn)|7&m^f5~S$2 zmDR0f(BHM&N@|?}Car5G%v{xAQO zWjvVqkAvSr5&0>zSV;etuv^)a{QNt2D^?xUfT*6PcHeOg?6VfKycaE;AK2? zaE9LUwbh`d9rj3k6%*fvY|anPq+;^KuC?>FC?K@um*X8hIxaM;Sapljaag(Ky|Xzv zFV&+gl@cl_gV#5IDv-k_8|VK#wllz=x#|i%QXHGJiZ6<7*TKHqzg`WTqhr$5m2R;> z9g2d!G!1JuUTUebwhWRvUHEORQC1#vhMUATJ*H#Lh7_^pX$AOSqLH#8Hy@t|*6sMC6N{L>n^T(*Xf&!dkf z-qljD;@X^mG!7fw<(i5Oq|e+`lHPT_l#g`wO1*>qgeTZl+Vm)D;mVgK;-NpN2#OuE zap+RVW6{WDmu`Z0H!pbQP9gZ#;7{ZtZ3gook_v|+rsLkn!# zg(pH!=ptx6J5Hdc#x+eycyL;I@Q&%Nd7KO=_w1}d^T}@Jm&!i2Wn4Fb5I#M z@5rb18i;x)DWg)$gx;LF2{_HeYtGf$k%b)me7Q92TnYsyYdDtY3GaDxP3!!B%|L>hj)6i-)H4YTBGs~@iFi~zR7L;yA=164C%QbuS z(fHt4JGG62vbxGmsoY1`??&Blr<)>RFObE0rNNDCI3V{g&|b)mMU+?tY^NwS*&gn%^oRNi{Jqqe6f) zRo_2s-NnX3lS)hF!)h?l4AD(+mcrfc!HeO~)F8Y#RAGJtm3&|NyPk0kh^+szpiV>= zbvqZ%9bd;mvr*^S@HZ^@709hwTPKhE&VT+}&__qklXFZpUrl(O^ZuLKrVKoYbk+`# z$I8_+HYU}x@Kp5b?mtYTBV_Hl(wL$LtAUG`{9mXbEAM*3?QNzIEbj2Bt>Iw2Syg*m zEER0*`rYg)k6oo+FL%FE$0zozl_`WrINY}K($Ln!(w)u!G|te$x@T)Aeu@pfReF*Y z;Y@s-6K%WLM-43oVx_u4hR|Z`Z(F1*3mb7cn`;_$1Ss3qvJ9Cht-5mb%O5jrd$;x$ z-+_ezGMIO-x#3Q6?)x;6bwtQJ4}?tlHP^YJ0hvj{G%WBoj0A~itwd$J{PzYOe$ zb)H2oBD&C8dRrJ&V1M(EejT0oF`M&5SVd^V-;-^j;rHPH`(Nh&&ib<{0L6M$v-cZ<=0BzShcQDr*%Sgz(AyyTbw z(hdjb|6I+4qVa6bAz?O-PgXacvl1XBg#I`sgbKqwBKKbX(7?<{Pxo|z0SX^Wuyj@A z@!;<+un9FdK)PG0%fA#ZHKR*LRmgn{kkh)`RJ?c4PN5}i#`)iJR zXoJ$A^gNvK(%o#;*`FyyC%99l@XARUso7_RjlGpoIlqMJ_*4gXr?=~tpIHcz;f&$K zVN5uVFRE3mrDF9eH`7$|y7LX&!$y{w;lqbl-R2+kaCPH}N@gnuFim{6sMHXtPb3!? z8*nguN!_BoP8sii88sA_N<;P5g}4irgpYO&?JFhyy7qhdxP7-e3NP8zXerQOdAcb| zaX%Gv?dG*GFEhX}y*IbdPY2R(J~SOl=Aha-cJo6iIxZR8@bVHBar>BKh1Lot*2xJ^ z{~XiCnD_gtazgybG22nZQDUs<$U73Ok(Dw>J~=o_q# z)jUpz_q>v^^S?B(V=nK`7s6-LH`@R8dQU~TtKS}#W=Tli>R4E6Er2`Q@zOP>94=1r zmR?|R;NNohLBJM8Ong3?_(9Gb=3itVy&NXGf%W*68{-U&3?9qWTqJ=Uu*9YIDB;s4 z*NZ_fIfzbHN$8Iu_#$p#R?Js@EVcUgyC7Q!8^=TzG#*ff^BdpE><{t~#F^F}@=(VU zO0geZm;)EbMt{dy#HVO?@t-DoSN)0^f%*ag7JG!ue^5k)jt`@$Rzm@Yo99Zutztk= z^jVjRsSXYgt;(VNHh_P$;hLN3X83M$(l|y;2a^}Ri9he);atI}q}Uf0Ha$<>C2CIv z^TQASItL2u>RxyT&euTUgJr3dZ3Op5zi?YJK>WOnJ7N#&IAGuKdziaJ1s7YEr#bRy z*lRNTeF#Gl727U54fiM^`d)ncs&`xzt$LTYh4{#m(d9=I$oabw)tWk2osE++NqMCK z+EDF&5H|axHrT7gDRV06P%4_-vbUFl(5S@=>BN7N-B|Z2N}t>(XN`f2$?EtrY})s2 zH3QxUyY>x_>EoiC{Ms0O7ADJA@&6G&CA~%W`|5M*_;at%?XeIGW}DJ8pEuL6tmu)i zNhbsGb2jbVM|8}eSx(|*GS#urW=bsghc;3-UH4mZoCWP0 zqb?Cbnn>K!G#_3JDLFo4U(& zKuybB^QE5PN88b}o`IS;Du2IZ8}ZX^809|?$BCl1<-6OTkFv6za>1}tq~iRS8r zRW_cfX1u*g`ao)tTAt?;4tgt})eWms@hdY?D!rG6f_p8~Lxk5?_Ez6(|#YZi_gp@xlw4W^I z_eQ8dwAcLXhzcL8w1&GIeN=I2vT;?2A{%j;YyP`hXpEM_TXuY|V8i{38HcGr^aNx3 zR6kck=vrCad1|4HtGz#szK^i+%6@OQ)_iSbRevu1N_1e^rQ(uLPmp}Voze>32!gAx zZ`{5)#ROqjQ^p1zjY!TRp>KsP73+P@Wh4j6BK6gbgK=gG_}G20cx(?3!NW`aSJ!C3 zH@#|;up14A$$#!nY|{a!;7j2$Qg22gUt4=Gk~uy&c!k0>gh_oGEp-bIt?uRXU&+$3 z!l>X&!4neUZ!|$8TZWRs2h{&l4xIl2A z#Kv1cd^8ydzSaF=xq&hUcPEDruArfb@}HDsnHuruXPVUBmqd_>(qbzP7s@jE&rcm< zVkbUVS`uF^>|Df6*9;C`y|qo6yr+&gW|wzL?lDAt3 zcrD)C;H`=hNE}#@ri0<87r9NOpM@Qs99~zV4pT68bRDB$^%KLd3dH|1?D!+u9jXNh zZQ7j`+2-gxUy;Ru@)h&v6JC@h5x=D>mx0XRn`XK^WWeR%xXf+hi%L&K9q>*i z^<)__X;W$dF?OB4AeQjFyH=Y&itA$bS-rLcq;IYGdNW$&q7rV7T8_&bLDV;5#5l~--XZzW13o|4{qIjHNRd7%^j3) zhaxm#E%eV&s#FEpA7w>sU(nGu@qTY;+&qZf4UjW(2S750!C@Rz|pr*R!qxQ%rX( z<9<-3W7Ij?JE?~W%~!t5Hbil8?ZZk}WgSw-Pv4aVe%HY&8@2oQ2V|hX+c>OvbFQ^IbjEEtPoVq~X`#0_$NclKqB`4C)pV^fvtE(;3LhiRQ7DSWi- z2@ntVXW*i6&#eriC%hg$YtK=VnxNZY77Ms`=mdrdN7ZQ03IKgPzk zzIWlTiBA3Q=wJ4%3u=h|@A2OSeHut@a6j>7g)GTInU{pT)d6EFyTCJ& zP~yE^tY$z#@`j&HQ}U9~o1Yq2AfgB_?T|SRtqk<&D^Wt1@DTpr%*CCjY53YNBgH+! z!M(zBJSn~ucHAGDXG?sv4ZiEnEcnF48G1JD_hXVnN<4SxqMJ5?+6VTpxWfSQg-v{j z4rEz>>YnyhUDPv;^zum_pmi~&ZB;Z69~7y}780Lzc=*11Za5ElhviEzhRfl~GJ^n? zBOfQQn15cB@C&v6TjM`95gs6M>6*C)O3%*fKK#f4ni_?>Pqi>{;`ylpJ(9PG8!P#H zZfZ7+9w%El5j{mBf8Dm!cqy#<-G1`dcU=?}INE-$CO+zPl}7g}7OqHS4PU!LflpgT zWXBw$Mwwwir(1IP&y8{W(-KP6>v~+b~rXMoTDYQ&~P zj3CQ#;vZ@dplOS- zlJp~a`=J~ic?jokULtdz16Skty5$d<`2K=+Rn(p2w%8RMTD2mMM*X=`CZvMcjXJ$* zzg1AbVXlzMJZ<=F^X<;Alqb9+RmJ3h2Cgy`jWdf?kl^Gfllz?bCDkUpPZ>Jc&y2W} z6|V(eq5XTMJEYFdyPn(%VZdQRE%oPTO{j|;R9-tn6>S~nzELr%*!1yNRk*MKuJdN- zgxhQ4T8V<_4h|pUlAgXtL`*TWxFOy?OC2G*R~kN}F;I5ds@d_W8oW0Ba(yZ)3+7Di zBJ=O$d_3OsIq;f1nxA&P7!0M7Jd=H>s*5t*ZS9&4lfM7DqjBcheNs4FG4CuTfRBwr ziL*;Y44^3Mb%R?*{Hhrf_s;G!0IMpcS$&2D);dfSzI(0>-*-uezS+uQMf}~h+85Yx zI&OFM?M*!_2?)(QeNhW5kNT{j+|z~RgVdDL4sHCm{p91I?Nt29h{&sKV&Zc3twyza z;@>dY%=TqW9J3S_ia$=~j^ruZp0iwxPlQwU?AJ$eOlqiZ8XFCdJ{8U5Y9mjqr|W%! zG{&U8_g*`$3pqyVQ2lv64E&}xFEJ;6QeA87k$Od(JTHCvO(e+|c<&ihXOVgRIjw}V znuVHiM=$Z$98{%$?+I8#&hNFneJ7+e@wg^@)u1HtC+@uc6Ur31tlT$`0Mky4=qk)_3@ zJRLnarmsIGzMYF?4BwYhQOC0IE~~8jdZ>!5%8lMb=5^TLVM{SSxK$Ac{wpwrvuV#M zm0Xf*m6OQH)-(cD&QH>Mm;o{2F^{+LbmSdNYV0sphtl%Fyy#2~Wbj7k3lrV)NI~8a zZZN^A%)cd>Cy5W{DR=qGz?#lWKigkXFj_V& zuQI|z{haEP;TM$fH!qYEG0DeMhvMuq!i#f1d>-W|s9^6V9b4NMRM>Y(jhrqfbAP*D zw?>jIKDB)4Yx<;tHX+?l4fY(&yK(A~+gnB0Zw`DQ z`CSfLEM&Xq`?nM+z?Qa7aZ3>$AK4l6mwG5+p^IZ{&jv1LEDjej6eoD;l5}osn+!aH zja4**bd?QptlyafyM zw{gTDzo%hsiI>o|`6LH$d;N=v-O9N2tIqWZ@!gu=cfX;Sn_`}MLUYU?JuLt5F=+BV z3#&gLJmgO79(cytW+dxi{L0n)d&!)xU~tnD81m@*Fmzl~ zMjvLPAD4#dkQ}icZNm|^B0hai-L!g(IQGifPfSQrFkTdL()AnhJrV_iwO?qM>ojUg zNioE+UCIkY`8*82p!ei;5}a3l=T)H#3(^;6d*z75~*A zI{~Jr&79t46Mfjh`pKSMEZAQV`XcE}a`$#E=tvAtB^K!8&N>zY_p1$^@uF_$9vG+U!4^3^RaZiw2? zT(*jj(XnHfBf0Wu+PTO}T33KT+uWSZ;VdlF>ZtKNsfWJiZ>}=re6?ne{T*Mcj>UEd zyLn{puio>7&%LdN!3fFJTj9pYVe6|dAC|^56UDw$_7WKVYVfY(!74%9ZJ&(0vZwkbiD7d8pN97?uZ;~~Jw1Br@vH0END@cx*fX(fzX(cgVJ zNc^&Oi)F7t7*wmo4+Z&Zi2t;qKI8}uB^IY1R*G>^t(M=UaE|1>QhL8iE@9&v>$=Td zk}s5$2*_xl%i&RttRX8>7n%c8&z=R!VIc4DPEWwH2x}yHH zM$<8DXUBTytc3&rojOw~Omyt|J@!4xEX3N3rP(?wW60upj*OZrF1GhSsUrS{RoIW4 zerM%zL&^Hh7dvgFJge6`p==GuG>I1pDhu(eb7g^iGntpVX7a6#>X_l7F1DKRA zwT&dccwvfR%EP;a-wl5Mxod=pycRpR2k(eqrry+c-i_o|s?8hJ9pvFEWK`@nTMGqq z=T>k&(V+29_CV!tvKP~2dfL~}&zuqUf{zKSi%Wv;lD_VuS{te>X^{hWw~8h1K;lW*9(wA9DEp00eehbm|ijHhH-D`8UH+59=# zr^p>W*{nfwUX;;a`d`L%u)^Qsk=tgHr;b`bykVs(wAU%`q@UFUXD~{rk?7(IF(C>w zaupGMK8Pnu^5fiFtRphcrg(8Pe|X1d0eHo~moP(lP_6#9=u#889ux1SnuUrOHK5!y zVH!bjuW9p44`u8!7t%V<)x-GVLo%lcPX57WJ@AaC!gQ@)YXixXHO$lbAg#s5=X-u< zTrcpE<@RKbMU5tkOLor+*rE=@lEq(gER>+g2o~FMii-G^sgl!V4mM1*n$+zlI_IyW zFK^W9!IgLCKf_2>yj`eyNSNdY?kH4VkDaN6wdXC~Moj7;p=k1A9r1$~7gouJk$hV8 z!4K*aQ7rfzifPXJqy-CF^xcMQWFP3Hh>)@u+1uD5F4acr{jOkZ-8T-&o2BTz>e)*C z&FHE?W-;+gkDQ(|+A0IpIVl#wItn<_7+-OH137O;@)gA+SV*R+@2nVMph9Yq&V?K* zLatPD908S} z+OkGu6o^w8TuD8el5Pwf^K~Zp!HdUGj-f>aC+zLHsQBJ}$3_?3L`Mzd315a9@J&xwmmnOoVmbJ@dL*25Cd7`=2=w z{m6C>*Sm}Om|yDcTge>Z7wlNCn?!*v<^E;flmPD!G0ftuS=bV*H~rC>i5gGquH(dK zTDIf#oQGs@A^f9WS`U>8u>pHor!W;)Ie)!Yy81qh^m9vC4GM@;+yQ_`wA7Bm|niB;CT|^1&m`mKeSR% zuKF(UNE`!q+nyxkCRw64dST&>I&}!L*X;C-qv2C)iRzm>EZAAej5rYfVNjn{A-jNq zp%1o9;g4K!Y)%gEZC69BR*v7QdtAh%dEdP*CxJmf$tRCYNS;w`Mq1c!CA>E=_cxzG z@`_tkr|w44(RsPcH)4(fs$OA;Q>P8bjn~aT4eR1eYn9Z^Afns-y0(jbSO>aZotbYQ z5{E&kUWV{M^OEO*?dH3w7D$ukwbk; zQlEI&Ld@NhP?cK4M44t@PGY|nOnSuvTpkH9yZ?fcBgvZ-k8bWcT19-@5MM`=COOO% zzr056!~guifH{4&1P{x-RcwF8L;J4_KR#Iz9$yl{x&N8u9qt%JM(gpgY1HlkcazK0fu{C`h*A;N_>SK6=FO{v_z<|GT1$ zuYbSasv!B_rn!@AmZdQVggTsl!;Px`v5v}m9O z6ZZ4OxTa+9U<0F`WlH+oM5{|l{otE@Re{CO%bmF- zU!)~<;pxB9mF1CDum6Ei*in%Uy`JocbRxf=|d_cRnl~FD! zL^qoFto<*Oizg20E)SoReag(Red#Cp*goFUKlM)sl?l(e#eK?{DgE)_)$j$N!~}fz z)zrqy&2e#?oCvK6F9ihY>zaTD{fWK||~)>ud+f2?=&R zy{^I0MQY*ek6XyT*$Q#-pXHH@&^>kWzCFpwH>Kr17a=-w=L)%Iq2n~jTQe6eO5|g+ z(AT>yS^ChllvuY|T@Nl8Es+C>5ev*m^KgQkP#3v3H_t_UA#l^p*@5$!l zQn-*bvCi`!1!sNA*Q>Y4!)|O*h3;LV^IsVC?>cXUx%U25cS8+uAXe+h9ioeVmq~D` zYi2_%`wP1>iv>ZD(V4pmbf{0TJxm@Fe^f+Z;<}399Io|0B~6lt@Vc01lc$U`ca>Bs zdpIb5As+mk%!B)q!N1QLGZ3RNb@5p#m^qMC{_`^b>A!l zY?5cL?zxo}Lx--8dy4RBJs6vY9T2u5ejvsAg2D$iIOS;EZyzJM%cont;{9m2wB<|N z&shq{E+4IWrNF`30OtL`FN#QWbPJk1OK4$*qagbP>^RFM1o7$UUTCdvJ}v8ab~c`d*LJe;J|jOVRTSix`-s%~tXO4_P)~eZ0#;+Q3&E zF9{md_S}i;7FNeEg_Sm^h%fGDKP^Ee_-^jmx3g~hPzi4dRK894XA1R{Ue8@MlyAM~ zFiVnyyh11QuSd9eFW2`rkko_l=Q*F|iEywtc1gurKeF$q5~@>i#}MJP)BOSkqJMq} z5b`E_D<`-`AJq&AUwFw>J4o_WW!pAe+}AQd=yUVJu9pm=&u;aJS;@u`V~^7s$#k4w zGG6=oge-J!{95@qfrsMD8%M8k1SneE*!6cMIbSd5N;m28k+Si?t5z$LD@c73d%DyZ zZM!>q|Jy-D;QISZb&D3_-|;#5KYj{ucYNEr`BVc0zF+HWRicIc#sm9Xvz5`Y*L!O5 zE3&V4D1=M(FvNMwx{VsM)DZK}e`4;01O}2bu7&MU#Qg*tsfk1l1PqorP)Ps2#_^r( zY$AC*gQTY~JQg6!y*2Q!s3y(?)=fn%P)4jv+nc~B4h996`2fZdEy;V+q+jKz zejDFR!6x_i<5N?*$cj^~xXM$8)bW&)$!2meO%Z7`HzITKOoGer^$fg_PF_eM`=y?i zdEd4WJk54m-?zP%gJjb)ueYt{z;usqqVZu3ILGoRAJ*|8d-=+7V;ef6N2Me9+X>Hp z;>q5rrH013r`O(2u;9{vHt{*p1CLv3u6QJBMEaR5#qb5`^9v8(aUlBhlvN4cd!`(0 ze*5qZX)G8<>c5)zMH}zR9~A!0kwxTmsISE};+It`8Mu*sMc+!hV){4}lD|8*&bX(8 z+HC`^!8!)mD(G~*LwJIhqGq1%85O8ry{8{rZ;S&X-%L#O_0e8EtL1Pz$r+uhp7D+B z<6J#?zb@ZS6DslytKExr(X+8^@7p|8>=xHzeAHziqNi#s$C?S+W7=xBKRjGnvY@Yo z;Lw;wb2y@b3$Z-slAyg&8g%75M_u@Gc)zTPW^Sg9Y#*1ip?jHVdiJL+BZY-5`kfhF z6eUn@?j5_EqJ-^BHw+pIDdWq`vG=nI)X)}K`tyCEG<*c=0ZNH1Tu;byF`~=kUW&zg z|1K4rk6>T;PH`E z;cDYRe7(*^`E9YX*#Dxl+6XK-zR8yM_9p(CvMt5UOd7r7Azd@q6a9XV$oaEh=fS6Y zPkEJ#JgRs4tXWI)-5iX4UVYx{genG;QbLqN6cPO5nth}X1G=|ZulMmK zV7OJZar=2DZrr#lepFEho2BnB9*L1dXuFqB@-aD>6V*hq}%W8ImtgKI_6J zdBGi?ivwcG-n{#0&WYn+xX3YFYr1Wx4vvW%w*`{D*9PaOnzw;JPo^4EYduzMvi8=51fECKa3ZSq>UUf(`H_6P#>(eX`tzv+_Yea%ZZtVWIC&alOT6>zO{yq1UMwE#kK@Zql4O#K_;9@uz{F9V!;HBX z275yw3ns~T`Q_b80W6S)l#4ox8sincubEEEfSp%e72_Ah^N9vxqx?6HVoRkqd@(C0 zF_ZhACEhB$*y$*(ZbxcH?2{OC-Ngbyj7EriY>JEr6SCn|Tx;u#GI`+u9ooA zU?f}aTOC_$nB@G~Vp1JE4yE1xXJbEZhXyEvnSlVt$aI1_tVIq>%%|cKxXFg+$6Rn6 z#Pb>)+UymDjZflnmy0WzEXS}=NB5txxE)|Xlz;88wa5-!?byi0a7Gk-{e?%Df?}bODPNmUdidqc;`)3$1yaIysk>6Yx zl_!h+IW(`oTCMF$~S-T)T1lIOaZZ)zo8*8M8mI zNuaDaiXAhIYt_1Y3KLuRIp$(`4Ab)R9OL_g$FZO0*;G*DanCi6<92iG*du9|?xGMO zESkq*kHSCNrn$`VCLPx=BcTD$o^JDEw!|dvggi#fYl1wX%SQ>b{i;hZ z%6c3NzevBGt-^)fj3DwIxF?RKG`;&+sD;-n$ZONI#_?9O92U8G)byC05clC>ye@^D zt`V`aA8wab2p_(N<0)?07rzzk;lyNCawFyO{4}PfSgC;;K1?EstB&I{Glqp*DuwHl zViw!iwbZVNV1t1}9)2eLnDFD4&VJidm`WPwZl1hY(sdlv-M=eht;^T zGitZf_Hg{>F{{@&HL52fE(FyE+K{>=;A zcz(I9@j*EOj9hkF*d$d7;~+CNT5&sp9rsm?P#ZahVUiodolSUskR@(|30#k}7`c8d z!uKJH{g1l@@OW*eIM#MOv@V+m$K$nMj`*3the?hO{3i1i z$E??el*!CwFq?BvB0~tPf3u`S-Vee$I z_o~G4@A3F8-Tke|Lz2g_dq)nJEzgKy`AV8Qw(b}<*OQ}f!^nw=eXSW?xq##9PO%Xw zCi7z=?%i^d-CP*?tiS#q!)c7)EF}6qIUBb3@+X%lj&GLq9QI!>;KY_l3xB1Qb7C*1 z6ZtyvIIUumtBG2?Fs5fpb>l`a9zT#Ra}OKm#Pn)7DL8rsu$JwPDfJ_G-H06Sa}0QX z5M`PwWn?c4W=`OJ-P=x!G0i&p3E+0=6vnZ>EQ;%`siFxTyk5nPLl=)s?&riZT{`I_ zC4@0M3k~~0+>eZc?GODQT*3}<3YS@=Fs9zG#1x({gPjkY8nB|3#+Duzq(3#_$9^f> z4Qt^0#*5*zOG}bM*bzkyDoxz4bpX3=^7W5N;@19gz*HTAnhS)G)F6PLmS;z4B+_rz&eZ0;_YTbcuW@b#V z{K)wAh$Gne{!VcsJWjVMHabBrg3A*vS|B|kg$H4wpw|b_ zC$0^W)E_;8^)}d?<$r|Zcm`?5o=$OLtKl7Hr*d%r)fHKr9W7SuNo3GVhfz+<@j3m) zv%N=f|FW-I2#)hSbVly!JYKg#XreXexH=a`FMqyN!HF3oWHbrB)#bv}*ZInG&xvA_ zyh)j-@O-Oxe@xQeCvjtUC7wQJ!RsHy>dV0ZDPDg|^Xrc}Yn(Se|1djF5~~neuJe%; z!>V6B9hDCh#LNdouPa>P$Eq|_FWr>I;{+kw^Nk;Hd*|Berc+HPus<>eY&^b4un6_C z@zl4HShd+I=8`RlDfTiXY^>t=Jff%{Mx6}UoA!ivjt6m^P{-u2kwQVNDKb{i0{353 z7H}BlSRcoa6J0GfN{lfevD_}vYe~=QOqFjqqi%bFH4_Nx!{K9 z`K!h27`6DH!u)QvwA{km_gl&Ef5P$QF%FgI^U)EEbErGKdxs8VaO%q~KXMXF-FMbM zlmdUhtgqT~zLmv*@MvkmNeC-!8+}umD~H8#@+M!g62N4dM%updoWef6PhwJg%!lRF zR6Um`X2PPT+K;%*2xIiqR^>$6%ov{T?C&o{i#_UE&2W&C#Iz~92I|JdG5?IID6c_A zEH*#%*kBeDcAMSkpe7!t<1O^R(tz6^%|=%}X3zOMf zc$pt75gMUfji<-L1K*@cO$lHsyx$Y!e55hGKpuCEDj|&X8{5gCW*i4eL`3xAz<;kZ zw-4{H#J~TC9>Kr=ug;vM)wP?Jc)dB>>n^T%I~o4Xz~%hUM+p1>P5J-R{+BZUhyP32 z|HI?&jsN@km*@UZ`+uXn|LOlL|6lsI|6lok=lQ!1hX2UI-|O;k``_#Qzfq$9%=_=Sf7}1^ z|I#)1pLPFj|I7cS|LG?yG zt^MEi{N?|y=U@7_|1bS-a{q1rdwu_H|Cj&Y#{KR8OaDFoFaNjyFQxgv`aZwh%<|IzmtE=P;!Qe@2MM^HZ~ViISN0170%TdGqJ zq3t$rR{rx?pplQXoYjj31N+!(YNunN{UF_&9r;-J(D%WW-#-?(J5IAsym|;qBW@ao zHRzeVT3{gX@b@$~!=f_sy-?E%+gyi#SKn#@k|bvI z>U0B?$sYG}CaMCX=1LN)S}zb9Xe{3Ix(#~wUbylUJAo#(POoIh4fs}k(~XMH0V3kf z*WWkW!(;%HPwk{V=rmT9p8VthcTRfUQ~c}%6$|@c2}XK?qgK$^OhP1-XUxgh9nOId zdx}rQ1M6T`?CHk2_D)D7S~2H>UKlpKQu}uiU;SncEB(OkHBF>4a|`sbM@O*{r|6Vu3a&n&q6YRtiiJqtD$N?KNq zKZcMf0uyKLBM4=uGiCI94A!T+PoIl?3RZHj0%SjyLxkQj>8!W4@Y~_DON3e@SPBV} zBxt{Y(`jW@UauRVCzt-(%Cj0EbrH2{SS^M8k4vOh1Ab7JG@Z9w{2g#c9&Dm0@WOuw=(_RCb zLr20H-Zz8i40p_;Y#UHZ@pV1kF3Qu{M5e}-KC_Oyi214KWyY`~aez0DruXM9g2^^Nh zNPc_kgU&F8tz3r_wDx`uHQw@$z{0iu}ZQxwjIL73$lQ*^x-xZ1TcOsUv` z)$X^aDb+=gwVL{!z^)6vTePjb0ms26Ei=HAgaCmJ;dh;>%)o!Ba#oi) z1g<*DZJo&~g0qxwp66ZdgfiQ1Ou~N{3fzQ06G8`}sBxlrfTjg5=FQy@A$bZvxl1#1 z86x4`9UZUjk8a@L>@9eo(-O{k+-qPscM-@52N@Va87>X7G}_2W!}`{Ux$oR5C^vn3 zcjC+`a4CME<8YY~GB~fqW-ACoZ9_!Vwb$o?)aTaN*={$;)_;3^`f?&%|3H%Sy`uup zau2X(H|c`n#kDUAcZPvXfPv|q#sILN&i_EJ(gbT|Z&*rYvVgVq6Gz#Ndr&qSB{qqy zq4r+NM%j!y1oK!p)H9v}vF_d}KbK=*cmD?snJx>Mmc-kBzrq6kQG(n@4(tQs=lY3H z{Fy+UGS-(!P!?VVzs;7vXADfr8N+Isci|JCjR(+l_AlRpk{jKJcJBMDg#UcsTO_j69D)WV?IZ#Qbr z#~|?1rl6F^7d~7J8ypWb1fIDYY*|}k(5f2QY*$SN0cC5mbstyIZB4dw3b)@QTNQX$uXv_!&YJ`xHf|KwT@|29Pqt~T z+Xb`gXAb2AkHAWY(E8rHK^REQ?~Hr%9B!Yb{=Oa_55>7Bz1?}uVergdDn!W*uZg&e zs72qQ+!Kub8(fVji?r2Ob*2avwRUNztArw>rO1N}JvpfUSKA!ZU?(#9G;(0{%SY58 zlFT%?Mho$x%zR4IieNPMV61$|4TKVtq?@dBfFtvqxkbTCP@3}Mxz{uS@BME^N39Km z6==fR)EjCyP+YW&VIdAr^yz`YDnP{kt5WmAKDa)w zF+u;J0+|^K3*B3`L7zHT&#XqCLW48c=jS82kZ#RRL002wWP6FdWafz(dOPMfmCc%l z_8;;|)o5Bo7DTQG>Cf|l({}r{o6ROLDS4Ag@Nfhqny*A3`&9v6%HD?FX9nR!o@c6$ z{3I}b+}@T|>4kGCdPMyZPeHl(sMhDZPHXh4Y+c5zL+% z;IjYV>&EUch@chpRPu^1D&sw3Z$W$zkv?U}(~|Qh$oNfa> z<5&dfM!;o}hwsY=h=)IBK~o$_wur4Wb#G6KbROMjWUbuo zJO%X)xpy=0=S!G=Npl&0kH0MO+?XM2h1co}O^WG*;QEG1QD=7$*pGLM3Eb<1wZ-Z8 z%NMFZzC!C-0eK2&HXZXWm3M_ZM(G(M4FH<^(oAOHCo(GyCO+t#fex;ps+%52Bv4yM zixMce2|~f*dH&~Y(X_jxPQ<>~Xk^!byiQdS9M6;ZSF=UJkwc0noBlMyq?gw0n!pHL zwDzK*VH*YVg81u6V;#`jw)G}tq6kKQ?Uyypi-G3hUV$zOci8DFD?a0A0(<(W_D$k; zg!4gS+SEQeSpV#rTfy)KDe&)Tx!Wb7W#;*By^E^^&$t`G&y=i?#*KWJ^{80{hiW7{ zj;aDP_thK9Ut?gPqOnR-=>-^CmMEc1BT#%%p*KWu6v(nNzq*|1fZ6CnCm$FTLgxtE zo1Q}vkehwzW)O`Nqz8~we{8!9OYXDx_dQYvpQr5Oaq3c#SMaNhY?To*`x2>5y42-Yk1C-FWe;0PpDL7Nm9if${EdDuNthU~pF-5X_r@bUoh537Tv;GnRXzrxuCnuYDklAi`JsBIkk|^k=^B^tx^f zI)`2kNsL|qttxk;?H~~te?k@6Zb$?Yk!Sc;YlqPKJ#g`$J@`FifyAIqkFO{gSlZOwFZaZowa)HQ~ zyK-zm1ZJGsEd~-F(3!*CS={H8;DNevxUFOeu2;eg<=N^W`|~qy4P3u192@Uxr=9@b zooBH}Svw&|xh{i8BnO;3Hj33uec-GkOY$K@1CTpo)v(SY1Mq_GHIm_hJ~@F_l3fO< z4VUaXJVg#J4Z#6M_ldwu#+S>GY8~B>oAs*FpFwwz6ls-qwISQ>)Dla+<8X3YsBL!D z4tjG-Bv{MSfIH1uOnK)8^kTM%%SQcCohwE&I5yuh&^7<{7m)agul9PC`Q9(%lGgm1GyL!s?3lo&m?voqKS^88ZN zkDE4;l=Rja?&I%}TZznNbZP{(kGGi-+Y7p8Q%!X$r&buo z`=&E+pc+stxa|I(d530XjUKaZ4WhYIzsi`+ThUL6)AUQFRfyVUoneBu0F7sgdvM%& zj0CpnViN+Xpujzq*n&$Q95uyX#Ee@&4dv(c*3=Mq>UCN z{0b}?VW}U7u@3=g_>oPugMu4!>@ClB`Qm_{Z>-d(tXrax3D%e|#EXc@{^=Wic{;fD zwQeVJ;tcfMFOYt5#~u1aI!5jn=fT!Ke&vkUJs`bMGB~d`1!70rbm>kGfo6x%yfIf9 zG#`ADyini`svjelPU_1*@$sC;e^fsq&c*yNj*t{oxF-Er`ke_HD_HzoRV;_rDHMEs zYEGhEj*JGHJszY;^sZtelM|`WS*yHD`G5|!tABkbApoqu(wTzEuEV)@Rc{G?eBVN2 zvTDxW0~suO$MWhXp?f#Cx00+h&IY|Kl(EX6N)lIubPawP@i; z0@=-bqb=yvl2WWflp~t(jkOsM=0sg*pC?*g{zTB}uZTB9|ILzB~LmiF7#@%wo zcw8?(zP(qm==%a>$)X!_auZ-Z`q?+)i)LW$F3|I7nFRPBkDF4?yaxgi7A z9g1_pT zSay-!7U~3%j0N(e7lvT@p>&#)%^=*{;*)&2&r0~52ywr&9lN@Z%&G#$GXBl`x)j5UbUne-o5pNcW(9v zWo7CJ#p!$jUjlVu(u!w*R{jofmYJ+P!2M<3cdaQr@+v`bg@`75rvnVFH`z-b4#3B+ z?ILE!`=M!DLKOdr0>G}!CD1OFz=6&T{f7H7!1hJxnZ_GiXmGj6SRgA6o;mswM-#V@ zrQkE(BFQpDqU6|=LSl}T!@92ZZBrrbuSKVswEGFGa>I|pM_UNVXBX&8C3GS13H@2W z9w&$)HNRW98wR^WnlgW+0{C0L8Y_sm!n`9VdC8kzh@-MIAEO$CbF;|t z!(s!EMxoY)Cb_G!BCd-QV zB*5W^4@;MDd&7u~q|>}h4i3DpvnZ9`L53e(=k{nz5q0PdZjMhTXkAvHNkfnVT@Lft zO@8%?po!gUDs|nDicWx>NNH~Ei=`9r~ivZF7QSKWi`JfZw zs1f1a0>x#dKbSZNA-eUZ_E(i*2vbiJnoM(2U>6Sj2gC37?8gQ<|+A5o%1+WyQCHL2vQjE&*anxBB`)8*o(VkOexIayoq+E8qdwC?QE+14?-3t5hbVsjW5$-bf_NsJ^-zGQB7 zWD|5l>f#jM(ZL-ub;-dQX<(7IwiMdF3XBbRqV1Xkp-OWkg~Rs=XzSVYf0L{Sp33-M zzsIk@j+NK*=zS=#tp9iW1b!iA_*9<1{h z1KRtEA=;hcR`IE3WKHq>sm-DjVu$dUKrbew@x@GpNVt(e&Y7p$E3O7>QdH&-NF3oF zy~}8US`3&}?|kxME(GOgmK(P`n!s=}=B4!hmtZ$&Cwa=c2QahN+zXGopqk5Jq|T!S z_|LN^U#lqvftH|m^`W@GxQ$2I?}ZZ-+jBG|Rh)s{&Sw`7htPtN#AWj~?{UOd9cR^X z^&tuoI!@w7t%}I?^<218HVDaE50fm~FbFc;J>bi59a3pVj+%GJz-qGh40To+@Y9A* z_GNWIm5gJ4n#dq@jz{!eXzm9CWrpAFL2b|*;HxRYPzL%tzMc#u36S;pwyCDQJ75;B z4+>^3LUlBW?okP0a4C7rLYnypt<|bujLhjlac}+IX}?6H(1JUgop26W99!xsJ#zr5 zW{QtwI6nf3o|d*L_F9m>zN0F`)&toRV`X6${ct6}OZNAvPFQ!~JE3>50q(xl&Ym?Z zhwh~kAAZkVSW_%69A8g>;~MPc=SuFu`OP;cZ2WEkvCXVmfv_1!>B}e&iKv4mVIbXC zLktM+#Qe02RPbjt6>CHbNGj>6hOqKev|LqUm7{Bcdc4SmY`C65y=scqzSt(Tr;Ome(nL8kNh3yzqiAWOFuukMLdVNd@0ZRk}}{%uh~33UPO-JvlF!c3k|} zqDKoYq3KP!PxlbJxywR@$`Vq1f4A${b`O$OxvefoRfuk6yE!G&1f!$QE0n)pnxKQD zyAR*4J^@`XT>)f=`?K5>$_{6A!VBBUENjLgNO<+8(m8to#Hf2K&!jX1*|KUH@ueKl zI$zkcS$YrrVp?7`WLbdY*u$Cy7DXt?mt@>{$O-)En(eRch~cjP1lvN`2V~3b?y8(U zgnIKh>OFUAP*YOAYSxcbB)rv4(UNl)U0Vs9)3P!_ot@xE7G(~;8t0PrQ{3P}=&uV} zBC$}|_bG0Vw+uq|oid;msAX4gZHjzZzlxHVFTA#gQO`IbV`08dX^B%Q5{hqhNF z)u2V@UV3r8j~4BhD88t@XHEA5(#8Sxl4n^!8#Cp4a-q)YLf&a z)?2ZQRSkrTmfQ3^_Rk1%U87Un=Vp+@%kwU;t%*U$RX|pHSrWQJP7;~tI6}`(lF}D3 zJl^H~@x$T4PB0li!J14l34B+z+1E-(fXewJsrFDc9GN3k(`g8RAxEuqeM@pc-{WD{ z?X`@$i<8AYSz}OS=J$*yMp;yS>+7ANyGw+-*B;XwUdqrb3Z!Q|R?i-!_MzBtRv= zFhRs54PTuXyPm(YfJ1UF_dkUPgUvG;+Qg7NP}F(SUuE118#F(M?Vk?A#viV}Zr)M2 zm@mNkZ3TbtPj!EfG{}Hts~&~rv1`z@+oArTf)>(57|tG;D@Ktf%#Fz-%19tD^0R@^ z9O3rq;1U(0`-F3ZYICwebwbT-)l9eNCVCe+|HzrNFz9G;betCXlxE(rm+s3YK)u zLq(8D5q!ZzY!KS6kyK4uOv5VcPvJ-Ny|7$kL1aOc1Jm#LWNSIC!PhGNelp(?AeEg8 zWD2ZB8*iMWm*(|QXW;YUj~h@kihBVtyvt`k-|^b zz@y0|h_3GV-qL(JK~4I>2b+`vg0zv-H7q83kW2{=7}bKBnu-7=Zf9(WA$G zO5yB5s=IPqKCp3MS23ne21IB&)?Anu5S>0LdD@mYVx{DLKx0aW?9K@{4c^ZqT<(&x zAGA>-@N~qRcYd}-p3|FYXCvcL?KmyV@SjO!n!BDp6T*YXQFB#xbgUrq6Lrlm%15A? z5KC<8)eOa=ev#U*M_@cxDKf}$4C0S%zLQ$8Q z8={+NQt&QnKz%YB7xr@#LI2Yyw=dYrLe1@P*S8Zlpx{To_Q>)hFe%lnx#-ah<<4IM z)EURXE++WV>&xTt<15fVSZsmNsMjnxdGT z=>8vGLOK=GGV7aif;6>g2T248D*8cC2^lv+6`7QQ0<{(RzU}AN*ZZhIC9a0lpver* zKkCbJRm+6VtpKA|)*eu&_jY$aI1OV-g8i1ML%>~DMfX##90oUiB&O87VZPa}A*KHm zR4sQMj-Z=B*cVEzTePm|E!)M%>FE?mXVT>6s&^(qu351D)iH5GI0a+LS>t>HZEW|% zW92^tEvl~iMivvKVK^=%aJ>Z`l-R#{jfWFHb)6qEjbB)BH8uaV#QQYJS1}dwuC3$@nn4f4oq}Q{Ch$BBUz9EF6 zyY@%))LYq6l<$;Z(oPBCwo6@VBE2%9)7doS?bl{PwN!Lap65~27N5sapcI6f=E;1T zuB{=D_1Dj$bJc(*=f#Hd<1nc5q`JbcSPLalA8apZ3_~_U?jt9P322!h35;>*0_Klz zsLvQYg$JoW9d~5CKw?YfOwtD>kh7PWyxH^%m73e?PnPE*?kFOiTVYB_^vpv{ZFZb+ zz+94K@w+1-H~V7Hc_mi@>m6_11NzGZfu&ooyc8{wuIBTlRE|j`8_dpq(@q`|s0JSJ zvW5Y}D!YN+wHEN-WHDv0oQAHO#-pZFW1w{P@WNHQD$t@UJJCDm2O1^I358t>;JnST zuE(~H+H<2%sEuc!^Go!n=Iu1mmyqBScrgb;7T4$P6WL(|${JMnJdTH8%k#zY%C}0w z+l6;;xpGgV^2F+CIl(FvaCWQLrGg6*73q5fPTq!Z@|Wyv3d&%U>BCIp!y(wxa`HSo zItf}bmybKz^uVyd=Vxp9{(iJeb1yO36XuMa?zGrqKt**i_ucz#)E!k7psi4h$~Nc( z6IV@8Sumq$pEenq87W-Z=U7U3f9r?uZLjl$xXK?Q;il~9LD!TB2YozpNT3;Ze)$J2 z#N-ySWa)vafzi!(=`j%3Ke`&RRSOFOds=ChgWzqoQRI7g3`7=Z8mCBK0kIVQ1!CWF zaA5eMlja@{FFl{A=c*aOm!{XZnB_QtO1AOMj@SfBk8_iL4G&O3dnEZ~Sq1dyMNMeP zwNC_zjL4RPwLuaTtd#Qrr`4eLThaGy}wm$Pggaa+?@jU!OF+|WS@(?wA z)P`t|oqBWdEIoLiAt4iI(1jxfhdLP#1OQcmztP+_qRR1Z}&0 z{Teq2T((Ds8nwFN*fiDEh>1FIiD$Jn`Sk>pdDL3JN(4d9+Ile|%@Q=KTg9`D3VGk_69n**J9o?pm+w% z@^ubV*V#d$pu-Q&&Ij;KeTKW^Lms^1&M^(X*9`ikj4OO91F#@ksKT@|1P!|CjL-2n z{E3g7dT-yBgTGYWpMc?5s91D)tzmT?C_ecIX=uuTorYffLhe3LKIj!mIn<7lH(kQ^ z4+f&=M5Y37cL0TFM(X{zyN7t@)Xa EUB^gaX}s4LGXwjyW>X26o3pne_DT!N@mN2=uI(pFjH?Hj1ZpqJ4Ye3>9deLwyhKmf6Loql9DLQ6F3M5wles?HVm3 znBEq4^P7bG;N~bC&t-ZQ-uY&7 z?y#JKpoW)4-za|}9_i3OmU?X{wa=tpp5s2EiohOv{+K|2Ua1$*@2#UagV_82DU@(> zp|njAM8Nj>{pN2`dhm3HSv_~}4lLyyk=6K<3aWW0XBSLsp}pWi z>BEo1g2($HxU^v`Y~wkE{uUEH5S9u%ugKoefir|Kh|cg&%E8b=qOqybFGOto`nrf| zJ!**kwBRas8xW2?Z^=}cpp+YYqKf~EnnpH4d*UHh0EBj1)k`nb!JYD} z-erynFiuBgpRQz$`zexQSvW<3&Vom0pkM=?HxE-VyH$z$ZErJr2;4?j7o2V8I;Ih) zx=^%8?;08q_5ID=Mh1+v6VGxjk3;Pa@rS(`RiOFSSIhXw8nlnK-r>Uil)p?AL~G|W z;ndUqu%hNVh>)h@C->?F>igZ#%kxHnR9E~|UsEqoy{QgLE2sp)Lv}3t--JV*?^vw; z5q%KXNnyP+P7h3jk+XSM2GAp^%)(l-2k2wmQlaIWCOj+VZcE$SLDZVyoFp0Z0ipV! zq?oLOaIM@zy@K}yh|9?`6t16#VLEv^_E`^Tl2W)kLzV&G44!)^zH0>C{S4a9+k^1K zpC~9YY81L$Ij(Ueb-|?lRd4^vLZ}jI{HZhO4~vO94|&Nn0S6%6uHvBre(GjM{ghsW zJPi~~=N_OY$zOkl0y9y~F|~+wqcRlzQ%#!1s0*DoNV|OM+%kG=#zb|dn*xSEc#T-k zNP!03lK5tz1*n=GkqS3 znEqJ@B7ggF!0yUNM0`Uff8!fH*r)4x^juYhTkkBgP1hXY;1%018DAd)jbZBn#f5UP z`po=PBDVw7&-A;CZ1uxuzHifZ=ccFUfQ#-7Lvbi^BaGTq&eXfvs5=}7-GbS(WyK_5{ylGR?9HmjLJik;6h%de6{46WkR zw_^ZYHrh+6^-jRsbJR*%G#Q4&(43<{4X7G4q}^S82`7(hr(D_Ygna^K{Qr2Zh3Fg=%B<9ZT3RmPEloB?S_l`yUN4|3-_E zZMPP*+K^=RLl%YG?kK!!WILVeF;YK7J>X6D5fwY`E12Y6zLegYq<3g3DF#vrvGe#)|{!D zOC7?HNWF;NW<(t-d^p=>_wX}%_=ccws=);zVYZu$p@tys9HuHE83C3di-Ue^l{oz* zG#S(nx^a(cjXZJx2b=ECk+eQIIy&WcG`SKOX$EKXs3XDr_N@@JxXTce#%O2}#0j;Z z1Xoq#mr+VgYC~^h71E&gG7NbZj3_f-&krB>L>^&w6e~TMs8QvYlacKp;w<$M`BX#< z4R#>tC?*S4=YGv<{&IwjLYl^^;bb8FCWBe})q!Cf&sk9%k0=vCEBtL~7~=l~3V*cg z1@EG`o&_9VVfiM_Ea-JI5HDQif5GGgzaJSLyJaW`4mDb(zRtuTwC#>>`Uj9Sx0u%K ztt@mnWhGd*$O`>&qab;w5{{M(k1=L{t4CJ()D@;2>nK)scTw`9065aw=J)4Z1$yE) zNr|~pz)okZq)k={1-)}dCpdZ`Yi8R|C1VtBer3CP`S<`REiD_7`qYAe>4%HXgK;3& zLV}I$w*q$K_W228T<`0feD_({M12^EKg*RSbdBwTiZpdBTH4#avGURz4VzvGeEIeX zI%&3#ZzOUADLD9uci$s}t>;Huh{G|+rrzhPkl+lpGrn=WUy^}kkrKN}RSVxNyhV~Y zUO`Ij$)yynVd!VbvL?OO2itC5e@evh%(ItyN$>PBfPy+rvgM*X++n^q6w0d#7VIG` z7U?u#b$FcG#(N5dmmJaw+*gPKaxW4jeeIC_7qMcM=~(nBKaMSi(24w7Uf*T1+Xqe+ zQgveu1W>&nADJHI3VuTyXD`0Z1R?|aw-d@OV1DJW$&D97z;xon$Ez|pew5w%9$e^z zdoNw=+#VJ|MCc%|&!bT2x#~9AENBiY<)2Ha4oZN{)$~5XrF~Er^zvpc5ehx&Q#-*&O>1J^@j8jB5QU?qOZwawKF4EK~4 zAN}0o&zpH zo^=HmLSZiY*R$vh3n-1js_f##z-RaHiBKFj!SUcoRq0$c8g{f>G)%HVkBm)!SU~h3-*^LaPxV=|^8u<$~c@uha>pi9E2*e!=)BqY3|Mte`%Z zSKVNjti}F~uMdW!%IC%hI^kdkjY56ebMSbm#rBoG7)+KX1J8U-hl|P?&PUur;ZgGH zIN5P~P{=1;eov_gM~fUqA4*WblkQmJ<@4#tW^{I%b!{A7FT)Z`U(tbGmYLA}9X0s6 z`}XVoT36W9VG#0snF7&1*sNy%bC`{I!cx*S2zKtNL5x>MA!$ADvx!?5usn@Z6bUE= z-iw9HpF5)=BJphIPu^QlJ$EU5uf`D6d-5pHcPPQ;cTKgC3<1y`*lllbAcaTU?W^kL zbBH$}`D5VCn~2pvO_J%1g^^Z8k+_oyde?(*F-J~$gSyb^Q(w|{%QK6WiUg4b5l zy<6W};Wy*3nnT(+JgJI4vb``08w2$$XA1DR)Yh4sGja^@(>~H?6CTV?*%)a{d zZgaFT~>!m zHJ7$aPkcb-p)n`rp zP-YdGw2a5kkL!i0dU;Ai+D`zS#@6Gc$gVFR8<~ zwfxv?{It+5L%JP++s9OKnN5;frN}^>q2&CzMC58ve2cR&1hMVT#&@OGqhdyrIDQ*u z$PScNqA|DuZElN}{gKb$`_rpJ53dfw%#IwtdiNv@rn~1o$>;@>`&r@`RtiFG-oa

_u)L+!F z2T4*$pM_L^Oq&_``q1pXU_Xpj*SOw{zh6b6b?1x8lZDs=4UHo{_dURK)(M z+<_?rz0Ym;mS8YP@|<6aT{G9dqwuyuSMEmoO8K?uE8Kx{n3 zbYwhAqG*7qblW6fnxuR-(apdSQjnbm%nxZ>NCFkvJ?*^f&Y)aH;?A(`~OE#nUW-A9-PZX&LnL>sTl0qt($BfB551D0_44ID1Q)Y^iAw?xrDn+@k_vib0^;`FQ*YDnS z*In!P`{RE0T4%l1^R=I6pS_=Z@8>>e@8|RRD5mu)4N1YCBO_dUT`aI0G-?-Plm)T$ z+Y}xjpS?w~7o3!XTJ>m3s;eGfs3{EVhF;G%3j&WbzY}dF3t{9((oZ_wHb`8`tn>FD z23|Ybs0VXHr1$!-dbB+);JTatWA`P(E6Qv^cn9M6t_E6)?FF^svSpM)xXq$oVuY!TSGk9eJZ10I^zs0 zm$$z2X^`}%vq3WZ;$e`O_i0+KpoW6uhH;bWbJ#0BXRC1aS}auT3vY?nUF^=85mS@d~!PY4(U7PnWAlF7HDm5lMc+voR*Q1Xphp2aICp3qRvWGMJpB`yOtaUugdc#gdm_ z36wfg=WX_l%kVT*D&*&fFgV69q#Uxn2u!m*T8=e61-@i!xp)5kFo~~u8GIQ8;q}js zyw#l`^Se7&S?)0~@BA^T%o_)ci)q#(@eZKV{^*U?spG&Nu}*oDf(2;X_#6ixyu)bc zrq*u%uEw@`SQX2Q1YqeMf$C9vw6HFdyPnb%9N1<1yPOu?Z&2=`Gi`pXRxo($O?g&$ zFnsI2dPLGVAMX6=*k2Ie1dB-&UD*tM@G!q}&Z}$)K7D@w{Z%^2p5%N?wLxPw;78t4 z&_qPTWaB<|`xSGrWIOpK`Nl!0_7JZIDzs!)Kq<16oeOj4wNTpfB^JuDn?rtT3!E*Q{2;%WHv{4KwL{AkAQF zJl+Om5tHrSVG^(+k-x0E?F+`_X>P>dQi17nx#KmZPS~Oa8!Nw~G?wDRwTZO+LWPvD z);mT$=+DP@8NZy>gJOqPro8K}z>L*#o7$#8jk{{F@tayO+&RRg%>E1n+{d~vh>bv1 z(soUejb2cejekBmSOa5Iqr7tIMY-2Xz&w-~Nz3rsQub^z6IfK6V*2-FwL_aEyY0vU@#R4*f&VT{k| z;nVmB;1hb%>*IkNBtM;SO9OEPn(Z7a177{X9(Vewe^O||MCV;Ncrrb)z*|xY-yfma z;fEJg?}l%o=P&-!DsFE@+jK*HJqM%E&84XKW#?{zt?TAaevd53D?GJbki88eDJoal z8%Cit_sA}~>R~vmp&pkd`xFevHI=6t&*N_;VQ@d^ z>&x3IVMucGqo*?IyL0;Ry2-C#Oi6RU+CSj6$FS+*~z3=}GV}GifSNgmbyVN>5>mYg?d+>Nc zkiS$CJJ(M!cliDX^!?MDvgna~w52cm;K>aeRFjci-y=>8T@jSMx%6fop$QB5ejrsF zT9VIT6SAR=UI{X5Z<_1%S`NrUtt$hN^KrL9r^kj{31i~lx2AS4_ z0M+MS^TwTA#R?`TdMc|ju|#^pyQNDPD?G?%B)mw2MN6CCRZx6_?ry#wXA1VH+VLYR zgAV)Anb0$CBKR+?qCPra!Nm_+p_>>5i|cwo#20B zac|$?b5L`PD$!*h0Yl3E1)hVo&{xtdXwnc2l8&z^OMOm40Z!N0CcTFF_$_j>h-6}G zVV4|!$7^E^_S9J6+3i?%S$@ne!5Z`|ORi#mhAo=hqEr+%&V}9^^x(l;M-dTAt^1v3 z@yMCJoqO%XE+M5laSPwr(jhgM|5_3H6c(HC9}}{}fCnV=FJ=!zbh-XFNxOEKWXMh0 z6J7!D_az0NaR~b&Z5_d>KhD+K`2`^%YCju3DmB3VCKidG%{e!R8L2rjEtVr<%oKB2}zna8T!7x z0=m#y4c9|GpwAlisi<-kK7V@4eaveRe%0;T7*uNjjmx~0C)?7Y$qj4z)^rE-zL|f! zQmPFD!8ETXpYwx)h(%!ho>}Y>MdS?=*=X#wvTi2BFzGcy*eb`mX#stDCD2cxHVXYN zN=#NnD4^j?S*7I#b4X>*?vZ-|amc{QpmGK`L!@8U?hCI>5V#la;-wWY0!g2@V?2L4 z;RyAlex=elG}(R~<=`3yv4RF^vw$byk%ufk{}l<_U-BJXi86%Ht8aRP`dGm>+-B+N zW)GHe%k!qubvMlDwR5M3!~tyi2>p?QLxZSx@;O*2j zm@Y*q-a*)Q7(b`okVY;!O1VT?#(<()a>8I@6>zB8#NWI<07K%>!j8(1!@C{SAFUMo z;NTM`yUmXkpmHm*@O@PURjCaI?eu?HIII zgIKW;{u-Ze8O^Bjz7yInt?bZ8XZF~%7_p#Hl+(v=Yc(TR8HN3DAA3YwZl*tO7eB&8 zXJr{P69fH_)%@pNGZ^+B|8nN*INW$R`O3?G91e=+pE`G$fYDt^SNCj>0f#?t$4Ui%6BtJq4W;=;E&la z=4C-Ns9*d2Z}|a)UHa9XhyHHJYya(ZFSZFI(~HJeYKkV5tUe)K#;r;4h+k%yGNc*O zFRJHob&i548%^DI>T%%Q;ZxT9xdoOrNye&bDc~Jb=c)0)0fzX zm~^~_^^c1;vHs~R2g-v5u|0iEmrpSbq2HG!EKW>&p!OpJ3TtA*DCYPn+(~i-2{&2j zHQwQa+|zl}|DjtPVKsc0)nhxW6nRGJw@pDkFjs&CKmQQ4%*oBZ%^3s6UyjidzCAEK zb>_t6<8qRn^N-h!Vj+-lu7ivFg8@7~XVSLakrRS3`{s79Aq?8)ny$R^!9=AA>ff5; zn2am`(aN-!Xl(Zf<9d!@baSRtR^C?zwU)^IW#%!9&V!vG=%=Kjy6Rd|6RZ5|ila;olwp8qKg47?!RM zLk&2DpB`3MKnwhxLfrY@B8wk`4_7e9AbSIv684-`M&@)6xSsg}f`jkul&GxhKc;BIuLsq*% zq_Z2Lz1oAJxpPv#KlorYTCU$8os`6~G!GXFFT6%&TP9C`aSua&LNshq`4eP?v)F=q2 zFm7vYXaP>vH67;b86fog)nZ!yRcQ8-YW$@l0Xy%kCnfj3#|*CgHfl@B#FQr%cE^ht zU^hQde(xrIXWrFgpEUkehpyA^k+I;gMOD|^l5&z+QCUHM=2Dh6MDcW`0M2m(Npzkp zQQIkm$SJ2UHy2MRC0{s3wYRqx9=_RGBuaX3@9FqZV(T^m`VC3KBhOpG(K@HHI5P&u z?^eq_xpx-g`3@9(zVaRO(%w6?%$|YC=Z$Ss7E{N17Wp#DnSP;JUuf47=qd%-z`0ucSNzwY3(Qfqf+yUACS1=;;Z;#U5ksYSOUCWIS+qViL2h zw%KR1%M)wtoIYdxTmZWqb>k3?e;?ZImElCw;)yBgXZ(Cew$g~1rJ~Q8k&u{lI@7gB?y(5UIP;N4t#TS^@;z>Is5lDQQg$8sS#li74PXDT zqW4*;wfBTA^7!TMx0t5m3#95oIHowCG|^2dg0Tnirm24F zK(mHDwwh#ZQ0Armlm?89Xy_$|v^Vlc`5$Z?$aqy_WPg9J(#?By zdv=xiEB$)Os;Lk;3Y+mCGqqACAfE#Nc5hcNBvjclXHS%XwrD`vj#VEhd|uQ+J9h#$ z43~sjvMIo6%BM)|{bQ_2#b)8v4l9h>MMjnJB`tRNn*QNMpFEV$SBw7rCl&OM?k|Tg zH|G(`%*)XW0%1tKeZIAAwmkA<=QjF-ev3*vpRT`6JDIAaNiQUPWbwR`m|>;YHPaE; z%zV$ZVm=8^{i-%D*7d=X&cm@2?L}~wxVKisdj~R0`jhQI~a_+ zf$4=o;f8-X@E*JV;`N^(_(o_nelpZaQXP_!tu0Tw$ueuJJnj;r3SjnGA--~FY z*S1-{?^F8Nzs)|7BUq`XWajg&%t07Z2_Jo9KMrxNZ4KsU2H~xwl~ie2Js5U8P(IU{ z2vsk5k8~ft4C$uShCI`PFt3BW$4-x8BYXB8)>QSvtUiW_i){&Et$DO%DtrUzI@R%v z>7rZc2Tj|uU9?=NXk;W`_p?TXO8Sx3&W~1zPD@t#ohBAUN7Z-hQbC{6=CLOVM+!of zrhg}I{B#@zs|Le~Ju{;a%q(%NV4xRf2v@Gm#V3I3*lyz^v+6pU$!-$!}N5!LqKt>iVsufd%SbKXJ1C z3tGdC@KU}`K+jz&{#bGlM}H+WE!R9>K-@M8kKfjeLVmp=DBoO^LnJ=`EVb;OR^m03 z@ZI4Rtz`1@hv;$9c2JvB3g}oE1j~5V+(7DK=Eg?iVKBzfe^P>4lFbxhbQ7 z=cwV9GihICs_3G0a1=KrY42~9VjRMvPn5_Q?eM~Comoh6P7Yw{rQD6XQhU*#*G;a~ z=QyEN>hv{l4zr^|Qq*+~vMtDDe@X9+L3_kjn0=Rr5epK1qp_OrUW-ypaOb*6;SeOw zq&&Ga^c*T`Q+j*aNOqRPl&U9xQ~?8xDxIHHB%I|gtDi?q;mFFnBX1=SfO4W%p8M82 zYF{M^F1{tb8YrLZ&4yI> zL23glTVL-u=nJl%QpoLxJPFy)>;ffFt>HgTi@Lx(J8Pdn{2|y3ez$dQpa;9zk&*9- zTVh$f4zwpyf1zvjMhw>L320p{M^fZn3>|5u+1T~w1M-M@N3xY|8d9Qo7yVYEjcjwx z8d9Iyfs}@)bO>}jQ3_J;PiGM=h7pz712jYk;GN{t*}i7s~ea_qEK`X|(RlJDNT#Q;>hN8s{- zd_mNt{D<(<4rk;v!`IcO?FIcu?Oe~ zdF3*JL72j4(oY@igNVjjhgbas_*}63ef~@yxQ4K^@!0r7f>N(|Y`q>Faf-hq!o&}I zSv)kR-n_zS|6I~u&Gd(mHn*c!IIAtIQ8V-cZ`S&~mi+(|uwh^+qBWz(Qnhgmr;F4${i;{4)k9}Mz#Nkz={!NAv(bh`#R;X^c? zzfMX6uq_#0Urf&jRmsdUdErp7-Q&x{Yi>#Ma|-4zvX_QKd!E>zmi~ot)F5W!f2uHp z57AVi^;XzepwrNS`5lOds?Nr+g$ zIUHFiX3F$68V7~5*RNi_GYJm)6jk*0q=gRLJ^50z$LL4BtW+mI-h6kKY}9Y+x*gcH7pkwtSsLI7bfl zr|kJrurPZjC6u%3k?M)|6fh_pyN#j+@Q%s6XdP)`5%RAxC`Xom@1Xn`ZH}ba7Cmt$IiWA-v+Tp@bjp7+Z;K}*z- zT`MG6VjntxG&HH~cPCQj%5_Oi*%f(b@YWi)IE?sds$3;S7ly#`g*jWjA)sOGyHoOh z0Hiqd>XWV7;o`?37 zsHS#owXYW`rM8O5a=3|9HDy{P$u@yg^1)AeBzd4I3Ld|C`oUBvOyJwYKB#UP_s%1k zpcI_+cAVK(2B|@RwC|b+L(wH9_3&*Sh}*e6m6dBZ9Q^*#;DhsMputo-ad8%1Z{ep{`?md@C|RNHHe z^~mNj?`)>T80s@GQ>hoD15@eVeCN$kRmxJ1AL$&ZSMD^6^Vl%b7c$oJvooYnZyS^+C(>8|f~Bl!!w3uu2aJpsld*NR$Ckm5@m z(yVAw%LZEiWb@W_E2w^OP~n*=3(Ws)z4>T&Bi2&SO<&<Q6YL^>On(hG|>9><8`bb5kdqX@6`!L_8xHzbSlG4m&+AFBO6@8JD>H?*A1UDNr5 zG-^{ZK2KAyhO|qihjQ@OA#`rBeOlHY$Tpk8hS~b_@G9NycHBe|Y@>4z-Erd)jI!UT znxg54S|>vv?{!ifC`8!dV^cq@Z`+kr#PbNI#jp1CiU$C)@PN*aRt+BH34feR{DsA4 z?mGFR@d3vF?n_qOaTP4b_rvDH+GVu1@qVWwOD0;7l9s5ceF3d5+_o)Bjsrd1#v;3I z_cRjO*_0^2o{boYny(+k+B$mrfXl9aW{i z+*g`G=vmvWmeK=omeyI0UUGqj30HpO%;QkPnGk8#MG0$~QRQ!aDlz2>|4!YjhFI>k z#;3)PHqm!uuX&ov^3mOd9MxE_FAoIlD`OO5sWZRkcgKSCuv zGp=k8wLoPCcqQq6?nUR$tHlWJ=|fU&RCxs{!w^=&e0-q57tRDL-c_M01l4h0It_*{ zuug6r3RNG6w(38g*GYD2Gf7lK1*=a$qB8vDHI+zk@o3@=t<@!+Ul@#M=raR9J>cU{`X$FjwIWX7%ZawN-uJ&o1(*~vfaO%>Rf_-Q= z9o6T1y9SV{GHvXsWjLa{*O6%;tq4kyizY^o+acX1?#P|;6qZDqNj)-I0}h`EC1F8!Sew#J5^c1`HP{#%hq9x>CYj#v=@=sZi>9% z-O<{Zh?1!h7acP;ss8QU%;{cqwd=d@%BUMUdr>EP@VGE4#>$y#an@ zaD7Qi=WP|R`fH3)bqoTfbnNsG_vf%ff}u@)trrk4lk1)~CBQ4ZX>ppz7idIcT@1yv zA=Jg?XGi)$V5m>}T6&QR7`b#;lDj4_6}#46n(-oR=GLb7DIRxBz&o%~=$RUJplT`i z_y>AS^}&>!)Wrt$8RuulDgzUgFWMu154Mh+_fCq4-N-^KA_W~`nf#iRRd&S&~FunJtHUxk)theC$T(fpvNIuOj2q;koD9XL6$6~3u= z*!Q%ek^M#u*v%rQU$+>eu;QM@`{PL#*r1)*(2o=e%wpd}8fD%u^w+P681A`xlmdI0 z>z40;wyqh5WkxZfYstFiTxAtVTybXyw{AMzneWqof3g`W$J|5w-j0CWxhAR)&Er4? z{mR{>H~_(7Jj>zs2@t=cSb5uCA0lH`ooBDpflc~)a@fvhlAp3!=zysYW_tbdXg9Yi zwulOz`>Mc%y+ZFVFMpgsl_!&59Usd@lixFzTJk%h<=8J3T2l#Be5u6kUc(#`+kc)# z{dNdK6OwoM$wU&&oh)w}Q?CTx1YRZPxlXVtD7zw|J_GB2bU>Z7}R27(pHL0Ayd#rqLH$!TQuJ za>Q#dT--p{?&dwl3hw%4?9Va6lqeI9%yR6;wzK{&t z)6ANVgZ?3O{MrIr+tO#mu(nXhnW-9yetG5W2+98TrzjCB{-PY_u3zn!U1){)9-$Ys z6NAv}*(A^5Jp|GYZ%MVW?lqK#=jk+YSA0#ksGgA7Pwr+4^< zP$?UqPi-T`m97)48&iA)LIblhB0XJjbmG`aXKWO%USg(MC&e$=w*TxkG0}Rkj5OJ1 zM6#n7pg$BOwq;DRGy5|3%aj$K53uM`MYUplZfhiCE=$bg<{rfd>g#B%*%y|e{y3EX zviHQ#JF4i}%4pNP5E|4Z>enJu_9(*Oor#5fEJXH4C)w1{>Vfdpgs04^p3pYK`&0Z@ z4m379^{zFx0Tt0y@^SAt+~;WI?x7on)@T#iv5yT?-8y z1NQdwcN9qH&a2JIx$1p=&?~=fl+U*Y6h9|u9}$m&rq#8h%53_ixMwKoGnpP*_HxQJ zQtn52L7e$>`ya>E`Hhnc(cAcN+-jNjizMUX{odZZ@D4_D5JQ zqlK(Je(khRNc&px1z-NE}qvt#OP{P&jkC=76qTiIag70=>dP5A+Fl4xU z@3oJlxG*tTn8?>bsJpi(lk4a>422(A^Tm4LheOF2Azcc$*=)|B@BBe@W109`tpn4G z)0z@aJaA&jQK6jq4YuoK?v4ZenV2i9s)iM5U3FRPOduHyq7tv^9RGL!UP4FF zdy*Zx=G(1a+isY+{lne#P9rFi&>VPa>RpY|OYLRAD=OT;$p(8nej{_AC%h|xnO=EalI zNZl3ep3zJ+L|E0WbH*2g?2uK72KE&4PpHHPUVH}S`dLT6+#}gJ2Q{yolk_(IpBpEh zysLul=I*`TZAp;TG`v%lwEw|xQNiOXdM@9+lcFh7z+$z%h4%p@!f`Ty9>&? zmJ{fnj(I^(1_#tiKisg>nF)OyFuRsGX9m5!S@ZlqBY-_suTa0c24Afd>l40XZQB?z3#v9aQyLQ8CX=OuW$5G5) zuGD8JF9Tx?GO}4`zm9EwuXO&Lhhq*GQ{Ns;W59xztUjnxK0{lUhP;1Y_C#~|UsoJr z9t7qNXwyzwiLR7x#!P5 zm$qsUivC#n@jx1Q;Z)n@kvpV#n4cYo2@PO)ev3LAQWTj{;^=)6RE+b{%aLouS_sUV%=`an|3hwLrg8TsGElxlghi)?2M) zD+ErXLtBpbn&FfUB%(*8%{;}%+ye?`Wr5sL1s6#z0`N$rkN5Vau?ao5WZCXvOY zwzD`XE~c63{CQS5mOi#igKq^Z2+qjT=Ig{7OBh^DxYDr1rG1;3I#;l;<(b#9_2QUR z=)U8Zk1e1RX%AYERXc!~IfC8xE}ZeUkIh@og;(w=Q;yf4LXW+Wq^sc&*u&4y3_C`k z`H+0oQ}zxhZOZ6S8Ylwx;}7f3*9Slw$H%SXC-osb`F&Gx`$3pIr7jk2w1HvoZYsDB zk70e{4qrXGYA~x-X~mH{saV4U>yv|bN$<1te-<4sYh%_gh1boLIWZR1*y!^&j={H` zakeT#q{S* z#egRAD5V^TXEPWFhW)?-=iQ|D#SvJnree>F&2Vf~!%5&-#U-qyP=Mt&?{SQkP5%bc zCWvjM(MD1_Qe&FcR*c$(^XMft?ikm54d|uYrN{q#IuAlOU+l1`4uJDV9xFQI#Xy6L zsv%fAWEYm7iK7?>Gk4chQ?G`A!1BsWBuQ}_grasd$mD?tM}Tc{-7Uy|$s??$fIzB) zK;Qh%KiHYBhMn=<&Delgev&WApZ-}eF@3M?Jhp{;y6-+Gg4MOTu`@K$Vy^-u&vB9B zGe&ZUJf_&+i%PL|YpWmH2FrVsnzS9x10!$qy@pE(F#CSeIG3*-Qi!nu;FthEu?@Y3 ztD~UI`k0AvFKItUDNyr)uOCQ!q%;ukQi9;Kb8cCJpRh;H9w(n0s90 z50>l@daOh3z{W|c4pdR^d_)87J#@cxY=4sZCG^yByV}<@Dk!)1ydCckVU#PMO`+DE z8d#2I$1-0z0|wi1Ue+jo=$`+))1Iyj9v?l_kuTK)8;?J1>HCoUEoQo}w!a&K&kF*wHH|FYK0gc-T5(cZlEU6?o+~SxT||9M zr1CTqa?#Gw4Mwjqd0}66m48A>-%Aq+v1|U)rZ=3s!$lzSy}f z7N+MGChnrA^WY4esLcOVpBIO^Pp4-*oYCQWwnaSaqkn zG7Pk)e8<8=2jDurY2*~i4o|9dZ{;_ghp>3Hz3t3mFnkF4(U9F>0@+kMqicP5Kzv$v zJwUG$OB&(EubEh372T8^td&&QX$nfO7aT>XK=Kv4&+5Hmravx)b4t9QdV4??w`u-uveZh#b<7sl`Oz$en+l#9$nlaU;kOYU zu1->TWev;g&rf7={qNlIwM$aC%g+ZTEBvUsJL zQg_N{Y5cpwpPrN~Nj!h6|J}ktS-i=vm+Qz5IsBB&{qOB7viR#buY`RMW$|lq_R=a! zviS8|cWK#%W$++-8~M>>8Qea;_koh86#mm{NAcJRS==)F;?mN6Dg5EW^9Z|m8Qjsi zG439fG~VpK_wA)+Sv<(0!%HMb8qW`vx;9OE{wck?7Y-y#<72ymimu$1!{xj=qd42; z@xyNNeoHTgGJ?{c`AUWRy_vpmjdbT{^9xR(ovN%#xN1;j@_vW=?=dUpz!xx<7ybEv2;3JcgJ}mt5_-SXejQ2_Mc*_o@ zuXUyhxTl7AeVVvDo;6ynK}(XKWcbv_UVjDra-9Ih*a6c2;Z7g8zsTZ3^-|go_ekSQ zUEW1{KjrX5*(c=&+_LzUXP@kUI!ofEk1`yhy<~8;c$3w^1}R+kj@Q^#Z5f>FCwn8? zVG=I-Qj)I`g*?73_1&W3pe)XlB|B*SSO(8}A=q*xOB(lQcW4mJl_udwB42I8ojRTQ@=g`aa>Lcug_g#b^UylWMbD>d}LkYpixD$kNN;rmDQxr9R8z8BUKsb!f`rRei_rcC3)bgM07KTGIS)GCcRV z*^R_^_<&CsElIx9zG3QdviO%zYYWGpNa3^0$g|E;X_rl7{|gCyL^?vH#ndXji|BURync5%Frr!DH{kOW@7_~DKm30G4n z&^VHNN&yBtBYlvtqPW0`I1Ne?`~g2>!J8^8K132|R7gLi}csDE_C# zS^iYu(SPBC{j=VXmJ$lufB%wxDE@(V{D)>DUH_~_|6KoIh5cQR9B;VTnmJpzkTAji zN&l=zKBOx->0b$M|10H_{A&M;KJl;g_}}zH*z>QH`Ijd1>HRB_aA~IgNojxc$>sgm z|0RFRmHt=C{`uU{e+KuZ6#=U)Q&=a*s?gag|%PPFy@ z8#j-HKUd(#Vn*!^{E3Y7-}y=Vr7DPjX5ZXj`d_z^?tAepz|Ns-1gg(RDA0dc{F_fM zk10zzDp4^IPiwRPRr)FROrOAAC zqe8>>7e2s?Z#yNJL&yH+lgqPRi@HiBJNx%}&eU^pRdw_Kqdxy){NLApvYg57CDY{k z^H+p7%yxBw&%TScMbzn}c-#L)llgF$_4cHaDxfeZwW#g8|2Lmp-eyLvm4Vmuzy0~3 z!D~UqxDd$s&8O}>v@kF)-5W1S{bK+Y$Zhg%65-EW-ut0(;Q40DLRAAp=s z)*IycOQy-=nLMt@{Y$3FeB|en>p{*ZmuG&@Cj6sn(_j5`<*2{L?7cUC^#xg;|GWDC zzxO+tCeKfDy~yL4Oq2P@awNBxoKKd=|C9bB>t`|_nI`KE^86*wBQj0qBlj;^&g6V@ zdE|PK`E^z2vn)vQg8zFyxjkh5|5f9k;@|7h-*w^NG+9rQ^%YrfkZJOLT>V?W zk;@~um&{LYA36WOx0_7=uj>E*H}4aBNR|ITzaQPOv-z(E{kQjtfB*TPpZ`eUKN9$l z1pXs||486J68Qgx1paxSz;9FjPxmKg#Q(>B%T5xni?fBLwV8vp;|(4D5~Ta7 z{GIPcuM6X!X23KXqLc;&`p;y&*~h&0oD8R^0#V$ergCDS(+I$NLZ z^*i%`@R9N0mOT>!5&b&)5>zqZt*x|a>z_z`9X-iwtz`)MG`Tj8Yd%C|XM$U5S27$m z`Szg4-~qs=sQbI^0*ET!J<3}L3J9j#i_Y`+BjNe6+VtfIDFg$tXIJo03UPnR36trL zBvN_1J(I)2;4RMvo48gILBl0yM*qnhoRan?e~n0hc3ok=aF+;>xU=is8cQTRUjN|v zI5>`wW=x9NyEBR?GWjKb6pbc2FEE<#dlE^Ua;?pLaLu1sy0-f3Sc3;pryI8o9gZRf z&_Ur_VueKK9m=)KL5alH*@uFhp7-EUNxAgklRVJ6%i&njSU_;mQHaiNk>YebHKeKu zxeNQYbU2X)Unn!{lN$0UAUZi!l%;Nz!S=vkCbc&1#Gy)IHHX7_1jW-|M#pSz;EaSB zw}*-^$zIjRD84wAFk3wIdrHHFNWM%}z27#Jgg-0t`&2;|F-Nt27^ey&&Z=}dVC;^B z=iXb=6p1l}$AOM%FUMqX`9(U6T#FzCmAj2k&13>f?R-K{IEy$?H<<8LCJc1>Clbe= z8xfDR{kOI_Lx|pvMw_+Yu0$Q7s@qr-NUS~Tm)oo7NvN~MHr}m>CCctK32H4ygJQi2 z?iEk~N#p1Q*YlgibAjFGR448cLE8s%OteVyi(37Xcr%GGx%U2mm250gzmLuQ$KhbO zVy0-2mhup6co<(X(~;t|2J$sr+?zod{YfpH>CAxR<$vJ&=Sae~+UF5!j#OT}F+PN0>sBp|Q2;!tX%8#@{tL1A78J$k0B3ao>wO7Pbb1uvARJAZ!Fw`FZx*u zK@fk)#P@AjB9V|gcP^VbiD2Tgx=x|v3W_P7q71Jy;T%_hER}5>@FaqmkJd<^^b;))~DjBMcb&pqKYkE%%ro&z(Nl2-B$rV#lwB8t1FQi$ZF zeTIX~xrA#~LEu1X8XOq4G_?{}BXr!GETvT=2=hu=k3**S;oT$o*Ee?O5{V{~Pb*Ton1;ZT&)ER0N0G$U-r^zcx=?~JTpigv zm04Gh|gPDXPjzz=t9UoP7N!qqX$ z>fK-(P*VijeID>9UbdW*>YdFePJIkfxbZ24uvGk-`^Mje2w3CKW%-jvaF(m^eSVls zvOi(Im~EC$3}o__`b9Z{D_>Ys!A2%*XP?+W_NEhR`ukU0NpaOYzfG!U{tP7^6|&gw zR)~brjTZ5h=!eh|9@;Y-7D?!KJFLz6sS$+%3Ez%Jq=G|G=k?14g3z_HmVU@44i*H5 zY1F=F66Qbi_RRZw5;9Jf!y7Mj2+nRklyWMQpfz#7My;|?|ju1Zr zdBzykJaz@{O_zEKG$_aCMw@-8%LBu109^bN+ zL_*?Dql0B#DdAFVdHnsJ4A^He`}3A~7J45k4pcgy;OY}Qy)!A2(2}OS z?JaQ|)Wetpl_TjjZpqNQ ze(B*>u|Le6Zeg_gb_;fL)Ov~`dBiEphP#V<++oFC)qB5K4E%1=N-f-0}vhS6yxzhZBgAgKRWreYphaApUjnam3c>f`Dh?17NE5(wOTugZ35w z_ku?LK;g#ia+*0ALWfJUb_&G;%JsJA{PhPQ#v&EKx}Ahq_uI7I*(Zwl!=>nfDMk{; zf37@r)q6k`9uAc`W*6xR6S{ZjUe1Pr5)+PHapCa%Y|h0u-YG=R_8RSw ztC7UC#jZG3mMr21`{*BY2`9MpQs9*zUj$?y6?Y#HxC`67zScSQB@hp{A8d=#d_ZjT zag~3kk_Ak3UkZDIVu5Nr+4@C68l)dC>V4ar1D&!m*x{~hqW=1^=iA!yV0b^=0bRUztB>96m(EeD+nCU0wovSYvqgOP%j&1T(*@!+-$JRx^8GP0Ox`OhO_1`15Pu!=e4j>xC8WOE5HPWEfkSNxX7SUAC-x09w^6 z<&^Dlu*b~ShDs*~F4;GJaJ`=l=DDwawTp!lZ)rtImST~x`^Aonw>dJ2;G<^$2Se8x zPh}g%r8gRcB4m{!BdL_6bScS7AuANJXIA#!+p+iFD|;kd$SMhCM^Q$p5GoPg=hOLk zerG(_eP921L)|S79MA1Gem<^@&yM)@?}_n2tNZsZ(o?3uZQBZ4*{`R-?oZO)hYv#_ zy_aeaqu^7BH5FhtzfyrRyiP?+$D@F3`TB` zWDD2T5ImqO>Ttv`7wqRH7=l!ZxkuS98%vXj`_x3Hs^}uo@m${b$8=B8ao`XAV--A*QSyFOx+^M3f9&Vi=lbq=1cJa*RXn<+Ye=wLl9nFqF9_7{^6 z3E_*6uPu$oqK!@>lCDE_`P%$?abe=d*gSzU+ z{%aS(NSc*7-<&59IUexnzl&a=^}#1+!RQLgtQM61^0LRFdukVHg_2O?s(k5AUI^lI ziuE%sG0?{?JnE9=i?a*~x*PW$v9-rz*fAy>1Oy@!T8`*YNkwi#c-^x!Ge+*)h2Y4T= zDuRj5X!^mlwM>-;(MH-jPen3;dc^xi zZcH+)*KzVr*kyuOIQ0Q-L2u+@?Wt5T&xQ-XRCt!&n8HGFP~+TSBnT!i?BPFi4{IiN zZ+?~0gDweHZ7G2ScvE|Da-Y01Zgp6zg-fKt&e_%A%zQ5p-2S=Cggz1(E-$lD5_7OD zDI};zI1NOjKb#q|k3>(-dw;rZ;((g$;DvZTSNv_9-6T8iOXLV>s%lXdgXDzX_xFkJ zAhpsZAiEv~V(+ zdXh2_=X_=5T)Pu+_Yv`%)6CL1(lsf>$KwljY-~y=8AFk5qW7cC>mcA9SS|Mqh{PEZ z+sHZd0ANer2(ODbLd7(*4BqNI?79&i^g1~fSaL6AmXAGxt1dr|+4IEUvf$z<)wcvJ z;9<{dWyyj@x%^1#P&=47x~8J77X@5;UA5Vk`FL&1-#tx(xJQ3eyXn|_51pBG`Av374oyXj{u%R!gyjUoDntY>q*2H6-FO*PbYxm{vVCE|v+-6mKa) zv+Yq<)3MQG-4UqbZ!5#wLX7SDYQ8m>1-Z;~HB;G7&>}lCg<>NFjakqBqr4Lhvbi*Q zt^H}BAJI5a)tiQL(hJLXJ~*P@mf5X}Tg3b=rjbf8$;595N$JmjBp_cBMIqJiAP_z4 zJJ+Wd1IA=5nxFRAfYGM)?69**PuwuXm(r{R?gmT#Syyg<{!b!Y#kCk)-G`Q3L) z6B`IO(EaTuepW|JUUY)SYgfJ6d`Kw7kuoBh?}aK1;ZFtQ-N2UHaR13q8OU|V$Lx1ZBCbhN zK7Bh#LY=7(?A=_Y$U(!Dv^<{+^)7MyKF((Y)#%r|M~QQtT;b`e#hG-#rN%!-!hj1eZBnZ*M9p6bQq3&_sbf-F6+?;0Z0|xb%TM%< zR^79v2oD9pwWr&-Ckno#Q~xmCV}dotQtJ;3Vo>vS)Q~Mn9Gzp;zeRtDz}(2ixD$^< z@yl#EyR%^?{*H=gttNUl?Y_$PPTlmt#hvF%@-+!S9y!QySlS-qo>7$BO;zFT;Q&4P z<3W&r!fI}Ru0I@%{g!5un*+>}^8b;m8AIROsI!Mmi8()bA)s{01Rc*PeTv@*#osTf zw0511h7wV|=KR2HV4~DL)MZixlzX%tLk3d7xp&{+13G?iGDLh~pT8?KX-a>4Qk8^R zT^GKJe@g^Xi&)q-zkE39n>y$!RD#1dWiRP}GyvcEP8&b6Ld+;krqLF)hxX0NyXnl) zSoyYP)p*bwq~G;^zw;{&RJ(t2rhEB=^oz<7UFSlqAEV==PbtNN13zw#{~~&k`+lk3 zOHxO{o#)l}(ycMAFI;=`rWan9S1=3oi$+h8^f!HF32?M)?7>EHD#ofgSTOYZ0A=T% z%D}yj@Vc^vHAK=EcuE=WovO5hN8ddu%@P~IZ5crg;z@2Ln1>Db~CDY-pPVFBo4=Y8CW?+Ix9-2UjE8;IKmtV(LE z#2&!b#qq%}1a2OjF-w~$$BCZfi)L*uNaYgiu0;6wD;?`Xn>g(-l-Fl+$R!nT?bYZ# zu_u&4LY94W)6c<%n&4m0<{k`?0@z8(llLB+|6G%;T zh$J^o!%2%*r(X?aVEsc{xkOD{4BT@sD>#;-Cpcg}rC1g z9k`a?EoqMR?c8dD{c@1CSQF8&R|P3_8E=#BhrkS1VpYsS09ff3@VJ-fp{Gu33@5sP z?tRYVoI5_Kd}dFZ=lL@zlBKIOYU+pc*Q}U5PejAZa2ofGzC4VkE@KZj*9Tv%Z7o+$ z2Y5XD(&CS&AGoZanZ78H2ur^v&)qvv1oRz8bIc^eaj-VgihjihSUmq(V^bt z#FwK6=lt~QX=CEPFU<8TnS^rD+1}xcBp?&gD+}f&zW?hAW8P$5=+15tzqKm}{E5PW zA+2mopJM2UnXZ7lTz$KZsWX8-i^lM(N)m82DqG%g_5&5=F7eJyUzjlrF!}q!7I&A8 z%*cH6!bxca_&8K~QbjouKXF@bUmUf@QndCMeOiEfZ#-2F^ee?K)-ek5xqOH} zBju)&Jaa9St!%iUCLK~wZi|SIBtrB^zfk()C~)KD(JTI&27672ss+z{z~>+3DNm<> zrrq_s5&KBk^1P$%uSGn*P|A@w<(Y+c7atuy^wNjOD^z5@V2}XyS>s?MWe+-1jk}fA zBjEdg>^+?8K{!NhFhU`g56;I=ANVD01f1{YX}B&Y!E=E-Q+D&;%TliRmMa8c(1b0ppny{0+ zv50Z+#x((JoHy>ixz|1u|8X0uyK*OC47K<1!t?;tq>i2A7R-d1OJd9SEW)tA*=v25 zNGR;_r8v>!6$e^%iANll95Ceu`A`jA2}FxBjX6og;+Lm_+trq_P$@oOpz*{D|FgRi zX#Ben{AHrnkShW1bk^5Wy7^$(!Wj_$5{og$_M7DxJGO9o+1&#Cn(}$dhE?IL(Y6xT?tC!)w{tXgPa4v- z{#no`%fJ-|t6iQ$IUw|9|GWl|0X#0P7&(2-2L45>Rc~C$!0|gwd+$B=fH(R{rVM;R zsQz#F2WAWc#=bFq2~85}Wh!Yawt2&q`g%BbP6_qO={|()^8y7+d)0dPd~o$A`#bcq z1jyoBMKh8^G4xlu+_`JXNWONHeznC83^Ifz)~QIid_r?%bvqifXd@Y=k2%A$6HNo1 zIV9w+Qqq;!gqhOifrf`uy}fi0>gHyQh{_ma`Srp(TA~5qu=1JiqG=sC+2=Fs z_xOR;RXrBV4I_}eD<-GxB@3~Z-_#`16k$hl043&9h#VBj@udUasHWlWc2mF^KAO+0 zDDO#vPtE)*ZcVoFcE0#z`+ zh|ufZt(ySpI|KgnO;PZN)ogprCJY;!qC);unL^I9z2ml-DNuPYDA#br46StI7M$gX z`TOPFUWrSQIC-kJEcZNdcH5nP_fa|@)?^GDgG%aONF_gWDJ162la-| znPfsqE~5?i`9$3FV?`>bF#+qX+A~qm9zTU}&st=r!zY;uM|z7SFcLdo;o|9r-{ikU z?Dez3&s?Z|r92N#{zw5eD;9PB5G&yOI3nmZl|i7&dm?_Q z+`yCF>>N&cG1yMY`1|1X5NHz3Tw6Pxi*tFen;Ezhv25&+HYb@QZZS5=m;Xx#{&FFQ zKvxn@)kwIe6ck`|p_lbsW(J<#IvPpzfB=UfW7~0?D9mx*s#o7_3qBIAUWNy=A;M9k zKxOC=tebd^T>fbVRJqRUic;42vUOP{=tm^3Gflhj9nZz4!Jq>tq!U46^Wx%u=X_|q zf9Wjug+!#UeWtfJFoy73ZYcaQNx@(5srXwgf`Dw~$$~fc706f^HF;T{4~OHDxkF9; zLCMv}%#GCoe}&)J-7!u=mCA3sgW3|nVUOR+rId$A(pOWjUvj}v7h$B)w}pjz({=HN zFu3($>F93OEO^UJp&L=1jX#(!b242Dg+U8`<|F&EaWnDaX}ev~#J$tw&Gq9{G@X`=y>L zCOw>#57m7SqLczP>X!*l_Pt+g! zp}=s}A6+jv#}28-1JBKdS@WbkWLE89b*_(r_K51c5knrx7%Cxr-k->mx;~R|_MI>G zi**FOQGJBnSEShlGl;!^P=hY_pD`-N*c9*JJxmR?9d0D|g-)~M#`Ahv$XC83A*$?za+0~yu_o3aGbZpzBQ^)1M4F|^ISLLx ztgoVH6@fQa|LsX*vxf|7s^m*&;!yv-!?L+{3@S*&2+7hCIh5pHB(H~~dK>P(;ggO< z9Tg&v`n@qBv#275n71O0%SZBzjUoE#@z;m%2f$Co@H1chNKmxzw8i`TwkY1b`TSm~ zI~-x?xa2_*2P&JaL;!mR4Ek-ha!&`t7;A=9IZqxMWf{8cNJWB($3&%^v@Nu zBH(FnK>}K)VYU8>I8e99Tz0a_gS&+uuYbvU!NrTAxB9e`klI=Bzm(oQ43v^Q8+IZT z9_y|b?WOR>1I8D4T}z|UMo`sYqu39QJ>@#OH$NKQx<0LuCVJ)9F3t7fzJ$U zfiHRoQHxblr-KRiJFfUwHlXEVt2-wUi7yWEcUetm;awVQ-CKNNcvaGQVURu=cZ)cw zvltZsk6ER0>`E?lbN~H&(J2P~W9K%wlp|sL{`OY~mI5@j_%3Qu<%E4+&3Vyo!I1mQ z|7*2X4hDC;exw~qLK*)G@>?qjFclh?Hu^RmN!Pg?Dtjp((sV3@!gebJ7iZYE4KI^ zhfZ@Eikrr9F!gfi$v1N!co9!-aGur*GH9~h5(r#{@6-)C_n%pK!B*v3`OhLeT%A>1 zMC}B2otY&z2BnywP{VtPB^D0oPGp&%_5&WR?=C57Metwv?5H?Z5uDEY{d0X;2me%^ z&1w2!hkY62m%ggF1J~O2PwHu1Jod1k^PqqfF#U8{icauEtBTqu5hr}Hg}nD9@4aXo zOijx8xGNf?ngZM$vx7iWu=Lnz7d<#O`|+7IQ?1^3vM`e=~a^cBm-dHK-Xegke%Ncm!hUD&+>m(5 z1-E`>pJL4P!mjEXi6=Tlj>Eh5|L$z%qF8!ZNOH9YOds6yD$m~^@9b9pJgwe{KidDh zD9HE*^B?L~ZEO$X%tDoOan3MOsgl>tyS_sqo}NDnv7?v+KZ5EKM)AOHY7x1t5%f0s zy7WeF7!A0Z_jwNVqekmW_WoM~_~&9K$!tCWl!j$FLz%Y0X(}7@HWUD{KD^&L7D zw$}v~{dkd;+q7rs6^>oIRY-mACDzex3~0x8BDaW$hV9ZzRH3}uycGBf&jlE%9Z&1T z&NtaPr^`F=Ekl3Nddv${45VFa6nsJOz+JQl#JjN8a`ZOug$^X+uT~S2ZNRquOFb8( z>QQ>(%bUiV4S4hLDqG-#He{JG6jzXLL*J>57K7FXq`H5Q!I`QS7qy}lUtg@l@bN>n z<%+dPF`>#*##W2F3=IM;0vhqH?t|5WoGdid=okr~tj4Leu5Op_2u5Rf%b@bbP(m}B$?{GN3iF}E=!NPQJn7gY+=_N z!Kry&^@@^FEPb)MQMx>Wl$wi!_IpM#y|K|CGH@8jVyi0J{*9pB!?wxSbwe0^fBGzs zeuqzuGBW96hmd2tkAD8jTTK4;=7Ul~KfWo8vF~LMF&`i=eHtZ8JX1A`U4p z(jVBbpEDG5M(R?9JL%l%LL1tBY^IZ6EcV#-ds*wm(5vp45UDbH1otldHlz z1Cc3-ThDRO>0}$rYCSHdQYG`etip~NmveX2F`Dij)9B4Zo%2kA94xvvvtnOoJ{?|7R zx+E+*Rn$ZKyb=>+tmZ4JeKG4y^Y7!!Y1lFBSYB>Wj>kW$#%PPjVf7`s-o|PhG}1~D z?QhG#{jJ$)6tsD0UNm>b-Z&o(et7=lp(l6|TX$*>{wO@N?&u1-e|}_Qd`O&4-3lg&h-x15&E;f$>m6EaQw(sBYP?rDe^Q*D)hZjKfKjXn33sd#aj?V~*D@NK;Q@ZrKYq9^&qr(=R_eQB_m zdwBEGnOrowm-5rJy%3I9@t)^+6o%ym#tuiTWl`Zv_K8Oi9>U3um)8{i(x52uEZKNx z9`=mZPCXD#gOy7$GY)(5sxni{@Yilmi2k|bK zJ<1&^@|SCNbGu=}Qb6e3rZ3(wqq#7_77tcetiRC5c|#AGT+SN5Ct60nndqAi$4Ds$ zhmGkdIBOZ5`QT?5uwNkC+OU!V(riYBA`;eLg^?}i$`&MG%_CF@>qQ`OYor@`oayAe8HTj<~sc1mq1;@zxPOJGng1{T;ho@c68einS^JTIf_7)9jcce>=y*y7}8U-_#} zF);hu_N|<17)&XN{ZRCd#D+GtqMjwkDfJa{c8 z=Br?n1&2kql_F`ZP^ww#ywsORIIs1Qyja2(=KXk6tj=dcj_vi_?5QFQEHkD4xE723 zOy<{z6}&Ku=&hWwRKR0qjj|!PGSKZhuX)_4FRFj0QmPsW#njgohPjhrNTZ@q&(o=Z zyZ3Tst42lw-_J{8HLUJHTAt-vCGwY!)|4D!c@~ap(ULB~_cict)WiPY76iYiRod~O zJO$bROzF7nkAwoDh3Bik!XRrPFa8428)$d&yw_k_FwSy>@!ctSg2QhA*dB5@AZ6v* zAJNiDXeC~r8z3Bufez2*6`x@(kXa;_-8?P(o+zPaM)=zrsR^x^zN;Fi+;Bx@9hHl@{^h6j9bP40kh zyS6K)P+AW@H1mV+>e)(JHVOFU`FCGtxs7hJ8!(O|JCo8p9nlASSn-C!4X4N zR*2TEa!8+`dGa^N14OgAJj}mcLm5%ar_~$8{*v4*bI8^Js`tlHeHrt`*V)^JBZ;vP z;VLoZ>`64F(u{~=A1y_WBbpJ~O)Wo#pJLM(AA_7&Jnpj|8%*fudVMEVg+4?>(#n%(XD$3-L{tCo*`7?KZ?D__ zHV_ZtM_SBS*m9AkV5haZ#0IO_lXn`dV$daXe`g(Q29(;h>U=&N4hBVC`%lIv;#-~J z-p*QI$hGZ$U*%SWrL<`C=cyCW8=QI_O-%wVu_-cD)dXPO9e?+&H3_ra&a%;e%0XVM zj0}gi9AMpAJwhtZL#{9KWKVJ|VL7U~kzdvl+u4-70)ECp50%-2(CJugXZ&WXF_#T) zGu#f7EWR*#!D&;OD+*O)w41X^lHqSp;2onAHgK21X?~Zp5`1wp?r;hU03A98-V?I< zaCFErL)AD0q=Q75y_1SSe<+vfQ-VJPvNuaf1=&G}!pZLhv7}XmVkTC1CBH_(HFIxU0fO%&q3`u~rA2t-T9RA3ojaUlUvn(9 zNyCeR@_z~qL$Snw>Y=@C2s%3aY)*Mf=p;}2Y&^@)06sbSNcP{BsI%Mgcv3(;L-=_o?Ut~Ws4TeHX zvSSUqe-g6Dr8CTv=R$Am;iHFh%Z(NCs#EZ@@J}Nrr2JR4S=heNsL9k1v5|a0KhFw1grP&3_L6UD1J19tvG% z@=(0d`^W8UjXyf~UpT*a(-ymPizq3ajIo<9vXW!M59tqit1Rgi0=@PK+wH;t_^3jk zHSA%9vga$)7YSU>p+fYh>^=f#WQ`Uhk1B%Kdn(PDwG!anG4YeB1=q14!>;GtP6m=s zdz{<VSZaeyPqbCz!6MzOyJ!=v@u^r#Jb+AdKZ^A^U%M_+>m|+5UbZ zl+Q}WOqAP#>EiAyUaM&k`YE|;;a?iC72lVzf13lB#FY!a2|pxxDXDVgRm{c14wj2Vwd8oxQ0Imf$qodnJ=S3PW;z0+h~2;hNts z?xe@5`1X~llv6_*9_^-CH-s>F?0bmstWX++RUSA&mZyY?27m6%RQkgVqYR4=@h_{t z{pj3ZKUavU<6Gs?u*ddU=H7JabmE@%&v8OA6Va5kbGIuB3_DZxJIVdQ`b@x1cAFJ8 zE>==p)9^r+ZOQ08=2l3VPftI4q5^wc74EXiIw8m6opF)P0?55%l3YpfkfZlH`GW4n zz=c%)V7|+t;3C7}PJbo<2Va@61O`T<-oJ*@O#UoT+vZf~vGssJ4GuL$>1cRXW+Qy! zcn*@kn!Yi&;epqfJ@aXL;?X-@mWfB#0j~3Er@o@}24!9&pHBN+q)oFcD5gk;m`BfZ zf8N$ZCNAy+o;0bzDHZ(2C{dQcVbU9ay$yi^S+VuH*B1ChnvJ!r#19#_j{VuS6b|(? zCv0280PrQd79DA_o(9iT5 znVnLS`P<$gbwkQBTU|G%JL!czSn0)?wr?yZ>^(TH)>`nlz5{*wTmLFueTl+3MTaEklP5w5qKf~4!oI^a~tr&jc8dE5-SGb?6TR)pvi2s#etE?pOtoA1Z?B_q# zp#A>lML*X{Y}hl#+~Qk`%p>v?sgiY=CwAC&%O)BpIK0Wvf38KNTDk~Z*<2J-GSQkN z_>*JD`lHzF)$wYTy<%i`I^N!-6SKNm@D20#MvVNqGFKP;hJyZ|XseZt5_lLgxzds&08pr7;vxlg1M{(e!z;MOSQ9Sm!Vp58J5V@x)>uxy? z;duM&YZuOqp+}VLwvxd+tj#eDe0uF2YX6-niBfxq;jhm3Uru?0{H@i$_RPP;4ADoy zK{@@%?l^v+&a4-yJV;^Z1beaQvop*4y*;=x=@$IrTnCO{y!MmvO*<8SZKwdmwl zWb7kbFJNm!%Q0tL*JIC6w!Yur^KAjTXg6)vwbkLH36>&q@dnh(NPI^7umU~3j2P@M zH6Y!tykTqqY8*~_uJLfY8Y@lo&s9s-pl%>ndQ5%=o?;g&IrgatkEV+3O{ob*E-Qwe zlmUz4uC>nHm5Et%WSTYcm1vnw zcPM%-1n3LrzxWi8kX+X0nTJ(2zP1j2@g*<{XZI%@knPAu`*PF1iTk-&>8S8^;429W zzRq1aaMv1-+W07GKa9eM`)0DcvWs!w0VnY{x0CU5iC&%FZBJBwK&i5P%MkT@G#2Fo zg7FPrDvNm&jT>6$j|8|!f!$KHvSma%YAO2Ino781=6Wo{f>{`5->sXLJ(h!M`fN8& zPLnWwUi5)zZYburh@}?xyzXYCQ#pfqd7S= z9*4Kce%y61`2l|Le(?aq^06f3E7nkS zA@uMU7DW$OJQs#XV&?C%??1xc2Ug8zI@+xwy`8MQ8kB0?ZFs zyr1qQp-a41zK(@I;&#RZ0gX(2Vz=9h?^qZbv=*n|l8=SduXm26GsuCaHFc;1p?fZ1 z=RSO}FAfq8F}@yi41^V~D(2sk!O+kc#7q0U6lWD2MQ=g3USQt+9Hw<0Ms5^p+E{miwp#yZ71<=^Bspy*tYX2?;2OU0ZH zW!favYJUHFN>~}j@sCfXQ7~%lml7!>3k7Y~(aFvwFEEe4zNymYhez73&^T*kL6gg| zGnDD6z!qyFaL`X3?#ixpo2z?Z(e_rLo=_-UkZfzYLG<01eGE8nGZu;EPgYG>?uNsE zcitgtf$+ zVLJI(ePC7s(w`IBp)Du)Tm835hO;4%wtPmuNrccTjke2c#|px}cJa9W9}f7kpy$kI z+Bp1=hOAI+(;qw~Kj_}iCj60MqZhQ+$}oSB@wPWZ6oRzYUB>Mo`1dntroS&53+%tR ziIqk`yW5!6d$K4TbjkR;Thutwv8>~6~JAHjG*N8F9IBN_T-hhI4!%*B_F zX-)WO4DiX)zq&KyQ79llHp>$m1=#`uDxZB7a4PG_bmoo~+}>2XKJeQVr~Nk0N&1i= z`dNP6yCxF2f9o&z9`MF|-#0@g*;z=QRW@_=S};EM|DM+&69|qfrET$(8StczCa!^w z@C7Kctn(&1!+c4Z`+dxY^vcWcKNe?zXwO{<*$r1zza>-fPwg7|eidlXC47;Njvdn} ze+b>IcDif5P8TW&S04*H@(y*=l%o^k=i1TjkGuwT=z9Ia3 zabZ%)?HvYCeQxiT8bn62Xg;Yk11QU7&tMWXfZska1-jhq!|9C2*A4W0@v+#{quJzM zTp1{{8D{NA$BH%AF4aCr z+L79qY>Mh<8=6o3J_s5F@6qr7RmiRlx7zLrYG-v|km?qdkYG0sKYkecIIs=l{+w{x z$6Sjeb>*IV`VF{|JIRq&){b@xpC>21pP}!wGKV^m227oCd#gP592;Bd7r3AlyAQF; z_mos&JIVFv+`cL#Ia00ujEcqG>91BIE9>wAL*&alKDo$HsP%hHzZ{c2{ws8k&cMw9 zk=ZM?g*e}E<6NvzDZcq}SE+xn60a0}yz{}X6fLv5-dRdlVpg`sl|9LoI2>Vilya#E zUEZv=`;t}Tx}`_c{?Da&RO8yS!+F{Gs?GLOhe{4+SOjEp2-#w1SBAlhUj+UbJaKTM zqY!TmtA?H3myhmkzX~|pvymyO=Q+1d8tx6a`Q}e)9LTe$Pz^OkppM*m+xQ|yTw}N{ zU1#NnRB9?e+^AeJ@W}P{ffCFtgAKwr;;hQd73xdR6@Ix=W?Wb`Do|^~_xuDe?$GokkOTS>pW z>B!0>&HK$J6U_r=^Bn0OfQq5R+Bu@<$DM36G089v{z&%Z6fIgn=fRXO-YJ>5y=SE` zQ7scU57wtWP>+NZ<%RT1w6Wk~o^N}W?zf5?g8cey-gWo#Qtw|^06bO3uaUve)3l| z5eDB$as~CKgI3x8$C|uhxVn#(L-9lgdcQL`x$lG@e97a}b(=^+-xa}fS;2f%Q#dyf zshtEPpHqc(g|i60wdByr_l3YdIrV$8+Xqf${f^%5Ho*Ar(I>x-L<9fZ2LW4Rx$u}V zJvD(n4$k&hxIPmo2gjR>hFjZ-SgsZ!qpf8F)A2Xny_t)`3;%^akRoygAWG_-cta>(}f-w_D2eVxh z^R67JD~Q5&@E~R=T-9C;Za$h zxfH)7v^#7}ZYy91Pac|Ib>oddW2+CxAMht)hzsB6Yw2+Y*?5P1Y=sWW$}^(5h4Q|fa*7!EB0t0P6# zi4f|nV^K~Oi5>TTTpA?wxX0$_jucDefOO1pi4*xGICyOHX}=zEU;3#b_j$_*WqrX# zqo^L6|NIyvb(4@k@%%&38!Ln(;l7)iR8em{ z#v~Z^Rq7^U{`-SnP2CaT(d%1kv5}6*_>lH}Y9=NypDXnf^ubcyfgRHrE2zEpg3ZG` z0q!gBsOs!7f}N!;_ukxa92rv5_4xM)lnw{|Ae)HnyhaGZBQJzq60suqExmogwwW+-eJjFV${&h<7Mb}=WMbQ} zsqAIL2;g+u2z>le3V!@yc-Q~k8SEYuU-n!ICGr#47R4vB(3$PT-u%rFU{kWC-*`^= zW9dk<V@TvIrN5kz z0ncq0Hf|AkXUBiBF-JySpx5Z1x!yt=-pyp1xwjpTYZVQGIXA;`qN6cn-}MNrO?=ba zbuk*h9p5YKtZ^AtCXFsNSK5Gs@war_7yb~Ls`%QrB?Df#+wIBP$isdd3gjwGCwQMA z?%rKl$fkIw&oYSky*w^Noq1w_!YZ1bhuEVb^Mu^uD~SNmAIW_FEGP*lgO+x&4-_Ez zgYRB%gQ6iQ=i2eDlOz=RlA{?m)`06dA4kP0TXEYhW+{TeQP^Acr)G3tqW!N&<_FUH z(O=QzH_h}bG%5CWFuKr>bG=G+d+)zSA^)cgV`uvCs_gO|E1n)C@vuC)x$_d`&g*~a zw|ax~8GAj0w_hSz*Okaiy)W>Ez7r7&+oB(ux-?qP)Pl4IO)z^!9hwV45n> zQu((bKXbBtBXv2-`XzmH8EVF~<(;Pd15LPJlzX>{auZfoxkh_PG$P&Yek(2UW(<70 zVe*1H0T;B?`>w{fVagJ{etu0g2A-m#)#QGT+z)j28(LT6(T8u2u{b_M@xB|MqP|sP zVv+=jl{^oSs7lnFxz+e{I3GRxPw6?^1>i$k=DmmP z>e1UL!8G`58S2P?8NcIShF&uH+SwLSn8e)PRy1FV17kZ*8%l(aDDlRoR!{~0ijl0b zOU=fsUK&R!%n4ucehF6p(pYqCwz7J9+!HVNWDMlTM+2RUhTn6UY#a` zsl6kLk=5kU@)rn1_0~13vUv}f^G=C=!W{sft0H%OYUI(gXzYYihd8d*Q7L>&%OG@B zEq}b|;*l&lf}6Wa0M!&3SM_4OkZHi})8may)G+S*e3~m0EvQ-yMo|5s3#YC|F_ zecOprna{@VsBd+RnVBfvC>Uf)=8N)P*PBnpL}78N3|&=17zECXH@wnN$F_*D9yjB1 zBx=qbxx$km$xfK>Qi>ZW+)tc%qn-j&r>E(qe>kGz#1EAbf)`)y;a}AK8UxKXm6z(9 z37=u3&qq_!1fn-2J)u$G2QPo=R*HF>hTitqY-w)V?e1Gg^U_sCcF$EMm9**>`d z;Ap9^`sWpiyy;m#?oe1E&w_??=nmn3a!;0)7|KLO@#ssLbc;5D2iD*cC_00-3mmZ}TKVpU3o~ys^g+<80dxSg_%~7@RB?ee3MK>DZA?M zY1c@s_RxxMJRF7^J}txBy#-i5@}u5ki^#zlPn!r#DFUND{%*y%dsxfQ_u=5664dd< z`!*Wb0>h&gn$vxmP`B(@{h00n4k$M6i6wl@9Q#Bj9mZVI!0OUo*S1U;?$>9GV0eND zB6(Q#Bod%ozvT9}$4+1*zb-rAN${DIrx<_MWdYNz9v9y7T=*g-{++8U4i!7RS9!~$ z;6Fh{%fbqKv|x=3e8iuQ40b0+$gh>7vUS8+;qFA>$$K6uOz4hsZ+o|p1zCYkm||99 zP%g|dy_bKn7>5EmUqqcEwScVf-_CW$Fw8A_aNXln8oIL1-T1f@2(Ry4)7GOa!rZ0c zWfhY`v^0yi>-nz$@9~@7FE_M>sgM6tbe@4!{(TrnG9poCOG8GAQc6ZYGP7kSBzy0@ z_ukI2_ujjZ@wK;X%FdsnMF>fH?&qcR0_UFJab2IQar-VG=8LW?S(D2NM;eA_3c}v! zfr)ia$vz|^fiJn)_k8`qzLum_wJ;a4Xbv;XGsL2|atAR6G&v|n`rxNCb0Xs5sI-_4 zvp^l0?8Z4RcCh7mK{dw=<01Kdse2-54Ga2b%8YCC(eXwHg#wE&G>!eDyQf(Nd?ez| zm#OoRbGqNpM@=TfKi94I`h0kF6xQWCdJqfs z6dE(~*nKkRfBmD(`evd(5q*WN*oe=Xn)df zG93}f&r}kz27?d(wGHW=9H3hzd!0QS1Qj=UX=~LJVgB)P@Zd@seE%bVT>rurSX?*# ziRF{g1tx}PHWe{w$M;Q$kx)KRwR{*j4$6kS7}au-yii~^W%q5;_QP@04cmI0zl4>Oi>}Uvfe3}7xQkZ{UXbvZ(Re4&j zCZl<8#W?&4JSZEVY1o{PLxvnl3txHk5c>_^Qj5-F6l|{^=02W-e%9Ap7!0|fa5@G4 zAnbZjvg}a{vcr7;PGYhNhpA|JV@`8a(-*Cox;o#abpSIiU#b$UKXyoLI$1563s3%P z|9k!hmQC_Bace_m>TIr^eX!;R{^1>&gaf4i>`Im(L zmE6}}^oT)?slT^!n1ey;G~WJIdmwx{!96z_UWoWN2D`X5;~|7Jwx8Dyj|Q%ty_!Cd zfkX?<0te}_`PqDdaFJUj`l?s?ZgCJylnf+B}<}S*TU{Im5@#fJp zLXRK6qT9V5he)?gs9V+K&=cN=R=WWyApVn{z>z`%mLBCh4Ptt{U-z=Sp6g-#5mlIa z+!f4s&+62JD@lR7y+0|Z`?Jv;OW0#4O;rgc?lT>gR+4@8$jLqLUT6Ed&xuNj_ zwd&)MWaO`=KCVv_gZln^tD$)^3SD|nPbr@85*EGio#{=vK*^`?L8jmXPd?C@y9K18 zVDqos5)h8g8o%aE&-H<{l9mm*K7aUkuM0<;iOtzdB|Xo}7DKws#h3na5zsr4|H)=M z3v$SbBDasjAxs(fc09!eb^kf?mc;s+XFvL|y!%L~?&>xoK5vI4P0H3^3S^*`3Z{+K zsvN{FoS^?v&K2Z1)9A`{OW@{?vF#H0_X3&C&KL<&?qa- zb2^1Yc-HIx>I{PmX@-A@p-7 z-C^An4Y3E_exqT4RJi_m=B>v-Zq5nT4>GanXOg$30(Ca%i*4DP4q|($$)OGApQ&j5 zl5fUlYCJk^ZEIq&Uyjr&a~{wxCV`nuZeGkp3i698-}&JljTFe~n5D7f!ej8*VZS&X zD(P7=A2->ci=3ZKLrGr2f6@f*DgkkD7_s8L{45qM>eu!0&f#cqVd@osq%D}tUKo7g zm4YU|&OWjp(?kD}y2_oy>fKB9jJLLx+~M`o%`c7I(a2#SK7-*7L&_=~>o zAx?2IaP^JltJAEQuj-{vwPH^;{Cw%ho0{m1LNrpn3iR>dnUQI!>w|-TRaYIl`SZ}7 z>+OGDVZL~0+U7ZbTms+_TjiygUI^?e4^`|NhySXaRDLQ?z^1J#Wun0ch*MkAr`Vi_ z!gVH@#pMrBJfrY%^Y1Ju37mb*iF*&ZlHz;B?`OcVG}!BA*9TxduhT#wG6lxxllnD8 zXMou0so_)kDPWT#Sbk|Z10_eV+nMi8K+_4QhbJFRKwf6+H?}X6!1jqRJAz{f7>kI1 zUSJ-Dr3cJALkTK+s>B5pKhFxZg|2J^5q+L>#Ns3+h_I&c+m)?At)TUoO=3Z9Q-c zQHyC@Y6n7l_HsGqI*^Z+PhR-Z00WG=a(`9d!jIeXZgov>!D(t=X^gX8rnB7;+6s?-?VXN7i=aZ0({N&~1T=#`CMS6n zf@=HeuDCmeAl5%p76DoCq59ylZAd2EcgVf}_9^zhC2z~NyjcoX9Atl2+mgWNGJNUy z5du{IC5fIh$%EKsS?liO1Ze-a`iBMO!9}G;3A*_ru*jGq78=0%yZ=Od&Y|asUNS#S zj3ExBXms>%oi&3!!K1P%t17q{xFf75Rt$?{7c=UbtDvIm<*jb4ZmmCehe>xd6Jq8$ zxD@9K;K@RnRWL3HCDF|Bv}vTn#-HaE4ACh-A~t5h{TKV3^Wq|TS^?|BLag7A{?#sn`9b4jx!1e=;i>xOb?O2;piRWP^37(U&-d+jyf;J zsYpB|)f)tAuqVR%m_N^Tr9$8+{!gT1+L?8}bx~8V)B( zI&jKF0YxoI;oO+tZ0#aJ=9yeHu4jMW2-C5vU1fP!c`paDI=muPa{_@k)r9btWFCx& zW@iX9W7nn7t=5qVd(e7ImypGh15Qy9B_E&&SzdoNV*Lt_h=tuhB&=H@XP!j+qn9sH z!O7`!i;rSJ_)uaoCOQH>RrqQVFy(^mRCdG*%P`b&(C9Q}ua9O)j2O6lF%ICeveCT_ zZ0_)Bx~f(==Ps zO9{}yJR+NaJ`Jf>XWrNT>x(EF_vG&5x=MAdNN z=D2ARa((W4{^B1)6g5%YLNy))*L;;i_EgPb?N^SVZ(=m;k?(2!SE>tPy+!CTSvV5D zrd+!r=zs)*Urad+;*sybL;d;YEEHhalMvRH3b;?#8UBUG!3h`MFB~39h^(LAP$9_y ztceO1ciXd|gPTf(OTZg-B-D!sWkZVxsLu9v%-?a>VblK{3& zKjhb+Ahq-{19jy^d3<&XK=o;t=A;H=vG0E*7d4s%U*Gs{$oT}Lx?7D5Ug55&@x0@a z?KOMEK-+g`!o(T*8ST2gbj6`gMw9v1e7-Pppqu@YEC3So{2wycKSz}Fc$@)A78IJZ z-x!^ZMouP0Tn*Ts!K%+xc}CC&)L#{~=a0vM?3PVQL$(7tf1Z-tG%pi70{ze$mO8TbBQ(y|K$sp5tJrW5}IVW7e zbTc-u-nG+v4Ex$rSRI;u*g%@&h(}s9u|o>G*t};poM}1-)1T}g&4^54 z`s5`+&FSzwv}Nh)u3Vpv<_)t{#p5y|Rrz_`ir^EN-;ZS07R95~l}jG&*d9P3J5VF_ zVJ_U!`ziO4)e01BPh|?-#QdCzMk@r1!NB47pVa0pe?%;=pwK#!0*Uh4D%f)u!e7{J z>;)LW!9P}wHw$@C;h~?R+>wQ1`ejV7Tv0)j5`D?*6dJJnQbl?S(@8ARR8IaPeub1p zep6i%%tbf;8~yuyC=is&+q%A(vs7Vm#2Y+m2h(GciDGOQ$<=CLe~xdLgXs-~IDAd5y^wWa zi%1;CSB+Ocu_cSmfhY|p-Q@^_VDVnl>0yo>kdd4jF=_IFV6D|}yJWdAd%K~uE)L_v zo#9qFnU;v8(j}T@#KOS0^$rE|`E20Xd*#cORR(=oYS!BUe(1jY=T*ulHn2pw5w+S~VpPPdAAJ%69-vE8| z>R29Hd9=e&x|ENSX>5P|It+xF)eRH3AL+=(=y{6zN;I%rl`*xrWBhIF+FRcw9AJwj z=Ep!A9-)kl+V;2c7!UlOn?htEdaL4Z-4Pg#Sm_#DxeT#7mP$*i1d`De*UmQ$YYA}b zq07n39w|tUqRidsOBj-=Ex9dG7>gpTYpDIogOG8au(Y~+9CR+z-T3U_j~stKeRJ6~5+b zfg)J+p9{`mdX3ww()1oUhVfRTk3zHZTl;4uOker9TYe!`aVVb$)8dWys>{XHJYC=*Xy`SmX=u zd&dm!gNKoyJ@Pv&&;uF#vx#8l1 z+(tPVR{X)1-K}kTo9|>C@x-rWOF6AWtgr zC;(>N8b?0V2B4J9QTI;^n18A;TY(1CxhZZj{}LK1fh|T;rCaB`&=#K8>d8tzY8&^h zZTuMsQ)&hG?Sm45myz%qZru&bfBx*2MG=p9ZRTW$cD@Ccy4gDcoj+SH5hn2OJZNl|bq~GQ7c``K~h)GiJ`AVgN=&=-ick_L6K{k9Bj^cvxXq#OkJCfB84Om?1C}xfX&aJX? zLT5j?V`1|K$D51v4VxE&9%H^)Q@!%DdN{cD(-tarvtc&MlrWZD8{IM}S(Iw^0o>RN z(lo+c#J5D`Q9)gXMq&r0XRE_7zJlX-hxtl)p>CJ6?CuXsQT94=(p7Le^Wlz)QW(tE z);rw)9R+W#7)pxloPbi4QE6QUtFLuGi1&H=AfbQlzK58sv55HMe&Pd+#6 zg}RRokG_|tp)O(Dk2f{(NHV;*^2DhWbcN8jcA&LrKGv*xXX8id^7(&J1Ab#^pHfG#qqV!Y z>JPZJlS6di7@Ke zWqF;^7{+A$#H^+Dz%J=a6LGCQlxle9wB;wDh^^C=r?oIY36-t!IgBSY4=n((N>8IHj=DWfB8yG(j8x@mRt8lg?fo0j|IjsG@y16NLu09NS~$ z^3c0|mxA$eJZvbrdy)icAok&o(1)oB@Ga^6YOGic44tukc&w5HdhWj@42Q$fUA`~& z9Z$1?{ZQe>&qr84Czm~R2h%%UPI3}@@FEttqi8xb9wb3|iXUYGrhgLSn58_Wn}c&-Din!2?VA7TnMgLLTx|xbNf4^ zkru1jvI(&-$co9_c%K=C*vIOQIi3_D!7Zh}51zHC<8%9M(|cHsON;l5T2zddhxbmC zUrvPm!NS^b)f5zY>rW;r=5vclDLGt|!=86#R7`QnsW77PnYK*X8=1Sb&ahNwA(d^4 zW=Sl!AE~zNq2bZV!sPNaWUzkr zc@f?f4Lp9CZZr{y*yNU`)1DWiBhAshgT73l{`)xVRYMqhPjk#wtdRhlJ6`|f@VbD+ z-2athb*F&QwSqi?ETAISx>$?dPkOt$>`ptGQ1>zPLJ*@3GP_U^sOWEnA_B=s^!!bM zMXi@DaK0G!|9$?hYKw;wTwmn9__6pquTWa-^BEC`x9olN=ij#quB|dcSoi9+D zljp}<1*6;jVmm)1Bf;_X_`(W9Dq`qun(jQ23hPYtE+4@Q7+vT`#sZ5`lK;ThKmO@( zpNwHL+x)ry@f&> z686|9ZD8j&^UFXk7p}oS{$TNT(23#EZndt5XX_(>DI)LGzcUdP`jjE3t`ktm*2SNf#O{Sp`8=)z*k0BY{qVesGcWQZEpF30YwO& z%C8s(ejOWO=$nQ?ir;}_6&jo%d%cMloF-OE9-VtR#|F9Ni*WP7&{Y|&7uj{byzJutE;s@k4O zMz{P&RnA+-!IX&HRr|U^v?q1x!^P=jR2p^|RXr1n6j-&#OU9xRcdt&%#;EFdvN`x zZAd$p2qQrhEyni(A~U+lqv~rBZvi z`uVt1y(AKqZvImC&cNzQ`oEPHm|kd+mM@32zy=8x9T5DxgIzZn>hQt?^IsW+E}ikm z=3z%#;;XbRU^MGL*G<(4*&_^}EcgeZ@3u8-TKO<^zy4g-zA0m6N>%P~anCJ9(-BK58!$ey6JirzGa#{Impk_x$xPC(#?o zROamSF|C5%Pfdc%Y8oNKd%DZ>V?D-G7m}isuLfP>u-d*?y5O5gRg+Os4hjK5;S$N^ zkZ=0u6l+g9(7({^uRdP^%WL!wOSj9RCg}ntd13+Fsq&z#zUzk!wvKe&NxY#flPZyu zpc=M(4bQA#+-%xEl=<5?!jbIh*41g=1dt9lg!hxzzKrip3#rRfGAKO{wuj&TvFLL}@qYZF3J@exZrVll13M(wFe*@cf z=i+h9O>oV8y1uor5vbmmIE4Mibf=tY+W4TiFx@`s-Je+pn-Qm%+0WEL%v17Jhsj#7 z@|}3b{h$@rBPFyyM>oSU<%8lm|F>}U+{qjj@;5-*U&zZrQh?>D)#3urro#_0o?nug z`LNUdLar3!f$@LoEB(`j_2)$D-|qjkLjzX_trj1Z!p*R8n$o#)n5??+Mn$*=yu~YT znLn+ETS=*WZicqNZr&x#$7=>X0i(aR+_1VAeE2%Q{#t#45Y|xuqPS zo$u)FE_VmU%@dF4lD#3zP*M6>eFC;$5zr&U{DO+c1(l{hFunB6<`k)$S#b1&_F~LH z9&8i7j#j|@SUh9}H}XsHm|xE})t;;jT-ss>MZJr`M&_6oVSH8mTkAXX3CZ{S|{+`m-@p96Co8Z!Nl#2u!2{%-6B!Zzwh2v!0X~P`_I$ zU$+qt*c?=kmMI55xpHs_?O^`=3c}zg%zo%piw=F_wPZNFLRq04mx{#f-iVZP7QlBp z9^aCdMD)gvzurzh2v!3gCA|&_g%)Sp)Duz;C|T6emjIhbXw-ao_@l-j8Qq`XEL%;1 zX-=jy=CNL=%FLhQ;CvdaJmuiArd2|3h0ars{?3Lh=OTex(Kz&wi!okM%m^_NSW@or zracfSX0*tYE(y%Z+9-MIG)M^u1HZ% zHcM+L9r-aEu$I*rRbe_Ck>K zvDI-b&I&0q-p;IR2t`ly461GuMj-?B3!_B9d(?)CX{9br7{hI9&z ztt`~`$WCsIJ`%CE@#m{FVZ27_TMk@0k)ZOzYK~YQ2Qqzn98BXFSEa27ItnYQm=(PX1&uR^l?G8_crI^)Wma**VW z8rSKcnW)3gQ!Da)GCB;(=Y7)`35~B#-yMz4Mnlve*1BlI(1FCu0SZj7qDcJCH7Q32 zQE&;j4q<+VG7a)*qv&$v#x=&?-Qo(wQ(o=-pK)OJ?1!m)TnA?0-^=-^?sD;CLvBFTxGeMNs1^y1LZ ztOk330H4ImTMdieNP_cXXE@F?xFgJ4tfF= zLY%Sv-k{s}lZ6za(AHDo%*z-LjZ|9~LSuZ<4khECmKSm8ZfnWPe326hBInK?jj+e| zPj$wcI8#(O^>ryGwGfq2#UDraS)+Q^Un191g%R1AFa58JQc?7~n81kK7$oSmSx_P3 z4<&PXx1{=XFBmQ6G<=_<2BI??UQPxe!$H^vZQr zW+Txh-zWM!cr=;Ybw%)`1H9E2rt}FZL^%yMUpF4QL9B_z+1&>Q=vyzj{ZySlYF=zp zdsXg^uKl@VDt9svw46t07@zt>yGtkBu7cA;V37?1h%g~R8c zS}ca31_guQ^>mL^=zE9w$^@6wX50FLg*+iPB+7m{Qjy+y?rzIQpG+F&_)F z#1mXm5z>85_Q$F-5S(N9+mB_Vfp3+FZ{k!wXzNmbx+{m}5m2nCxX;GG2g=hkgq&Iqi-oV*X}A*AodzC@}kq`Qy1TR&4PB>X&Bk3vT(L z5yzSAJHEE)+jp0nFSDZI(TEeh&S(sXpAi@7miGc#myIDkX)|a}>=&9)#Z*-CWCLp$ zFN|P1xb&NJ3HH8X|7q--fik^s$q#zD!8XCOK0;#5cet&2DX7jBorxH|X7e};-pqDB z8N&F(aTW&kMAdHCzNEh9BgSXeoLN$me1HR!h2lF$x>(Nbs6y?NBPTfYdEL{@(+ivz z?xyjoVRgvx&g-}jQxJ3}84JFhg%^A^x1AO+f2_55?NdB($CmC)mgshsuX-M%EK! zaC5b0c_?BGrb(1eTK{?v1LQnP97l`rhoXgZQ*9Bt?p(2Ut5^aP#rZe2*O$OR`s69T zS&c)YtwC6rSRdo4Dg6-?iP?ZZZ=@i63|_dSQAp+-9opvMKoL6I}TeRs{sbw^%2N*jUw zXYQ#wELW&wKZ8BE-T^gTwCcjqr-Ilg={x`3NrU6Sk0GAt@Q|V0KWmp_hc09bv7F%b z2g;8{Y$s$2;1pqWS?{7X%A+*A;58KkiPa3rEfz(vTy}KrA6-1ORrJe^uO`AhvGj{o zb(nwK_vuOUuNe?V`X=_=Cx4Lmth|et@P}!i3(k)jF&;^&?zX2(6v)wtEKwh0Tnmph zy>%y5NYPXM&igC|(rYYP1F+ntr(*Sf3u;-ws7sw0WBw2*KKbWp>ohe|pm7*F@kQ14d*JSy}`Vk-Vn1fN=>8h^;gz_+U2t>|t% zw0#gcrG3^4VrE{EOt;^HcSc{`#4w)|>0d7zQqL3^ik_ZJ<0^oW^Rt1Uxjf+S@#m^8 zl3WO)?|t}BD+E&bXZ)@5oe>gsrp_}=LD$CF%XT8Ld|%qw)QM>)bmvQO-3-PXd08~e zEWct6c$K=0f!8>=Df8NOwk8wWjA?V||XVPm77;pOGkYAlK<|pz?5KSV>MQ>BBQkK-?0e{n-^$^qhkJObFi%{mE zECvz>8y8PdKRkCrnj#C?K6_T3>JX2*6{T)kG+}<$v;)Q0o$)BM_co_3cO24=0Ce;HPj zVoiX3t~iG2h7(9VyTP?!?E&;|S_FSq^3Vygw9BK#eu$iJwZ+HP4^4`DS1TM~{==cS zd1VoJus+Ac!W;<%-ng0as$3u*-MPA)k^!EEsgbdW_P(o2}ZZZg@=7w3vF zUb{*|Dp3uh%jeJeBy7%?BkWYRRnLO;__BqZP447da>%6 z8OTJ`Yuc$m9puY&9m{kJ;A-p#)~oS2p!NUjacAAe)h+$JQ1Gn_u+3oIOKO!Rx+RSDY@&a`+ZHD~AXC zgiVK_Z4#PI7>V>D#e8?yzLGt1QbY-KhwVEP!H}lSEI$&Qh@KFWxpDV+!_ALHS~C&J zFhgyTpNjGjv$!~}sV4_%W#;r0VEdf$>2EZwQPJq^U$R2h+%$CS#MJShMpxKOZ>g?$ zQU<&fj!*UfrlLo_#Iy_QIOyblvlM&H5x9STOpM4)MGseWM5vnlfJl9cwMHTesXX0T zR>OEEr`DclSc!(9{ZTHlLHy$*E;6W;Xm4qoB$+YJ4zOu_ki=Awg6<6$$%2s&+u3iIepY7(38gYc- zKk++H&!wP)T$`q{91|pS;iEi=Vf&Q3sq*>PV?pqeYujfl2bgO<%hZqg3Qy52YPjcO z+*?&0X*QV*^ts~hLj%lzmPqC98>kos+RgKt5v1{GNZc=+F`*Qhi88u}JdQ={^z+gq znvW6P-Wlf}fmAf>KYOkX%P;eN&9FWllLfw-W{vE&qQGsN{Ou-3EIOmxrQk^!4=t-J zRAzT@&_JR%aK$nnt$4_FZ7Ml}`l?Lg*4-@R&77VXfx{t}^3|JaCJE42MgMNn%or($ zq-Ks~EIQ0=Uo=YXH2VF6ziF;17drZL5h|@^x=MTR)Dz(5<2

>JiF zI7`S@y%bsq4gt8^Okoa)ri1Jsh{S`Apn#HH0uJ=6zZ(7(#N09F?ez~X=fPHXON}hM z4`_C7UXQt00rEpiigc+`sO0R|ODD~6$hg=Tvq&9( z&O%xd&TlQY)F2Z{^C+6;!pakItJf!;QP1gp&HUMHKos0cFN$nY{m!8FdVvNwY@f?| zxDgER2x1c@8AE`0Dg4|1OHW`qY_D^F9)y_LO}d6I`9tBiQ)ON^qQG3U+aX3V9+HoD z-hFp5g)Qk!at7`k(7to(tgUS&@DXLJ-R5;ie31^tk8X!R!>){Z8=VCd;)sZkE?c83 z5jDP9Tv)E<$P2gp~-B^M!eq6z~*@fl|&`4I& z`gn-xnXBEdO{^8eAmfr&D2XrRoez2PaXu3%3$+(YF597$js4jDnliZE{cucrT^&W? zPYRQjWh0uAAtGi{Oh-i9UD5EKl>P;lBifr(A>oHMNj~TAiN+&mCZ9{P%;9L>uG*w`AsGU=*tBW1 zG2gH&5nKCIIy$G^S!Oorf(CY8oVou0eXq$!{)a~eQqQ*jb5$Yx&xABE`mo5}(9CM`#GkGCrGlkx(f2-l! zcQtzd@OaQWm>OyPorz$d`M$5L70i^EaI}5QMxoEXX&Pa^eh1rvjzl9I?7sgOtC(U5 z-?M{z_{3AeM6%D2X9m;vKwP^?UjS;Tn`tKNFGlCr#%Pjv4UqP!ijT{jE}&wrll@OC z6clI{)G5;};M-5e;s2z9Kl7Quy15|IKK0t4-#+ z7P&j9@RaFDG9`lOnw2?gRRYle_jCUQ#*1L}%x3)Plz-rYRH^1#QJ_l zb+&EHM;d`HE}uz>K*~uT#=lK*5P}z8AEb?j*o?N;=I8ahZ7vP-4cxBd$G?I!zXyhi} zyNnC*NI_GDy~!XCJ-(AbdZx`8w%q(Th5pNc^^f)v@y%GS;_CKDX}k-R7mJg3S*M}6 ztE5S+*D^6(D|aJXM=<1l|9eCfsD=zP-W1%hlturwCfJUcV!3FXF<;+3%z>vG7#iVh zAvFAXH~2IMhhAIpE`PQ*hbu~sHDX)kprY_P!n89HFf$L|aabyxi8D13=gUX=+SC5D z6Ii|4oiWz&EDil}yq}(j>0|Zue+5ZXc_CGXvYxWv>5xw?(rZiYjlF;AxU(N+LEtcVmu7r2l5AAfZ zbut+nz*&KwlmUDWvIrOTv||lNf6dE|*UXYp+vt{ce0VyjlNlS4J$tJ1@43r?Vv zSy$^Q?FTP;r*>`hUO-AS@!x8UhupAHX_7MOfm}*0)lW;{Fy704h5+V2?Xcgz@s9!H zZ>L}VCeWD;p5rr0gr*sAt%XUt>Rto{K+t*j3pj{4c(7|n5)Wen%e{BxZIKYEaW1(* z5HfNiOS`&({X9VO-u+b)()syrFFi~hUf!KJ%P|uI?DzZ39kI{1<2GDOhvlHAMMjn_ zH68wXaS}OIYM}{X-TPid@kqF-;js8!Jlgsy`+?;e4)7ZR)UomLaAtyf^Ezh^tb=t%kK<-` z0X!AJZ!~;Ogriicb;~tpM7e4B;_R(h#6Ttx<07bl=o)`b^4&>A5f7X^*CYzzAcq=L zj2OUm>4KP?&}y_LcKDz}1&4xGEYqd2TqoVB8{b+_*r5CV=1k_d9gugV%9GozSk8F> z%a6mEDzyD$mS@{L46LaxpDMhbgQyZE>0OEAq1)ubm5a~vP%{d&*B{OV2fhHe!!9hB zAndQ7XdI?z|2diwK35500isC)hmok=VNZ}JB?#lFFz2^Ez$3>vR?a=?T-aCZpARwg zLoI9{#u{#iBgZ%M_vX6dG5>XbT)ML_;KV;vKFwA|muSC*3Js>CHHHTPax_@}nwJon zK`Z9V^M3Ur*C-SMs#J}IPfH`0lRd)}nKod=^EzM+%VBFH>Jt7lSpa3d^W6+9X(8yUdJcN&!`@_Y5MFBn&ircGVQ zK<`IBkzQQFbavuJ+94{9U@S;|&LL$8&b?rKU2t*~zLx8uD2Fkqig?^c7Ca8SBkvNf zhmOOcyTOSa+bO^=FV`+Nj>F1cTdDSm8Q37HL|*(-*rqfDZmag~dDq4Or{J|(f7}C$`|TgcB!}VJ>r4Gt)O$f@ z-ta_jc|Saz|ER#B&=2cGC)&E`d*Kz=fWkl4Zg_g>a*(xrH>g#f-TCix8xWkDZ!zBP z07uS5zS}JwaI<;V`q@wm=;@{NkmO_B-X__{<<#wvU3RN6@^k~d%-&Y_S+9m5KADt> z{ATcE(`>#mRReQ6XWdWG6amZBbW09L8OAlV6s9OGhfMp)$6ti7{1*&|9YR+J>vWzs z_pP(xZcRN|BgQ>AczeLwU0J(Yn6K?$-5+e_4K<;xIb^B7jv7CN29a*!dD zVKd9gg6~5q7d?b)p)F&KD7rTv;xCS4J$!uB$f6c~G(@+{-Fw1K&h+T!+0gkXq@l zUL~3&bVy?AEZLuq@o27JQ5+_NFpKoQ`FS^>R{wI>veN}7y@?XTx}v}%b(D}@&x=qRZS?H@q-(HTcQ?}49wDi$iV`w1(!lTh{_-EMm*>Ohocq46_xreN zBr+J>ymAS1|36Xut4q0@2DL-SMa=^VAVJ}K(v;R4X}#Q2)W!YKE#)C9y`B*0DO|Ym z^lcGvD@5@B8^`nG1S7rJ@SMQfaI@aU6J(lf%SweyN2`^u=qPdZ|lFH5~~#yb^Vpjd0wrL;yok+uTjEKebjwOc0VWD1Or&o12a@%!;%!J_4nC3oOV96w)-Dv_d5bAcLu4t7129yJ74B43s3 zt;=b#NQ9g1{0GhmbY$6wsn$&gUVg9hx+|lI#HW`QT5Rp1QF`#)Mi%DG%DhW6miI>E zcUJh<6`YX)vBsyna=0hdNHseZEeFyxkDe0a{jRwpwWxd@GkLWLEYe}0{^ZO#JdyX+k(MRY;AjrqK3zb8sQ?P?)c=mU$QTT#-Z z7t#FdMGf7;L}Ym8(b!XxBve~(X2~n(fO5xd&N$<}ZXLP9!fs0>)TVsMA3BSDD@WeA zmD6cMDtW@uAO)}?{^v#RL>Vaa6@?C zaG_Ee_bHC_9`F2`0m~msYnQUHXT8((V=PTBT;_a2W%x4 zQ70vp;%*#>ncQ_4`R4{QM7ce<<*pIc+ z3WfB9{gPu#M$IvdME`32L63~p{&`(INODC6?)7+q(>lfOm`f6?AR}QnwpJG#|oX5)?Z3tlLmW!gY3Qz{PU_vy)UT7BGvbuom`vVFl+nB zdVVV$WfjJr%f;LW>$+=%xGza)Wrq9uMUzk;ekf<}@M`hjFsS$Y)axMW3wyNJogO;7 zBHBZ9=L1g$poSestK;pZNG?+2;WW7?w0r7gct&}EY+Ab>Vb2@%L!}vCw)?}cR})>s zF1%=Y!gO7TkcC`%WjI%7{85F(kF63)>|q?gvtCu71!uWW3aT3B0Yj$GCj7AjY|J4D z<6cw{@w3#StY8p%ygYv;?-uZD=4?OSEyBKoMCKrQbLft3@9)2x4OBq}qxt*35Tf2Y zQ(KV=gt+HC6Ra6fIK3a0av=hrmz3_THAkS|5g{y#zv9q~1+@;hNgEWY+#(&}TY?OW zBGx^5Lz3BkcH0|5-2AiL5k+)dhi5Xlu)x4GI$I9H<0~G`UF@7aK5Fz{}#?l=S{F0 zy#&Y2N zrZfT{vM-Fb-0B7@GcM8nC&R$qtM{1LeGo_DOg1P zo%aQq2W9e>2TpK!(_t$5H$F#kpZy^#g!ADyoCyD+|DUrB4}T@`M_KP_4qOBLkmhgg zo%Lt_@JXRn@@@m3(^Q^{%?(JybL!S?SF>ouBxilkc-IlBZagwDiwQ^7yJgJ2q(+c) zXWS@G!5{c9-ER-c!1u*3Y4#r{vfxr!r^*kUJK*yBtm1bz9iS&%|B6_b>e28`JPeVkLdAnN=2 zK7*${I$-!WAJp#x1|e?6x{Rr);soCV{*)|4au@1=_sRgu+ZsNAO!0iIA#4!M6Oktxu!ZM@Q+!N)a)Pu`Rks4sOAz0?Y0zZ%WRdv zy2iD#>|u%?(&c&`r%ZrfT1{{Z&w2F;|d{ocb7px-HW zyu5vvL>qfR2N@0v#^aoI(tq^0S4TU2HqS`2+bwLpjVpC>Q^oNQC^11o1Eeo z*sD}$#uE|&dMRDWJOm#YXJAvgmXiRHUyi8?m?om@q*s%_J}1B@4GM;KEq_?|>j>Pf zHic7kmQrFRuE1&eD&uj;Gj!~{B~BsH>d=>90p z&R3iUH(9A;d@QQa@-3J8*cCfa^Eu^pu{8rdSRh~h8*Bw9tRg>G&&ETJbo|K!g%ZS; z6nB-=KL@o3b>4paF&2trLaL2XDBL5@XU;h3hbrBkr5E)^BKEJ`X|;m6aBACG(g^Q! z#yM$-b#)8i_us;)ehzPly=RCezMXoV3m(%pNy7t4=)~d52OD;|pcX^2>Gm-SIdsxa z7dGdkacLJ`MiNKZ9H*%WGr=4wy3nOr8UnmcBfF#dFcTf6ZaH&j#RKKEsm-u?;a=~J zMAqXh5$O0(RRE$0Mw2vYAss|Th>YPztW;Yi8XmlOVnNUgYV39srkDyqC-i=Rpeo+~ zg@1`Ten3ECIkORw!o_IBXQ7eK-5nj2-TJrvI0OdWs6UNs=Ah!fxc2k9k?7Q}iNhN| zBV;p><&*9bj66em4R$a;>F0AhMwZ${G}%?!#hs9ia*LbC$MGDjQ*P~n_;XDNqER-n z76^n-BCpt_3~W&z#ebt#svaohtN!)DxfBpL5q-F|CyV}=k=2l-+5lr@X@EswB|NPz zQ4_$ug)&ZSuC;}t(@6M!T zHQhY!1p#@7 z(RdqAG`&q9Z}uO~5AHoDsrY6F+CCS`g$HBM9nZx4U-Ouol{YeC&XEF(Z`#h}4<;g+ zYt(t$V+6=tq%hQIb46)OzpadVun)m2J&@Kj6rh2IhLj-{G#*K&7XC^^up<-hEsyue zo8I1KqUk^vUq0a98;^3UH_zm;Mj`LKyv%VY+&^b~71xIQv~)RNtM?O95u@qy580=o zFkpP#KekE*5?96Qc!fhCF^Z6FhVP9mo0T_9%wl0rvm%;-faj8qtY0qSyrB)!cuEHx+*F8YhVD96ja~8VbDD;iJBLKM9MSc?Db1xY^oo-v10@Sk# z=$v==K{En8r&3;Iqo||gscn_Ph?*@zPFfT9nS2yd#@-m9KqH+1xsz!i7O^&(IUNBm zH+N1C)`Wog;$iN0amff0n#6Ch=7MySvHmcg(+CrvCyu^)9Wf{>QfWG*p$en-q-fp{ zjOj>!rYm931?T8NDQgbWcKIh085sbDty0wY?^?j=jlpx(nyp~29(vd5O9#Z}>h;Pd z_rjO@|NKuFj6nH+x0L1@@LVlO{MbpBH;@tGe)TKnBER|?JSQnQ1kz{ehy3UV!Pl8^ zE?A@&ylIxcb~^V!#lq50i`RXyUq2#bigWI38t3%a1F)~?Nv%(|bvHzg^AAi~_kd&f z$+Bd&ei*U7J9&Ak6WA)K9HT_LK>6l0>&NSDP<;NBS0LslADXa>X4CA1Pj5?vuHR~e z5dXktxzp{iK(=w>-Q#-L_|LCyKdKq*@;V+)>%4^YCLJfDrAmN@@U z3gA?=-O6d_deEaUQp)&R2R+NIPOq#gLGaBDe`?u6kfOWN7M)cAk607;m(6OipOf#( z(V1Mhl3*lUIGBTdS_;D#|HFG=ZcnePx{<&tX_ST5Fi+Gu?VGhnH4sgTA9=Q|kF+f{ zt#6hW!~Hs&U+E+y=g9iw>2+9Unv=vM^F>7VEBbmYNj z*$W}@f;m8@!oxXkUJJDqDg`>k;V|)hzU`cMCCKy+tR9WRdtAQLuU2C)ha%sbx4O&( zD#>*k4u0i|hLAB&!Z=TW|V z%{v@~1!gaZKQvHB~X(PWaQis#^?R(|hF{?W&M}k3^DvIRy!@s6W1zi#bst zUxCg(5%=O7o6l3Xf>Rp1ya87?Je%5+T<#nMx&5%HFC&AnML6Ypzv2ztzpv4wH2VfB zxVdZ;TZcfR*idFGY7`1C%Bo3|j=(j7^z}iLH}Kd*f8}sWFC6+=s(y*SA6$AxFHy<% z!~T*a4c*OtxcUNYZiRLOqv1)_JM6u1B=k}cZGRsOlT0|$N4^468q%pvum50qUh&kt zVmla=eLit#svWM1vj1c825<>vx8@UXh1~%6?6=OH@JveUtX%qQ@DP8LTqpP+6b66V zPnyBpw*Rcp`cF54<5v97#gh%-a`mY9SxJDdB=6xzytq$iTx8jIvK9ipDfCqY06Ln; zX5@9NflXq%C0n2h24++z@^%UEisnN5A)MFWq&`{a)73cGy!V7ZyfhbVcE1b@D#pXZ zN7hwAmu;w=BW!=LRShv*1kv1DH?a zyIVculDm zB#k0UALYuOX&|=80G^n78y%WnsS7A{+nTYTJ#`lLz9i4~}M`abR(|mYj`F8KE}W$bS zICI{*t|vtojlj{?STMGqkF1XvA?HDn`BgWKmzDxU=BLCHP6bERgbaJ)`Bz%|egb)8Ld zGjY{LOZx=(DBc&qn)g+DR{9097@7?%W{*R!Q_Ac{g}sqimCv|A%yaY(in?tTvByMF zOE>=NGvEOU#Z&oFu$2AmqbzG5*#!67K6DgNom)CboYKGu-m?1PsSnqs7_*prx4bbpPt8+MaRb6@}KgSGSK zMg{a;@VA#v{Ue^w9NjHG*^>SWZg5k6ycO0718w!bDu)29)erF*7gvGZ#fiOcnRZBA zh<}pj*8-Y({QFZjop3LKT{3m?B@kIKb`(d|L0xpSV?K`JZZs6NC8agS`PhpyAtkkjA@%( zsexhI3t#0S56Ww#s$MZ+u8+>n@wklyh|0W4nMhg%)yZ<4#mxj@UgLdiHB%1P`Gc>r z;k;{TO2e7;#TpnnNNZV~$pKUOtV8Wdx!BLE!hK;SA4oOVj2E92z+$g1)B0#FOc}Rp zey9(Cn(JzoLr!rJlQRC)%rgsiBNqAjv1g#~u{m)>w>wNKUpn{|kqV4rtuBdtp=ke3 z8`BIgeyB(#7op?X{fi?_HxoT}vtscXnWOoV3mFaV89C zc2dzP zn~gSh^mk6XMnTeMxb+eHt5jS z0j=nGE@FK1-(|y9Gjzy-`%Fi!E(AR8m)yj8PS2IhLN1dq_*q6QUS1G~3@YsJeY+Zu z=ireoFS>ltd!_-Wq-RFxkbJ@v-%tRa1QvO>2*e87URMhZiB- zWz^Cu#Q7d;-YzK4N0$jygg=%Mh?(;0?#{C!M5|Z*CGJ8V;+Gq&nB{i_WxlN2@zEh5 z(aBYO_G~tq5ALzgRDO)QBv=;M+Deh*J$=30AbCWl5E(_q<^xMSPuA2llA$h(`3G5O z3UV1>O&KcAL>}S!Ng>VgsPX3SBf^F8D1LCGX3r=IyDsx21NP?{rHBvtBmUne;<~TX zkqq14^pjSWh`r|A;>@-=I(4F;kxo?&jgl^7YdArb)_V6I_FFz(wz}JyjXF8XI4w;^`%=O_K ztXgZvM{t{)o!}yz54rvNWl^F>6A$`juRa}3PAo1CO%xRoIZ8|&E zIPC2Ogl!SWI9^*c5`6dL%aU9O{XTvqhd&-IAC63PvW|w`_mS~yi;jr1AXE>vg@QXn z>NmgcQsnvVw_vSQ40HzdRQWpvprZ~#F>c>NP*Tr+%I1(GT6i9`x-ppv#O*g{VYeLd z{Ku|zaBpbEMP1qs{U5}@)$r^w>I z8~X3*f6d*8v1i9t*6yXCI|@FDo=6v-Uh7AG>nAlr;nxFZnAQ%#aBRd7G>OZ1l+ zp>i0R@VYLuR1IWC4_S>YoNx}3;BXD+kxv`_kR7_`0}eOFX7h3HQ-{`6kn3<9x=y|G zthK}rg&rrkmp{mdG@tsrSMdL>HdVDRf#M}d`q;b``$iU0P*%{|h>Qg`LW6RlXeoN( z^+0-ovJk2J-MB4R84nC?R@&Wj_DHzNg!mH#qPx|J}YfR4lO<-4+W$dxdhclHbCC z?U=db%Wo0D9HaS@yDtcK-`#Pi_=)=*bLONL!9{S5{dk3ha5VC^jnx-CZ31g~S*nrU zI*2ZVbnO~#6$)21UuRvx{K}sZW5>@)qo4Ul`2tHq5ZNA8OYVyhIA(9TemVFO>>n=V zjl+^K^B|L7k3*AT@>AC&eSI)Yi4mPd&91m-*W97|3io%nWVbdi5@1$Y*V~WF4A___ zE~joJqc}cp3cn?LcxT(>bgv)^{bA|Pqs9C((`PK#RwAQ-)`S)V0aM{0lkCqz2!+5U zapu$?2@pNHb?)qmBrvHnm3^BMha&7$1oR^P5&FWY%hObjbg5{0>8$iot&g08%vWA; zqT%Z^dKn6&cUD`U9*;)W%o}xIOg)g<<(ElIbJ`HaFdEh3laCHxN}BAJH$*z(cdpzt zibIkoIs;!y`-8X0plz>b9t_)m{(W|V1&yHt`_Z~e_;d99WDCAeIhdM>lWYV+Lv)pm z&UQNLvm_&n)e6EstEl4)c+MnHcXrlMJ{o@H7QHJeu|RJLMIt&U)6s3Ai3{nEvLI=w zn)G^GJXBh-D>1ag{DQ-v|yyqX9-vYxrK$ z)q6WC>~aM}hDAvDFDSvYm?HMYJuAeYMDSagj6o0f_?Bx%V!(LHT(Ofj5q{WVVl^l3 zE13K|^kFF(eb;mviB>cMnv-wusFEcj#+IId8P;S7_ilf;>Y5C-{%q6vC7H61w-B}KU;o>GyLa}aOfy?B^rqAE1za<+{7o`ly6h$J* z`6q!|=dzImEpgR^bv#@>M)r|G#23yA@O)ek$NYW=M|FPkK=A2itGxC*2*l2uo22;W ziPnud%yhl&(fDCM?zh^{;h{W-b2Jl+o{4CU$>f>cD*~!pJ}szDp@P1rFwgT<+(O^*pX}680-DzQSCohRqwC?rU*~da%_QLHy zp9(>JkyokzQ3B#WQC=uco{zk~9XhSCoQ(TAE!D}9Hn3F7NTAF1LiZ2s{~izmp>?yo z^Wq5yByUb>&Wq>a7xTxNwQ_<$s!V20h{XnSuIlTYUrI;s7RNPiEtw(a^5Y+QFh@H{ zk;^vrW-&^dAVT|pHBh02ZQ_kWXZUc6T@wieqTWO!nnv*`Bu~puufCQ7f<(dc-|b7# z)t!DPP3mlz3(hidc%*?Q{Y-9aILb=gM!eio1rr zjG{zWsnR5(QP5rg$nV-gAnBY_XWHop11_ZrEwthA%l@(14O%hOY9tXYQj`fIBcuT{ z-)`V8hOfB!a4sAPJ8L|+APZyPgp%nfW8tLCehS+UT`)7v?$Kw8MNc^y|A-IABi6}4 z@;Ig(h?26~_-&C4k{sh_j$KKGEY4x}LhOw=T`Y33bJiAuDD3uRgo?{^b&aWX zuq6{$D*CGhc|}Uc5M4GH>dA=`xu&8tntK&@@Hufw!T)V_MJNh8Q|iWkJru^02}35h zpDs(;nQv1N3(aTjop&&YA)HEID&8gw-YNdOJ6%$VwpuQoQ01{gX~~3*Z?h37EXl0- z1(h?BH!ie!_dFQBJoR^sKamSk9}lhwRpLDFuLOH39v@KIX?*D(SO^|#d4dgv`23M) zGSKmv3neuuvVO4kM=YAfPcPG1q0kM^^W%a3AT{}+WWl)!aDs>MaZr1n?BEx z;zzeH>rU~|CY$pjku3sJO8)2z4F>>a*}1Y<7wm5%(H^ukPeN$g>C46;YxK#;kT=#k z2e>BW{_-^CBe%U{#V_XaV9%HJb<+C?(9M6>YFwWH1K(aITzKw<_HEUUT%XK?P~vCC z#TLP+&YoWNiSh$P=FfL&jy4oMc$X;8l&T9>_PLSit4_$mE5EO8BMrv8Wh}F{ZJ?&< zIR@GfIuh#&7>oIwgp?@FP3t-x zz&-r6OTBO~8hN>~Ht;0{bJ$r5nDPaoC!WYoW-bIVPV{Y3*Cav8-;mb;bWY_hax9fkCfj+t|kdtecYs|l?w!hI6q&Rdb&%5F$ga9T`T(i_oo zO*}}%{iVdO{z2X&7RY=gje}Dp2mO@3$UR-*0ZvacM}^h!edgd;5Q$|uN^i@UNj?^X zKJO9sL4cQ5jT!U$x@a)w&&0_ML;pdYN| zxp3C^cFG~6XV~k>MR8a;9>ocrTP%%r1=^y7C!}tefBE^+onV~je;~1TG~!tT(s;=* zzUdf)jtmIbpZz9{M(VHfi$<_g9uB2m2^^!LK{@nI{|l6ij!aY%E1C zA*6g4buc%$=c1mSOa*)wl6z%Yi}|kVl8l|C*l*{wdN^t=4@iqI7tQ^MgS2n#2R|6X zk=s3PZ*O)y*Y3F*cH*8dqJPozHFpT}Nd*rl{4|e-djhuy+;Lx-`$xyr4Q1>-xs*u% zpj-$YT5w|*UL^qOGwt^gswQwsT6d4`d*`E~StjmU+ z(t&{Wo>cfuNqSTQr6A+PL09&O=Rogdow}tLin_@|-@h96K<#(?w`!-cSFm<AhEyg{Ti(HS?y!d-BOeFyjF^mt^N0ehggeq4xCGlN+}@}~>&n6s2N ziuYToup4EfeT7FFTA4fC?zaZRB2v$Z)Ch$aJ8-Yc*a1zSFt!)<%YvyGrqZeUOmtoM zM^JEZJ~9$F<9p?vj6P}9UGhHUjMy?BxB4Z!LC8&m`*9cS;N}z|{NJVoQVV)^%_l4a zvB>sPwHyfq!Nja4v94gSxSoI5^c+rij^xGIk0zj6p3h^s-(2AM&`_pCXc~x0TB+3( zc|+&jj^h#MQ;_I4yL$oGah`{}Qt)9?BzoGSEbh&ehsyuc?D1UdgdZLC9+LxuFheM@ zxLG?1Yg4!RHP_yOXa_RLNcsr9GkvL|uik@kjBu&H%_I=*1T85ZH1iAj>|tD+%6#z+T8cgZMa%ZxVHhPQL}gnvaSRm18i(!=f|sbPQxQ zTF+Q6jsg8P#>XETSNn( z_VtqYC;I;&g(gKIJGBe8$>m-p#=L@>9SXP2$xdJ*PpvN8>jJ^?hoSjWZQ#%Nxy6gT z1K7@=6K2LdzpTKZysGss$PJV7*9^t8iUZ*ujnF(RXP8df2v05~eSM;p4FNJmN~f|LKs5B+)C@kq1ifr# z#XxPNhGvvFPz=j7ZMK7D1OXj~q zVDcpMoc?GkIFJxMwZ(bte_oAu-NT~6>+IK)EjnW8D%F3(=9JZ-I9JL}R$2!4d0vGL zG2tBlWvhF?!w7K6#$lz|v>t@-Q&Xs7&igTrB~8jzTQn*-xfmpw3?}CV#OJ82pm3_( zi2H{v(&5NgI{Huq?NZUST-URK`Qy%ts?kZnK2SQ4eMS{=?)YBG!}-U=LC}M$;mVyTzFsW&7<{jXawX>@RhlY zjX>#xe1;gl5lCNrceVM$02p$ZC*Fx0gm$jlX$GY}NF|Gx)P_MLe==b5Au+`sE%wheaSL-TxssmgoUdU-TwL|Z+-xGGrcBp*CRW88M1+uOA z>B&K_!EN`h`tpw^kZot+Axdol7r!~33~M8NaJ-QAMX3W;|D0cX{<;&S ztio|YtQHQ*vj;_F)PTs1u%`NjdKgMs3jSMK3x8BY^!_B30NtHS1H}^Uph%~|UrlI+ zphPSBY}IpT0I1rp4KOsErctKr#Uq5;2w^0No44& z8rbUU`!~#82_5evNVI4Rp;g7WDhBskCD$0%8~y~t(XjRf!)ImiZcNI3uQeOw8U%B? zsFQK;XxdtFCcwUD!7F~I1x z_V===57_zKBfCXmANu$glAl^nSr5)-U*bW!F+0!ml-OTR}UQf8=8#ayV< zo#(7C{Eb6=)SQ<>@!#8TPg8Qn%wkYA9EwdJ^n+$GC4uGvdo;mle_rls0`%XZq3XkX zKZ=O2{nRA*y&q3`OW7LpVMKIzW%?svB*VvF=x-rJbi4E=(S$&8mhJ^zK|eGoZhM`} z74HEvO>!CNBjC2=Ri|VcA86k!jk1=FK?2fy_iX9p5k2p#gO6(A5dU<#TYJ(EQW>27 zvDD(8oVegG710c6{6VS@ckmFoz-RfUqgNOF5Vx1iL5!3i^a`h1*dzqPnVb-}cUrdK zW@SO9D5wE>#}zEKO>)thm9Ix`OT~isC2!3P0`@TOYH)2WrNH$C<#X!c{_sVh`#-M8 zD0In@nmMyI3yg2q6R|z@gL9j|@3|Vqqnl(Q@AvO`fL#;WPbZE*JZE>DU2%AUlE^PA z=G&(s)^~gCzrJDqdZx9#_;NImrE#<-WZFXM`+P~FmN>-wdP!hv*#J#VSDl|@bOaKd zVy1AhMxRsfjr;Pv0DYd!&n(7x9+z;+z3pQpkO{1=)Qe=I1GTiJh(>8R0b}q5#w^|KATW)vAFbd4Jo2D_`HkmGgYk2cCivRC=)YKY<>P?@|ly&(qp&u;&*=EwU5-I8d_ z1_#8kts-~kcn~%bv-R#Ej3wYyqVMwW5AB9fW>(3JBpnKdMp2u)xhBHCP z@grvd>XvGLdv_-jRm5GZa2)hQ>Zi%uSmLnXVd{hPs(&<^C_3SC>R%kvDf23IbHIC| zU9xt!Y8%L6BlG`~9)i^GR_EB=^@7H6br0Dze-x(I^=IC;9oBt+m2W;Aflj6+4`c0N zh`%l+T6kg{ZZ#cD1jXa8pH>WSych?{l&8xiZKF^X$$T-g=MDT`8Vc3wegi+lu64WU zzJaGMwh{7?Zy;6UeBK2#3Qo3dX17X4z?(YZ`!B&Uu;)H2)to#4WaWLSErEkjXgtzV zJU0yA<(=A6ZTex7vK22vdO^>auUbR=8~TP)~=qdO(s ztY_MBPx?+NFYe_Q{o%VJs`d&hC*nVcOmxB%Atn856|dn8Ydhci=1VA*q(A;iy9wS- zy{JCA`V!cNIAfl?Z-%TBN>_DouCP-hgym>%6>wx9nISn=59f_YLr6`VV74Z&R^weQ zXnq+OVCN`?9YVyaA8f7T^UwGPdE40V6civ?VE_AK7|flBFl2me2aN5X>~#*sfTp)! z+W6%t*snc*f^z_$zbN(ZrLV-nwOz^E!UD;tx7Rfz*|-2ce;kPV?2rQF<`x@|`7)tx z;!rS87xv1;tcja<(i zws_$2`Bc+Z9|t#BN`BEuq(RoVWBsqqMG%kA`yRtlAK-T&68aUNhs11OnsaMbtp-Z=MY6nYVm!lH!pD(3GPoFXT3k;8SKvNO0}?W0g!@)pmd^dF4c z+)7V@KTLe@ny1sy{OsJSfT}p2J8OvUy&_<)-wnQofoyaoWLxgWuR=sfW2t$o=nTSz zcf1!~XCv0sqfT{s+7RHHL2El7gszF)K6zg&4@C!D7pV9@Upaa;c>?Eyq$>;(?9#yZbr2P+&1APP63>M%l;v|87pmqtF-hT9uJO=-_j(g~&I2uFEX* zQF;;zX*pbUx@Th1mD1E1-|SondbVBr%|sP@mak0F3&$fZd1)foxNLMPKMv)JUH>nu_H42Nqo&O+iFSJ!SU=K5u@!hoe6s$S+iQ!V>4K3Z~t4U5BE`4YJAeLIPI zY2&0h@@s#ae=FY)6+ffgIJxWxp7HsudlEs=`oZM$cWqx-pnah~ai<*eLauB-F3SMF z*IUxB`wO96;Jxt=>2xqI`7=V19|N|%NxoaA8i={d%vfQ|8s;Jn`Lf4hE^Cat@Y&^1 z^z(9-R*(&~=%4OKRoYO(7TIz1w zN(P!y_{4SlRyI5~=R};C3zzrmw^Al^CVFWlnr?f6fWo3oq)N6t(WHXB0$&UE7%HvO zQR6(*rsKhzYTJi!?O{Me_>cjRR1FWe2Rfpg8xD-?IXQ^Ua{aHVw>MIMRCtB_SvvRz zr?Gs-T)<4XL_^BY;mBR^*;Xy);>h&Mij6tNArb}jpCL&RV$j_x*mh3?g_IAZBKnCy ze)`MIPPq;!9X7gk9Op1i>DQ`Ph;4xRU^Y$aLIrp{nN(9g6NtReiMMVD;`!`}RJWbq z)o}1CAQqN$piMMpqY(3nd+bfO`0<=?;m`?li#~6t*1BI+jpEU}~hGsuWhwmxGcGxvFqVIj9z7@r}9FfI-|V+q6b}PkORhBl#Tr zNPBl=Ogyq6Z|j8V(0^G_Cb=^HkUSlJ3w_F3uEe=8p^?N;MZDLoGf*(OR|wA=ePnXIbDHDKe z_m#;}nQTNNlhn*&Sr50@D2A(3(jexsgG`@x3NSqO%IoTJK^(Rc{EF&n5IA~F;#gEJ z#63G+)QRWMvcHqwl^Lf(rNrP%46TB5PxOiclA}?z+h*Rc!2ncy?-7v>&bK(YtG#Mh z*N0+(acs;A1e-0TcN3=7u)dK>&!CzEQ?A@(&Y>1&NB4W>b>L-zgl#&&gl5?FAhY?}r)*l}vJS%;_joA@i zbc(0FUCl$ow-^5v?-fAY7s{;W2}ck(mc9{>_X*y+M{lMc3V>L_|50?_;atCM81^fr zWoDI`jHD#VDjrHStn87p%HDhLz4zWbBYSh(8JQt6N<>jeqD0>3{j1~Xh}4(Q_j&I7 zy3X_CXEH$>B-C@7;x0=Rs<4=oFdR-ncYpC1)S1Tu(UK?&8EdeLY8Qy8yZD%ReAZs&U7(U(-}#OyBPT?X2Z1dqTsV2e&i0?U}NxjbBb;tY=4;Sg2R-`fLdf&XGjQ8DLa|=1L z_&izdb@xciZ~(mT5+`e@jznMjt7R@FQL^`501k6IG? zpjQ;M#+Qq((md4E=#~L7ft97xOIAp2$Jp!YL@N3&P9ke-=7OeTLgS(j6M(ACN_FI9 zEG*`HpYs{@gq$=A@u)0s5VPHv;2T$llQC zO?(DI^xMR%!)l0ruj$wcG7BgjUJJdEwbxpg(o3AOtp^9hLRptcqTts4f6T^QnegX# z<9F#g1z388nw35lAkXt`HS6LL=$P)t;#-Rn6q+DFrt{=J;?2{~&9HSur&Q=l-H*LO ziK_h{9Tl@tWmV$W*ZEGM*VQ&%Z0dx$e<}a|`x1s^LXN0LmxQCP)U*J;+sW`*!I95T zuoUf&8YNu%ngOXfZ&XMj6^R+|Uucg?L`;|eG_kdLW3JlY%>3^$@bgG<`irCl{rT1&D*V^gDj>(X(3jNEv=Fbad`e?g3Q+VwV>!X;3Oa zRb|?r$Kou3ETa6Ir=|x&V^&ha^c9HMdp?sb#tke(c-TH@;IDH}tA|a+9x}cSF#WB^ z`GhD1f;(z5*i*f&MgGeQ1qcn!-{nn#C-pkHhU%F}n>a-&B`6Ur>RDfAEd`;{e=Adh zHDQSInC**mFZ02};AnHkVgX$F)x+dJSb%ORKh*xH6%Aw@3G1{tKjnALQ%@@{6Qz6A z{-8I^gu0B?F&gY;woaklzGxN$2^ypWlWjSWJ6c9{av}+xHYnQZVYGy<1Dd8=1kUiD ztN1A=zbD!`aGux@^G7EFwPx5ka}i%%81==I-Vl0LQ1d-?B$7~GqIw(=hsy0}^L)v| zk$|(gcqHEUzva}gYH-9J>$Q#Ri3#}m+?d5%$(Dxp*YWP^&=qa>+Fd^LBLyi>YIE&0 zrl67!0;P3dFemaBY1{kcn<(ipx7P;ebJDi+oN`R@{E|I<3@zqB+GNwnAT{;`k@|gS z(@}-vTb+xS2du#I!R8Zr0^FBZerEqG&jsbWvmNLt7b4T(AcB9sX7Eg(cxx5sjUz33 zp7`Low(y`y!0$^uRD!C-x2NH<2Pp0ey zz})R*nz~|j)V+;5UwRjyUVW>H#;|-n&o+?+4;I38VV57}gBphf{l@?@B?V++?5#9knFF_;XpsL+Z$u+~#nB1( z+jtkg&!3MAL576GU+LI$(b!exN|S6?bg3_`IU`OGEM8r=In!=#3;(o`IM2wE2w!oSq%uv!xqD%HOA{Y7!l8eRp2G=s z%-l^B8i+t2LL?HCIMX58qwe!2OFHJnXu11PP@~;px1j$Tf+6ILH$Iv~q6rfXobmBT zx!)e=wFyQcsqXeEt-nDit@`t~PW}wEC~Qe~_Lw1xFSwp8`_mTOeiV)wy)HtmH53f0 ze-a=w{NVV@*Vu0_@qVNJgdrSTFRY2c=k+}EC(Q;d!e}#Xxhb9F|9*NzM?>U|n2ReX zbyghF)jG`!Ir7PH$MyJW-jgAalioR4|1A#pQ^RuHFz+G3^|_7Yr4Ogda?W1g@`nSEnV9^Lgw_r|{SLg5kLF(o(sPL< zfEe8*|B2{GM6(|-B4ZE+wknf zb$8>Oz}BnP;Q@WA5BLw1G%dn_uwi4F#B4 z-Ii-<$%eu9XY#fkMd-LGU2FL#Tja-HGVdmz4!3*%JN(3zh(4cet<3OIL+6L5niYI7 zpFrpMg{WRnB)^t%GR`6i>Ao@d+aSq8U&~J4J^L&Hjq2wfFS(k9ynbe04L-<1DhX?Q zN#seWJoS@4XQdn39n!LiP{qFNqgN7*Xxz}F+h&^U0nzBl8;J^m=ujYhwMlULLnO$Y zl50JEMG-08+qPSrzJu}zcB%v_-62j<`FTKNHu@nFJUZ=m7q;9OzAs+%L?)$o*H$k2 zq3eJD=;}#g{^>Ka+vI=DP=0B!C$*40a%;4F#?qPq)uHGdlV%|jY~XP#yc8vUeYuMmieM>EM^bHg_>QTG^s z^JkN1=+qoG>&K`VMp6Yo$ZmG1q5Q z@RQqzM~-N-lvzafR1kcURGMU!4Z`>8Tk{vE^-*`Cbdk{YAW&yr|4MS&8_vpVJiHZZ zg*u{H`f4wEV9yAR#zC7U>TVw{a4rr3L-Iw*QyPhA63yzpkLd!-I~n?g*kAJeHC-Fg znLZGa_`LZU=Q~oHb`N{`-hzMp)-&CpQIN1QgI%vt*wn~T(Y`$g+pmWw+$k5}^<<*K z@YzN1|M<&?g!>bGQ8>;Z#kvUJoeuK%-By5-+DHEKi#13OP3nf7H5iMmy6!%*2D-0Z z`zTXZ;S8r-t&P$etd{=fqpti4Dl8eY)#YD-iREHltmIetR>zttZMFjZ@BjI_zFh%@ zka_-p!kMdxWAi*q$veKI+AJNnYcHr?l}#l9BIal zp*WW!aT-lDjX@)D&!zu222(bar++=f`;Gb(<5N5%aL?d){I}&H5EhcWXiPo=jb!VS zDtAYq@1Yw(mDvz9P7UU|H4VTS|GGJ%&_4LHvw7foZx~XegSf63y$0j3z{RvT1HgBg zUZ~({C!FtcBs7if24Nl3Df^r4KuhuMJHtXZlx57N8J=x{q4CFDOG<6I;>?&(li=gx0CWpkbsVHtLB#-i})}jr$eQSGJ3V9A<$;lW6Hz zha`yGl;nEoi@mW+jsE)o=aLCB^%93?fZD>$<2N3aQ1bSPPS<|{pxID6e}6U;g6z9J zI^GpR5~0ER^N(q;Il`xQJ*N~AG`NSTbtJ%IBK`Aj>g0Qk6Mv~xmO80lvyw%DBVMpa`Cs7DWb2+D+3s3TK>I;&t-CoU29 zdiuwez7+z=X~{Nq*8m_t+C+6YUksiGV;k9{*1+N^rhMf`GVoERuywknVc#pSxBPW~ z5I5%e(6v?o$CjHck2~dqrJ260OV&f=I#TOkb<-2ImzqV~co_i3p$rPaT8}}}%DzOt zE(JJWvVn&>Fk|*pIT6s;I`Of|e2}5Z z8SjX}oasGL?|B5Bxfi2A zYW4+HGd?FNsy)5@EE;*Igq$dIapgcEbDUNIC}>G>ePPR8TC@p%56>`ipWhz;6Aiv1Zh@=$+0Io&UND~Nt@kEiXb42-kRPd>`raI6$xhm+L>zN1?ESEGZm?0pfH_MYWZ#$I{)q3a%*55a!5>%cQ-YMRn;!#MU6Pv zd%#ni zTe&I2%kh`!k&+MeI}Zu+5s?g?MipV#dt8R+NZwQ?E{?rn`P-4j;Kmc(j=@@4i>Y~@NBf1|0ZUn3GU7o7=Ui11we2y2| zCd+QTW*H5YrUn}S?^%_MlH_girXh;^>KB}zhr^YWvo8|~BA_P!IlE%e4R9s@9@-G+ zjDoiG|8cU%!Q*GgrEsYwDcX2PjVaq;6sXq{XeSDnd zLWU)r5H8OXXH7s4n|Pbslf6+bU(7A9CH!-GY)nEEpHFK*Yt8;32T24Am#lox0bbV4 zfe#A#h_ot!N02odBYiZNrfjVdt;K(j175j7c9usU`C%L^w$~kw*?6O!DaD0^x1L~X z@BkzKf>E^q;cadL45LgUkx%rVkTB)O^6tTCR2F0&ji=aD!= ztl91wUriRe8X&twJ>i2keAKj*ZK zbabQf{Y+g|EJO|#X;q!fM|D;?grbbN7aMT-J}Qz%JjS909U6Yf^v}PC+@d&8xqF7| zpSCwRc{iid4^fztc3f3G%@gIf!9AVseAJz=b~`6F7-@)S9dD*dMgg=VZ`)>q@qOiy z&9^vfbZm;k_PiI3LRjSV2JZDHL zPAKru!F+pm&d`Nh)+jE?hlRs7703*NjPq}Yz!{OuBrj=eFi8pC8YaAc9-sl!V`2mmMa&s(VZL23tkjaMXQ%CMS{hSW4NR{;Q2=3QvpR`Z7 z?F)h5$N#yWjzU*>Mvi&(hXe09lkS;1ZzL1=rE=WM7(yNd#=fO22j5@e3kTNu$nEN| z$(wJ%=;hG$`z_%%i1_tgYT=v^&|aXCXvO!HBeoH5-JBE9pxE2d9eY>o8A%Nf;CKl! zuIZQ3OETfj6W+TY3^HIip7V&>bNqd@f{AVg=0ZBX5ZnFTb3TS9 zU?;zI@ob4Vva3=aOjwVHU%CS4V@V@W*(ZB8l6zTz2HXct-(;g=qX5N`_6kI|>J>39 z8j0!>mG}!sOOT+`oY0-FB=BdZKUKtpdz8LAjPfzK0d+$!f2RvKaX%L2vMwr;4^?(nl|=mcrCJJQYKuiVj@K%FX(D8Sk%Fn+}RNFLppM zW6zTCw{~bfGW6;8TsutVK72_?*a0uq#INzKbwLROQC4qxBRq?gjUO;YDnYVUqjgQE z3|t#z>|ebp2S&$BTFP5B!2hP2^%hAZgiBpp$jAJKG%hOEkNEs$YMZ{TEsf7hCtsCy zk>$cJiAclQ?NXrT993DN$^&yfe}QH8YWQWZ*Ub2+5k9uIoT*nghL2Z$=-=G00b!57 z0V{aFbe|?~jpU~%@VJC_=aw2H-twt){QC*8Lz?26@jD#OEvwqr5Y+*RiAsK~e+4if z9niNhlZUhqG%r{^cLC5*}K26gG%Lu6)-07N1L@&G&5&?Kwq2eKzybT3wJr@L-qj zLoYm`Gpzqw^9IUe^{DJJhvAC1lH&7o131TA_IeVHfJ_)klx*uT$d*a|CdnEF>ExtD z;U|~_v+GY4H{S<03+D5`boW8EWcl|c_1Ca|;AL-o={1l>UZo$L>4UY=bh0lF-N5z1 z&FCl2*HUsA9XHhP1GRw=ta#aoebrt}x&`&1 zbnHxzWKjk75R;nqHnxL;Y;VTwcnN%F;mG_kSr1mEM~*SdR={5U)MdH9_?&)kQ*5fK z2((+9-G1To^;t>!u2`iCu+MuLaoCg#D;8fhd4HC|iI=O3+|t=F!#GjF*k1tz0>S*} zd>$Ys0mTuiye;pDghHniWUbAS;LUle;f{73xbkJu5BP*4g_hkcBb#XSFp8!kNj(lG zl#C>_8wz3TqQ0f_QZn{}8y&y1ehH$bC|>$$$3k%VSZBEyo^!~0FkdXed_1ep<&e@a zbbPnMUS6~i*%J|$b>ziCt1CI>-N|f-?tFOZqFyw_e;rqRqYwy-yj#Drjbp%0XMXj0 zLkw!sl6`7x>W?bgT>`Etq(XSoTOafz4ie+eS08BO_vWfe)s$T_Emr+Z7)#dvwBXfGVJ;5Oq|JG>#j` zAd5PRSS{-S^q`m0_p&bbi-%wKq-B2srw^VSv~UHWZ>>*d)0(r8Rz-o+_=^mGl#?)`dbdoBXe{__cUFl0sQHsyY|#(Yt=v<-a_V-_;AQjip$$brXxyBR;5b-%~>W^ei=wzk7~+ zMa{?Q{#qdqE?(-R{O%Ab5kN;VR{(qbd;e7ObC7LZ&FR0wxF1{JUcvgp4yKa3wN3nO z(1)-IdAfQnNHx~@rsWZfwkv4(dAdE&5wzvC&8G(^3)y)^J+ZHGNna!8L@emjX{p8~ zWT6e|JN8#jhGV{+pW2>qBvMb~5R`4tgxAaw!u(#D=p8}E@Vy)O9KOQ;>`)^N5nTzb zY)MT9U55Uuzv_CZC0hS4-*6D@Q}Bu@_8Fkn*nGWMk0{V3omA`f^G183Yy1E9QlM*7 z)FiwW_sSkTtiF_Q15A`Dl1J0>z|dv#74Z>6*kxadTyTp-jaEUmORFBJOKa4JU^X1B zaQ^E_RC$V$EbIPoJc&ZX9UQ#x{c*1-#Yjs_$O^zT{ zE&W<#w*V#SwIvipWdYeP<8w~_5;U-`Ag+5l6S1p&dOI;@2m+5j+;k~QLzT9=gN&W} zz(*uaoJ~}P9+S`?{fE5;76g2v|1hV=<1%>A5EetC;1}mV;@F?_aN{uW4?_ebYE{SFkknWUf6i7iItxnzJ7!Ukw7z;j$2k|2e;eGJ<}3xnln^oZbZ2;G zB9c5d>a4!ia~1cHzMSS;y% z1)#UU{5q@*xf`DU%zi5oNI$vWX2u{%}RoH!pi6 zd-%dvOAS6tu^?cOnEhOg=hiy5T!SK$aiDfm)2Kzn4tX=G3Lg^WfLH+YQStRK^o8(+ zPJ3Mtgo?WSI?Iv?VvK9S;q|Ftp=lV1q!K`_lB6JeB^Q1e^(m!EWTOP_A#c`lXZXMy zS5B}Ti)uwkFWTzop@P#)*Sce3;r-mm`GWT@h>-r4{^8$LbhC6MIS8Nod;N0km_A^R z?%TVBCvbk|^Rhgtam-!VxtC!a#gmIX=ycW{wSo{;;*T?SKi$Eb*f#_)XGOD{G(a&S z5t!Cb57w079QQe*s9jYLlqp3VbZ{;iNfZu{ro1UazeY6AEBORM?^Yf|p0O8v+*V2o zHq1l6(2JNCbGVO+>+vMlbAfI?_}fo>z7ysj;a5m=K_edNeH+f9@TFK}{~$R5;_B`y zY2xRE0tM}{KkSMq>0Om5K1xCQruY4*V{c&G+u$t~-30LFtg^l)n*e#Q2U*urC*aD{ z3hvgBN$9;oOK(Up0R}obEHtCzz<$U?W?DQB$?wu_c#0?R-Zo!v^~GC!d8%MEoOuh% z>|Uz=KStqqJ9)5p%pg2B;;UPs8G{IkN4m4*!(g-BJ2VwM2qVZ%;zq(7@Soc95qR4R zA)A8cUKM??Z`U3YVLl96y_V!GQhktWC&ngxbpUj&B%1oa^+1+HB6qugH`q>UJiT(S z4djv<_jC?=K=rnEj5F>xeQvLwrNEp^rsbZM@`-k!w`8RnsOh%&R%z|gN3-EnuOF!c;6JBd272CI{NsO=VU5j@4Qi%WpX7j50tO^ z<2gsm*#5KF=`vt$F?!;4SP9}~LA3uZ*TLD##rM-Ss)1n8;o-{{@^G2&XPJOi9vrz~ zNNJ;$2i=22df7p#@UrGYkj@%Dzq_5b9z9VFzFa8(JB>Ho&8;w>AI*mmjpr@x-9^yH z#qG<})Cj-gHdMT53Lz3sab4Odg)zRh#5cIV^Yh$)w=QfIz?w7rRi%Uy*zS4!w}!SH z!uviZMsa39@Q2x^~b=zo^@7bWid=AFMoUQTmYGQ39L2; zI)X_o4K!~)58UfagnmCi6{?Sz2N%pws>PHE_dk>_svgk+mb2<~$|Y_{O|if? ztfUNnM_CiR(AEPnk4L9{h-|S}{fCHnSrT+0gTMBW556;*<$XiJ$l1F_OeQ-54K*^4 zyc6^TA}%5!S%VQU`r8ykt`VEU{U>eQxB<{PZl3l!5*7l z0g;Pgu@D}u*vR-iALu|$!=KU$G1O#R?+xVx&)HKHc};$BL~1; z++QBti~t(zoFrfU1L&^iuVuUWe~h+V>+Qn^;J$ZlBzdC+rd%%5YRbHVg^%<}tPRy5 znpghK|5X#b60ew@{||p3u9XSB?5%+}6%G$q)o@Ss-K34h^GfJR&{7twE{B4DeFIK5 zRiK;CaWGL{36)~j$yAkPaKi5PqgxLPAWqh3XeK`wI5k)YO*0CCTuq6EAgUZJ=*p)` zZ@q%wd!F;NiFI!@4Ja`Q=I*zw_g@g8@j6<+2;&hL3$Ru5{NH-G~5!YsQKm@v+cdv&3eG=la5z8pf+q z6QNyU?%U#KGz`6;ZgfSt_*_nvyvKs?W6qYOTCREE!AmT!;TMOps}l$E&f~o2hw^w~ z+}Fq$3boJY3x}^SUo@!k#)CyPW4?1?ls(UeCr&q9*>SZJNqHtFcNO)J!ag`DhB2DckO}(cy9Tph1uTA6VB{~ zn56v7K#>(>0v>^3aGq-Gu@Z3(x?SHD7~Yo;i``dgcRofzJIRCImXQRwEI=t^`=2Ao zIV|22{_BM%eCJzU3C9BKkq*6gmlL3t&q?EtZ7lN4;y>Qnkc#eYe>*(>AOVP_bG|=e zvqSCmmSX3+@)4mG!wz?GD2!g8c=bdx2WE{oH-C|(0>kh-!uuV8koBBkoS!@o$@xgS zQJCkWoL70*)~Ni@wMnf9H79k^p1v!Hh=!w8X^HMP;>k!QCtb3FJ|AAmh)Y7Z7-Fyb zJ@kK%<_W0Y3_)Qe8%oZN)!syWOa_<#(kcp&N-(q(V%uL=#HXy1Zt7%TfMXE zj3@_kk3TP;n_J0^`2y5IW9u(sfbEXVyJ`hbpl#@XYP64iHmjGD zr!Xfaeke^nCnx~knyH4}%yoqRF(R5P6w!#L^GC{~gH&{bzr}KTJr=1RR%Nm1Y9k+2 zhOqQ9Pc%$=C;O{)0wQ>>J?%D71o?9(B6Bd8@k_tc_)qLhm9t*6<6BQcOAMT!iShHF z>Q7d?U9~nudZj4$6VbZ?86*0BNz6zj5Jw% zA;;dXqpq!QYqQZD!!D6K=1TZqkv}aVf%%|A-b5BR0+0aP<(IAxJkeI*wRO}Lh-%t? zYrhkUN0Fig{VI3j!7{i{MQdIMomtrM7;{a6OcSY_txQ?i_vWC?;cf!2dK*S!Gq9gC z)uqlZG6TH}qrGmRT!aWIx1Zj)tAi}gG5!>$!Jc}y_+q|DLu4{*_3oxG?zg5@#Yg)& z!McT`TsK=1qH`IiALoxmZ@AJv&pY};iC%-UKDQCX(>w6|JRgVnZgF_o+)YBm!4}q` z%z?lq$DmMn)(5%1rlcL*$$`QiqPWCi%rPOm+!6Gu0NK>1iW3-C;rVq~UNYv0OFbVx zZDoP|g4<-;Ap`Dc?MY#QuBsPY+M~O_r0a_Io`IFgei>3z`#|=i=pl#|v(t|d#h@>@ zT|7#pY~W$5N4Of!?HY>9mi;kFLp1NMs(;$SUWqXD;nVvlpgJj$=$Yz;#uP;b5*c0L z$DU?&_ktyIrN<^5_9}ERA443iCykvbn`**SKU=+pEzVIc;s}5XCbO*`~2)$ zh#PvwZ}Ra`vMIi2XjHJMM=V>3@NkR#I-JVUx{ zbuWz4;$da@Wa%r@NHjTL{IB?XH2mV_DdyaZLei>H9nmVD&`HzFDkzkIIWWy1&V9mO zI?p2UE^=TI{7pD0-QwJvYZ8Vr`w&eeZZa$%CC7-SsPFOze42E*q zNNzU<90djFI$EP)`8~m{=j6VSSYdft9rvlV-q&8tz`%FT;0GU);TMeuzp^_8aAJv~cN3VCiR;oh1QFFJ-U*Erx z$g1@8z2uERG#q;2wM+}nA6hF^eKGVwQ|&%e_CK-Da{pJ5%|hgZh3sVZIt;pfn@qP=4s_WNN|l&eBEYaB12!G;5BXPF|*Pt+UPXeMAhRJxbST zc@BHy-OT5^2b|C@MO|i_d?tw0rA;YRM!_Wu*T5?ET;#;w-BFz!3%4uT49G4AK(@ot zn3W5;=v#fcUY=nrqP|Z$;^Y(uUhiM5P=-Wep2OXlKTD;!XM0uk>s>F*`)fS2-&hK) z6iv1QM1jau!P|$L6!QThqo-IaoY6bQoIFXrG*qh|SykVm2uxBIr-`TY;rS~Cn|Fs% z$l+=H>8KGG zHOd&Myti^D24$-DpS*5Z1D-p58M}k{=gxG%R8AriL~OLr9X%e32qPnpZ#Vd$uJYIS zS?7G<<%yq9X9wco*7vv8g}Zi8o*H(EaxeuEQa}1{s@NBam^tRQlwiNNjoBOVQOuJQ zJ(*tTiu?Xm`n-C1xlrxs-gs^>9$j7k6L?1{8x2RzWY~J!B96kZR*AXy0aVlkNE9N` z?}uM14l(DPqj8JysZknokczkbAYBaK_rLh}=Efq|oeL?nltoCR#c@I+JQ(%n<_L65 zU=Ef~ry^UrCsJyzl+n|d2LbQP*1P$3$gxP*InXT{xt}@yJLs=3$O=Dx%=uqFk_%YZ z^Shmbwi%M7#_-&fqxi5pk}3lojkqk(Lgk3|Uq8;`Q}qO4gFzEcr))&X)}|cic?Vgs ztVhs4^#-%ZHX@hnzKF8#`r~1(4D@;F!nvZ{V0fFhHz0pB7FIQx{q8M?q6gE~*<9H) zXq?Z;WL`fQz4?|%wvV|)TD+#`9}nZ){MGXR6fL5#&tX52lQ*<9OWBXp07BTh(1Xg zM%Uo|XzyFKbJuh7;aLvXfmMAb>i0gEAo?m5j(*x&a)`G=(-S(ZvuXY)TkxR&wsr!# z{m#+(H%%}apPp~;a?3)3w-ad}rlrH7JVW>k>|f$x=Vv~Q4S-$V0{z23cIcPgBVW=b z+;jXutNVE+6;WDJ%bjqtf>#3lA9B6Mv4^u~RyL zn*XI{4=y}sW=*3~9oIo5kK5Z^+Hw$!j;^u_X*RkY((1JI&KuDxG11%g_@dDiB^T4U z82Buu%#;dYwcxQ6wdQ5#^jwvkV2)lGt@zzaUAJ&S3H`;nCiosf zN4r8Ukt^o*6pIB+#Dl4Omm%|W9@M>bhS2V2G7|Z7_<-)P z2o8C@&yc#R!^ne~CzAPv@N7J=e1kX`3R#atYnExEKs_O$M@7jfCX+AAEKnObYENA~ zCFzKqPX~?%E~Oxswa+OfE7_nd?Lz0moq*oFYbET%_m=Y2;F%{)u0YVEx%O1m0iOJ= zYSA)ILJ}jC4PG~b5Nk7Q(2{`#VzwbloWJ4#dlWO`#`D>TKFRRJonOTue=T(XZAt)g zmX(`k3{L~;@J>dV34dgW;Wn=^--9R7 zKqHZU)0Mjn0+JK18Z#xJ^rIC|pK@p6d<93{1LX(^-qYl;*Kk6bg&vPfqCC;&eNKkX z;8OIaz~sePe=^F`UhuiLYzZ=J?TRrtXFY!WzsPG!MPT!X;00wQ_RkhLQgY+olI_p2 zjTNyJq-TANx@#~S)o~bbxrfD~1uD&7QQTG7-)c5V#EdgS0Y1#sFqaONdlB&@$Jo>bHbta0`Gx7*s4t>+wO*y(%0&9zu3L9Us341=nL!~Y6MUP0 z2$EGsphU^q`|c@vpfFMsjM*S-&`UyA zvoG$uXU3p;|CI&H>q%fM^x%6rLl{QTP<$D6j)LN_qG2{ZRmAmh@GK3>S@2)hS$v#l zg`UsJ4*#is4rONva&5VB?yI!1@qtYya@2}bqPv%fno|v?ZbeDLB@S1nz-`pS{otW}uz+TNO8_WMOM4;El zF0z(kPnoavhTy5`L?rReCG*%n>`PY5XP_HMMXGI&g#F?(VD!MEmBcd_JdbA6-!)7^ zdwQ`?oC@-gTYv!~Rg46l-4l~eeUCsQK9uLRQ99Da(6!<>Zg8AGW8hm*AW*y-a`E%R z&l%n#f=iR<(8*8M|18OH|Ebsav)|n`h*j5rVoj3<1*i7wDOi)?2J^?4N3JM<+;dZZ zb$T20+h4QzKvW-d+cQs{*@yu{(defCs3HlWLV}6n2@MPxiMj^gnLEXxD2*hm(Mcw5_LF^1N-*W1M ziq|Ago{vU=$E`q3GNBL1M%@Alx_ZH2?ryjWNiW#mC`C;!oj^=1PBfm}3I~o=HO>#a zAh6coh_M^ziF;WGKh4)+ueY1nd;2zE4;LTXiL8aksUA6hx&SIw9=%IiYKM*N%tQ9SYm_S^aQ9(`-eLVX=-LO{ z{yE(bqQ85Vn_@@sJhl9E$`8E%5Y9a$eKrc6?8T@kUH!w+*aopLy#VJl zeJs`opyTxS%Bm9sKtO%!+Bc^ju%cWb3hM2FsXt!okL5ZbKd9y(dq6vw@>Xv=ZEXk2 z*V}TOhduD@xQ`D_VH137h!{)W#d&JC{e;4wEfD-Acrb^e7K}f}I$JQ8!TKak7O`L( zl;mxz$9`!C{AkF_*KLHW!kvjPwW{Fm0PlYm$1&&S_ODeY;!=3Cy`oB(SqYb?4T~=> zHUe)6;q1Wk7BCtlHtr>@0awlwqvRJbM~%luQ9H~DXnj9}KyMjvY1Wx&%9cV|KGXR6 zn|kmB7r*c;`9P7Qt_{)^uuA;!?ZtoLK=P8IS_${XACWOm(+bsqZM>nMZdp0JH`OOQ zB9jZ}W1ilYKN|pcPD#$u5wW1+1xhXH$*(Zb9ilVG;ZGE+2mW^JX~uj9KHk zJSqz%C;)cRF0S65C}6Kyy_JXO_4+>!8Lv3`gZfT@Tc029TXSdYg$<>`Z5?t0wZUY_ zp3bS^vB3Knw)uJ5Nqk?fxil%8=n3a2RYN~F;e4K@YFt)u1$c_=AG_|XfSMaw1xr3+ zjxR$<)AhGes9J7QG_W`hntM*AnNeqeqg_Y;XL@^>zT^|4QR4;8pV%6V{uIF9R_zCx z7jW;#{JRg;{rV)5>`|DJbzqBwCp7l_t1`hHh8x|U9bNxLVP5^Pm^w=? z1h=k>Y{O z3v5B?Z!E1>1NM1PWIbF?>yAb@a()+k)3`vK*};E)?bK_D!jG)#Oq6f^{>AD2G!#re`pwY2M6;5a6<{kzWv z(Mw-p%9qDn*RZJz?YRF+wI!nDVxBl{} zn^_5H<%$D&-my^hw)`A(Cg*kZWM<%l60<9meo>aYs^Ws*j|p1eMEJvoSLKCbFo%yn zpYS$T86Y$D?H<*#kxZa) zCh~C%ja^8JhQyQMgO7`hkn5mEwKip4&dyul^z zgh+6!Gf>Ug_ky0sGcPt7Vo;}!7vfVO}4 z(WT*Nbnf*lC#4qwP+raOP~9&GW^=DFdJ5Pfz4(WM8@x$iEu+0nM`(vaPCcdn`Y#@> z+%})Q%NYWL7rZo0=ED(=kF-yzrJ&A;IVmC%AGo}7Xve4?21=)xMG-+MTpVUh$rOr4 z@^tPOUDwr-Gl$NMBeyHE^~GoUwzuZc#;*GKt>;#wI`T#m4z4-ob z8d%1^F-mkwfH5EAE6*^0Za-D(6=Ifuhiz|k02Rl+BgNHj z$k0)f!;4zu?=y+W`5&?S)o)JFiBA#PTms%YKKH5%XTtg*V(f)dL;&w9~k*p4dsKj z^}QL%GDo1a*Sep<83P+HoG66f>LFf@zEiAEh+y@5K{rV1h%iHq0G3il4I2%cBUAbp5+Yw#{C-BeFPH9Dx8bV7wYmm!&OBQkM`qiKk1X_z_%hg_Pxq%B&C5vJbx{L<(_fO@n;@z=iJ;V4Ou*x z9wapOzleZ4$vTRxZJDrI8x$HO-~f(`8*5Hn<Gh5Hh&LDeM}HoIaiDUZ}B0bJ-_L>t@nWNMPV47FQw+ zJ<;jAb0!mcg(KnuUQrQ@DplTrw z>ofVFPkYHO%(VVM!y;KC-5dk7t$*}&abNoLl-P%!88Jw(p>@GLCJde4ZME3k_Cc*z zC9@vmTylV# z9z*}MGcg$67rN1Hkwhc+_Pb6bC<*!+6mFTHQ~}CXwavDBelTrxqa<=F1^qemACi+u z0X2(z5^f=3sJb?%&EPu$^-E4$qL0R?-hTSQ`BFbLXCiARUFHe*K3~Y?;7J45<06$j z89Au$aR0PzYdD&IKs~=lkME^NbA5k)&VzkQpXte5+>b@N`%mt~MPy=NBu=9i48M)e zw9=&{fbPw+ksSEs0>L?-TVRjp&yX*r?o8rzxdETh@ zxWF0PH~xs`&W`!fKRW2s76}O{r4Ed_CYZ>D#Gv4ju2PW*B4Qi1IWM+Wj2K(^PY5r% zz{)jptt^WIRBXsm+I~0-dF|*XtWuSrHxrWoTs&~UR6)5F<76V@m@@L07YKl1?v+OG zE6H$7SZSl9LaT7jZ& zi+(N$xDF0;eZ%?$OQ2fxajs}M2Y$?? zA9MIh`FcMK>r@AiSDL(xH%2PDr^Xx1@m%C=p;A<1GR)DBYxZz?0KI8P#Ffcdh*!B* z_T4H9J)$@)^7ljtx~;gAkaJWGR8yPz$y~5LAKdV=RSvHcWD@+}vI3EqfXi2pgaXt^ zYc0-79g0>C*Bo{sPeXq5!Zw2ynP>|ZzufGKgD!TY+}#(4ET;YcMo#5{yWVvDj{z5i zwmElxjb|agPbDi#*pGb4sn9DI=kPuykU8tR>7zWY$RVu*qG12*diU4v<3Pv?N#HzY zj6SZ_w+fSbq8(Lzsv}ysPlY5sceAh*_vuQK$(uW)tBWkidNmkP$)O)M8pc4Uqv**h zW)64xsHPcL;?e8B{nQW1{ZZH@o7Ze~sc>iFjnJJBdFbA3RPNjAG*njm_f6V=zF;M! zI2GvYfqlZF3f0=!U#XG)JkdH9p6ia?c-Vw}j{eE-w>6r;n)twl{%{YRBCHAA3>kzl z@~tz~ZLc7__VdWR!4QZ%7ET@5eFa_Nxw)E!13kBc_cQgA$ph0 zn&(C>JUE+qqxN(+`1$fodeqm0M>CZoYbfq}AM!V#(9Hva@9UcIQGjsXG4}}SB2bHe zD;`N-4fQN{PUsve1w9v!Ir}tc80LjX2l#V9?1&Tf*DFO(k5O6FKP#bbxl};aDGS1= z-&u55WP<3=3iq$-d0<)hShwb;KaiY$qM3qwDB>q=a-H#WFRHOUAyr%oy=hcrwJnj* zx#8gfN|=Y<^QzcdHwV;*W7pm#*Z@go$KBo38z_yA*D(Gt=HOi&{^k*r4nAM5-kJ~P zMZ3c-5&O2dm-MHW#4EgBMX`=wXfjEMNIo~?m%hR1`hCAp# zoKAwpBR?`O;T}b;__yz#?W90^jo5s~WFU(Fu&VxzG8NQ>oORxRO@!ww-;Nk?hoWB( z_c;G$mjdqr->%>*p~$`#$j-iW2hQi@H{=@p(dTb`v4j5{(em71hTGqQP}oWFht&s@ zfI(m@?gsV~tfj?Urc#GOjFs%CjmI%)_SG30+1oxSgfj5d&dC&n_;u}gujYaE4aXC` zj<~P>l>bH+Z6XRxn(Hmeuz$N>K1K0jnLh)5$Wn0?0$?=L4q3zc}15f}Jz z-G3E`F47E(`Yi?`*?R|-;w}Xt{`^2imYi^ORh(p2O*j|X&xYjaVx8jW4yi}b2LfX9 zYwn1A=ZtD)nf2o>5`paRd=gzxJlv#1tMdI!EGmr1_{4W03KbjZ zeT7eC$gpz(GgF=;2(1JV`i}wDIS@{gT`GX0iI zv*OU$SHT-9>H_&M%I&wA!yrC9{l-ZFg!<}|FXWg6fEZDerX$)I3cCJmJK_B_HUm-W0fptZucbvJE2(q!E7Y5 z>|EgN@Qz3K3Evw|Eu#F)mz2nhA-vrQ36-?T{z^qvIx>1k8h}JbZ52 z3I2<(ILYo}i9RvzYY4bcwVj(|;u!U+g04fjHOvS5j^2yN3aN&}wLylV-(rS{Xs|9; zYV42L9R8yn8;%7zp5-b1OL2%tWj#?<&mFZ0*wF;lxS|%?tHmV&$>4Oqi;BQXK#ay7 z7ku5r(ZUNZ3Bs@+vR{a1Ol;9ZaLdBke)VV0$1X3p>2I^`y>x?}3Ih5{ZCK-oHDVn_h z8^{BF(!n&TE!=yZ&w5-=+5_&{i!gs{^+t!}g4@z-5}}ms*SnMbo=E0**NUr62)dUT z5n4Li2|E3@GP&zAzsK_CQOGN&sq2OKX!W3xv;~!cU0eb!uYB?5Gk?0Yndf z2quoEp@*X8#rtvnJ!-WS}Ucp8R$ zW4~~0C1m6I)4`hU#Bktfsr!fO)evKsA=4&*5^Siw&VRk=hWlJ858s-LKuKd?ay_F$ z5&0}f^?}$lh)DnD2|tvfWc8uN81wfmYs2j6+)?*~?(9#&4(O#LKBK;fhi}|#gO4y@t3T;! z(Duz(q}#m{V*679ZPHKZX(orF)5&rqRImNvv(jc5r6cx(UT34q!Txk(A3hh2Y!l#Q zy~fJpl#Wci=uSyH9P}n-#lQi-*Rr%1j{7)d0o##APIeOmXsqHF?x&ubT7RF_#!s`51+h=uEyzVl^6jg;$;@e?uVMH~62PLDW5*Q!i* z%fSm>6BXSwRmnj-MrADIU*gcz-5wiyW+UYKlMVF=hM_W*{pE6;NBDf%EWvXt6$)7u z*h!>PKz>nTBOpH<&Ye0GGvZYO>KBRFbo3P6Qm)uYlS_ca6^(C&b$HG!D=*GIkNI)Z z49d7iI^4(q}Tzxj@!@};3 z#)Tx0XXE=$A${GZ$KOC?u4G+lQ(FQfPlHCu`dpF1fkyTZ98b}k6SMu3?>vC0om3Gl zR|OBdXZd@mo#B{l`W@#P7ocn=Ua6eSMAjc;*ErLn;pUL?(~Ewws6k}^0n^+=0A4qMXB=fb1mX$$}9kFL0X`R)LfHRfnBmiw3=9LNVI&u)fj+HmyS zxlc8SzO9F)9i7D(QWVeP!Y?^o=-I)+qXpJmi!sk{7j)CT7PYrhaP~Ht8r7Z%uWe zj5!XDM7P~?^e%=m<%C;#t1;+$PlnF2a1@A~nwZ+bTo-P}-Ln;sOps|zi^qZ02&Cn@ zzw@Rd8VCoTAhT9$^tZFF?mTrB@NAqe|FSEGYEzmH?f&qEro8*4bNQHS(>F9!@y7*5 zWgK0PNQZ(y`JnIvpJ;U5O^YE;s0P*oB{b%l5>c0KkKoz@0Vy9|`hLNr0?H1_&(0sv zN9?P!rxNx9anDf%@j!eks04A4#xs~f*MrNVtCoRK8Z2NUmm2{~kzOBDIPiVu?6|b{ zCZ7MkD*weEnE-1SKUiERkA}r?rI#9@zKUY6Vpzeox zxtOWOk1s(!M2B4W%uDzoHPD_c-3=eADr#yx`asTvt=n9q8zLE!gHG1H0DqRN1uf(+ z!G?c+jXxDYV}z{r2Xx{0o#wP=Sr;Td{~Ius4A5o3YJD@L1(dhK%2%gr!P(9tR9CAS z)~WxHnT?l2h;2Aa4xt&UQmpHm!m1&t?v~lmXdN6Hvp-3Wb5o^e8GD0@Ex-}v?PD)i z4J(;LUE(+o&Dwst@^y0wu)J0d7BMLV{<;0@sJ|ZC#?1RPaDFS)EBkZda6P!kUO3n+ zUWWNluNIX!YJjYmY1>XY7c66L^honZpgZCh*txF|K{bjf+^1g*y}eI}DN?17ma292 z;jt3f>()~icf1-%-w92BG%>{SezIko3G|+oY5|OcJiB5XnQMy~03)?-5 zjcTLGAhxj(dsaOSzW-1tq7^F!g%4{1qg?**A(1d^+*}M?dMB+#XPu#ahLb0}J_m|b zubk(?>t`DyaLO|HBaSQYwrOxb617az z_e~$ocidP=Ho1=|4<(#w2(m+G+Aa#w2pOTQcEVx5_AE5g7`O za*1NyfJD3HdM=ui`b%-`XBUtNl~Oo8#eHmV3@vWbkAt+ajknwB|FE7QxV|kt0Y_tw z+$}uvA22-mK68KTKX7Cq%geej0gIR9XeeYRV1t_F(s;!r%;XjyDpwhUagK~r$x*Lh zP5!~<-LYY)*}Z;S?EL_IJhOb1B>X+a%E4C}0HUM!ak33=} z2Y7z)qM}7@Cmdt8&A6!B2j`rh!jqe; ze-m*};_Xk_7i|H4n~B+pJne*sLt8Fd32opreeCb9RwJD539b!p#T*ejt=z-jt?(}R z@Z`t5R_LD5?bVrU0re(Y-MSVmXpHDjlGFnY zTT%D5t1XZ;x<5OH^BvP^7go12>Y(}X8_kO33Sj%OQgTVV8gR#`kYZ*g7?qwKNjQml z7lHWbW-jN6Xcar{^>FVys6 z#XW4fyagUhKVo3m|4`W6^H`X=0xIOJ1bmK>$bIJEkM;;1pV@C?u1G@TO4VKgxW#;z zN229m%T8Mv=@JAwp4L4|66t7Cxi;d&LswMuBJtkVUJht-Ug6X~N`%XMrfT_8Nl;B4 zA?I{I3AilEy_oiKzi+g_{dE3BINw{n&3%^$Y`KdUj#Oo$Gt3J)KM?L$I)7>a)s?{L zP+!Q|9Wf;FvGy}vCFbFi2)QdBu?NRtIrb@~EO@m}b*XQ`6Y|d*G7+or{Jv84VhR*N z4#_ixkD{4qub1LYs-ZK8hN?1jj6R3K`&sh01qkqOH~PwINCBw#JvM)QBOZl^HRR6? zB;r1RuePkTI5*kwTZs9DJ#2Ef8!#O71#(m0A>n74CH$#qcF*R%_<1sh#RK#jmFjNn63H`>oV-n$N z(X)ZRAh;Y!&6g4l37?YQg!!kyXi+D5Q0YzleeFlW4D%ta^s)7TfCG}+{QO%|IT`(& zC{(eQNC1#!R_rBWU;f06$E;>KV7VVo`Q@q)=rg{D*+bcQf0b^v8Nu`Se*0y*ooKLo zV|8TJNC9{!zb^aS@kb8D>8n!fe#o>o;oMD`2XODH=aFqPSGa)gj}!((gU*w^yen>o zAnkGRQdeaR<{I$MhI|QtP@?f|GoeHnJ39SrI-m$H9uGK5W9bHI@(Nk2HBo58pro~D zJ_3alC4KJ4{Td_uM!UN~Vdz5{d}-n=05!tAZ`4vUT%c9FcJo9k%)d>TZXQj-{wv=~ z#*=w4&UijuW!o3{MZV}a%LgHeRE>)-h<-r&z9QM^Zy0>_|IgQ;%^g0pyTyJS6G!}p zqyPOY!Tk07)y6r@Bb;BGY4Gn$$6UV?VpmSs!IrWIZ~c+@$amuu$0}e`H|E>1&WC?aZhT3- zt#HubHus5zCO{1l#jF?lpsMr1wLiDI;Zcxv!1?GF2t78)Ef(Gmx4HbJDIKbz{p>~0 zMagzx$~u{4cB2zG-aTKd{n7yU*{vaIh+YvH|+^xf_2xp409J8zdJ zf8b-FVTwyAg1g;|id#3!LG0ACC<4}tK8YXhORXbf&i>6VUgmTttQK^Ar}PxI9|~?& zJd8#ps=rR`ek_4r)5@I(`MyBteaHO8)B*l}U3FJHM}QBFM%7eOMW7>Y{PMs^CP*iA z=2+I^zAPrORfQKtQ0y}9vwbxK&2QZL{q(&UI!u2+@5P^J=nm0EV&7fS%-5GM&+!%m znT2a+qE0AWWTV;@!Rw>?$b~!YTZynq`0>@E0rx+&_Jm(#3Wd0Pk2Y4Q9DwGwT(KIB z8PwNs+%;VAL+-BmD)NuQpz=e(gP@}-nClz5sbe97&O1I+Ht$`RoQC z2a;-uDB-4wLi*icgp>rLKCiUF`nfpWEyX?<`YXx!RALBJ^lCHJ_{QP+vSB5WZ44~q z6lA4ujRA9(x%Bt+G3+b;mN$#W!B3-y$oFUrh}7qzmU%`%ZaTn2#&85&9-MtXB{K$T zX%;%Q|AwIL>h1TYni*$rC-_u@A3Z=HF-BbIL*92r}!G=I1AeEZ}l}R&w*udP(_B%417<~>`}u0j>)Ql5V>D-V5mqb zs>VG92LxLqR+FY7p>$ES(--&AwS9(YxR&GU)$GswjH+G2RMAsG(+#ex`g%t z{C@gcvgCUFB-7o{&zf{F%uw9eIt5rKXr9!>xl3%?p8nK$4*~RUYaKGXZ~_XEci- zT&LY5@slUkfu31*C+30SpT>IglURRh@f9V1k^@|4SnvH&afaI(RUI=2vfzVxJ5brU zqNHr1m$k4hki22J_`OUC2@<#3j~{&wXKKCpdug*l(^K22s5uU;yA7YbF`9wW1V`jw z#G9afH*F;$N_-D#33+aMEesuI6Q`rV`c}EYmP+#n0&rB=5r}t8k*xFZM1zVS`WP1# ztUvAvhmQLis23H(>BxxoSj}{JPuw0nyBmbqgzkKEypMkuZrk>1)^H%CnPf9Zmmo_^ zi>tohuy0P_;Huy^d!&6wUG~CeG|o*Z!p6!n<>$@w=$NOgU0vYp%( zmhRZ6vB`y_Eeq0&WHUY3A^F{^U#1LiE$b@P$O!1e_k9|ZPpN2J!05ph51enE(zM5Z zO}yWm%rFAHZf zkHrJyU(d8Jt6dHLK%7*4BMf5_3!Mkp2N@1C%WN=l)GF;D10$DsV}EjuTU9@ zJ*3M0>>CW{ML7g%S`uOR95>x7|6Fv&m~i;TTd6n;OA$li=lI7jo|$ROT#^$cTMw=DK@R4teT8(;$CIieWh{`fq9-ZonLt=(M{N%CzDjv!! zNtnw4MZmm>BK}Ce3ATxKk+?n z7Vv>2NAQqzEV!x55I^6}fwY-Tnm?li@Cp=Z(pbwuk;5z`i#>s;V)=JzO=>h$33^i8 zN{vP#ho`?D=?{lu7dyjL%_d(o- zXZYp;A4n-?DB)`HLB%ZYv}_v6C}G9y?30jqRMtz?IvtY+BOS-S40XFfHSKzkiEkxr z71k^(Zsh~b8c%Hgcs#fZIkAURVh+2(l`9XiUQXbNVdojezA9CMTu_!B_$C}6MhIcw zjk^1L4XZNLPbwf&W>^XJbAQT7nDvoUiNOjFR|?`XcpALAPCz--^fez25>ch$Y`k|Q z?&+Zzosj%NhPL$|2$p$kBki22HzN9&lU5`U$aU2K$m5H+xg65L%Eid`)Y&i?j=1ok zD=8YZEjzBMxa0!$>FDc30|)TAvco4xngorLk7{FxxHs(LY^0T|C(`MvW^8K7g6ns8 z1!QV75ZSt3_182_G-KR5raNMa%yR9pS?trAYgN?A@dNmKDmpxLy~SB20j-Yjpmx-T`RyZfPlhAOSod`ts(4BqQ>>Cn)Cw%0M_<_I66P9^BJT zAg5W*hQh*KkK=j%ki>sEA@D8%PAj^{38m)2e=lrF_1Y!CZt#7V$mx94E-~HlDlQlV zQ_>eo3k#4#-kzUdQwZ96dtPUXG6+4Rx^JACnuP+(s_n}3EO8FzuxwCmG%_Ji={3=+ z1mTI`yet(na1eb`OCK4FwuZ*sY%eFEm?^)0iNX6&|HD3!rMns|t*?xhKDmN?u1D;y z^ZOyxO?me$Ydli?Z=zG#$puBN%=aGWj6ozxkAt`t;tT8k2#-@182N7beGbskC?XUbt9u`39bL9ZTpMm&XW z%XQFg4_J`fXG~F?8JV!h;t9qtK7fQv0`*O^R zP~PmV` ze-T<>_?2cElZ8GJ!fD$ja$w6;ZZS5rH_$Gh|`c&VHEeH z)s;C&Jfld1Dmtx|)~k8w{@O>@?-Q5D7d!>#R0!SF1OvVoKH22yIhDJtCW2nT;9Uv^i?hiTR^ z<*@h3P-j)b68NPWxbk8iDW``b)AL4WO&)rI^!UQ~2Z?MD;f!~7yPON7VrR=_#WEl* z@%QpVV-h^JxV?6LH5R`8%bPw{o`9qZ+uZpM6ae$p%<(AyV({Hf&clFp7`c(Dx8aP*MJPSLY~eD_%@f)G?eX<_V$N~H zu@FBOIO{hr(JA1I3a5XS9)6PtEuKbHpB&@hO%dzfkxrcByv+6@^+OW+RP>y3{%r+# zN;l1o*_R-fOG1CosOrKuvguqVxm>g+=A<<9HWDH_+d8^Up8>pMdWW7n1S%F7 z{^lKv!Fp7HkOprmB5^U8@IDcSMpnckD7*3@c|=t+l_Ce}E`7S5H|&ExvazJj6VgCB zYGoyDSqYi*)c>-fNPw=WGtvtp*eBETdOh?_A{2Z+^wZxs8=m@G9#^sSf>!3B4iTI$ z`gQ8zpC9sAClj%ms5f~8%It0**k|!M*mNMe#ylRq7rUkKo6`(TKN;tC_(!0qQ3bhM zRkzXR?NY&1j&$JNx^SD`AO-Q5-s^Y}7y=7CN`Wylxj-~re;pa_0Ih`vbC7a2ycvnm-KgQXQ{K`8v7GX*UNq=v5+O6TgnR9JcaK#e)j@4uN#Zr;?d}g z&sO)Lm`F4glOZlsmWVdZ&nf>q;ed6TKyIdS5xBD<9`-)E5Y|hiE%l=G@&9*H@<6*g zqPaM6+|t_*$~Y*uR8~_EbC^L~IK37m@RA24J<0`*6y=QJLEMjRMfdj<=6?)vetGz- zJ{^7R8FAy~bcGIkjX}@ zz8ud$S77e@;otWmzJRXn>3tS3|Dme%N;(YD-LKJ|T1`N)glgp-is$IV5p&kdPpzSW znw#kv_Um2_58{o$99*4aWUMzL^q@Pm{=hfvww!wIofdq|4(7)dZYkm3hYQz!_Hyf| z1DhSIu#Rgb@_Y5rURpmJnUR0a9~32^-H+AJ$)Ee9!~-^WU**dnS^zwBC>*=rHVdwg!*R4WmD=6lbvc?F@Ot1pfl?#Cb}-fyDrxF5(tzKk_}I~#U= zZ;BpvaRLgPg!qq9)o5rZ{6B0}pK;Ub3bS*ZGWd&jPu z5gJms{^}Y_9Eb^(+!;FMjL35KFp)0{bB-gz?jO$s`xEs{uiiT&EB!+r(F#V$_uft6 z2MM{Ta!ZLe5Br>3uF}#?yhujMv1B7@IHyw3Y+@dACI>wmX!9GB%|dT7g$YP5AHL?d zCAYp*M6V_iL@e-oadBC{_VjWxjOugjJE$asO#O)Q#HVC%D>0i|nRG-pwI81`v}S^4 zx9-)!zeOPIa(%$?Xcl;i{w=HXjfUcf8lJAL8HiFYvj6z%|M@+u(%dQWAm$SBnBkTj zqU&qy>F{wx?uTx>9tnSjoGiLavM-uLB~L`a9T5T$f@2o@lp~?`uDto+O)KQ|@%y+W zMQFT;6>g@7FEa{YQc*4i9SnkfeqV{HGTev8IUj3+^Ghc? zWImbD*&<|fMZM`_ETU)ZNl4Vm0x;uBiK8q)fA4HW%%3Sh@eFO}&L$M1S0vA98T%q( z^XMZ_*_>qb`^F)X#e@WOrC;xqgMlqnRtE9(@94vYXP?&YDY_x|i?B3~dlE0-N|5_4 z;*2Jj=joYQieOs2>eJwQ8Z@}xN#8l+30~KFG+GPrK=cS@igy*}z>0~li?KchD}{5@ zS4#8Xwvd0a-f|+OM_nirUABUSfH1qNr-A6DgWZ*UdUNzNU|}CKwk-T|7kORuq=dYV^1Pauq2?$ zrPIl@j|u2z6Pb(EtteQAjIGak1#nwOMsxc@1O)HbaZBR)gp0+Xg!v8JLn^$he|t6< zU9hM<{BlqS?pCz2mb68q)tEQo`Xf1TTd%v1@m>+4(0Y`CpJkNS*~?j)m|3b9s9}0U9)pSsq=^0;-`J%KPtJK-0ePvtq9obPiqCkNA{_RNnC( z9h=8_eXnBzSL!m(^BE_f4 zK_wWf<~Y^f78RmBxzh3Lm!uF`zv~l+XBp_xo!;aZFUkbI8*jM32Ci=52weavu1)Q=U4U_i_hK@&K$9IoJqy0eF%k6|{BnO{1$>vf(`Z4k2 zvxyq`Rys)(wvPvkM|@RZ!kysM1H+323}#5&G^f7W`~fhh8Fly&JmE&n1*wyv_hIm4 z#E-vA@$lUVygcL;;6y7+t@h_<@WrudNTb~sRtH$?LtO>YirP}0v}ph;cq=~rsloxy z9(~0;=7W1cGUo{9Te+Y%LUZ-yZ%|T>jv2A=^sCE1C(OAxLATu* z;hG5ksZW>&M}0tDd|&sYdl3A~sa4vwbB6q~UxO1nsYq~Hwu+$c3z=mJ-?R+MAjz^e zgZU56Vd#(soX7Xk^_QVjt<05Z%`ErN1E)anytAsR8CZdGdKpOE8N%SqcN$TtGYN=X z47|pVq@a9ktq^@natQ(bXm>3?h@fsa*qhk#V)e!8!<-Pg;BI& zm8a=+5i|t;ESoE-0G%MtFh4UQ*grV1cWxW&yO-q772lU#o*||4jcBoo0|r(d&`J`Zo_vj1%>>dR55ssGjHS*&3ctEtqWv4h6{bV`#>v(QD_Svi`6zZqJ1#2O% zbxRH8w|KZn&MpXiG8ice=dPj?g%1)LZxSK6Nb7ZoL;>*XvV9b0jf3FfH~a6fF80S& z<2f~rBeLGYGzLS=wYB^jQFNvlp8qg4YgP3HdHn;Y7R)tJQp@ek$x=<=WN?Yre_agK z-SZ_;3E^#93XIE{?G|)-NJaB3!tXD!EBS4jr&taW4y`} z(cH7<1zQ&<^nvTy?Zxn1L@+*B9FY=*_&SYPOtS6KdusjV(6DU85qlknVZuQD6xlJU zDIK8G@D1>ZD~0PNU#~0M#evk$PZ>0u0%l!jn&S=QkU7Qsz+KF3Pw`kJ9{H7k=nO9S zhOGsH#J>}l$|IfEel^+i{XR3o@!Q=4XGgLx6B zsT-UI9FYu(FJ-V5&YRm``Xry1i2|{c_NSHy?3~fHb(Zl5CHqF%iuqie4|%-ohEicP zXjmcU1LokV<#7L}nSd_Zxe_cl@{!&~{HGS$XTaTIS4_*H2SOV(3^y#)L0O%O^QvMA zoGjILHalvD`tzA`+MM&@1Fz@fNZS~=<7iBBqS+h$sgZj)kGaf!ek}LoJOj|k0W$|( zwG<@(rs}3I&L5@8Tm2Ov#~jpeb^8yZ3Xq-m+&4RcK;&7y!x!0{fkgjn-+Ig(gal}{ zh-~yx$j{;J1&$46G`-np#O@o2WHbza(s-n!r32GEy*}|M`jYCcn~IVs^q+ZS(=m21 zyB3qT*JFw#-YA{F<6;lHnXX$KGv^nRO{Idrwft(N}1Ya z`Q940t3vYq3pX<+9T4xC1D}h?{19bpjdRlLG_={? zv#wwt2P>4!3hyrYAq^S%Rt5T4G;o?n-cUXq49OD@OU5T4PtV<~v-kaAK;1OXZB+}+ zguSy;zm*D_H+v8Dk))ya(Nj~};>Cz$rlEy|lMk}CwAT2jEm04Tes&fU5!ExU%pMgE$G zT0O+%;I`H8Cy3Za((}nTUC=Q%_H_Q~F!0vcTC%v_MaB^~t$)cW~NvA{*7xLUr7M;(Mizz+D%aYxEEM)r1vJR4B&5u>*9uTp983 zOy|QH#UGe6an*E7$kG}0jo);TQU5+e`ogN z|L6-cT5farun*}!1n&NJs)Qbrwn(+MJTULoyP1<)2!6dBSDqZs1x9Vf@yJ8@a7N&X z)u?woOuU!m()k$;f$W2hYpWTkX5gY`rxc!ZX!n1fSjG8IF}j3|zAB)-j|?~cjDftc z|HHC>JZh$LWDnZ4MSmrQFR}`ELd0Bl`MK}`sQiaf(;ufnL3-z*l=KY92@F^`oSlWE zDEqoU+cXf}4$i%^o`q$By+4R`3gXDGXBkvZ0o~$o;)VVRpfa>A;?tc1nO-yUjkFo? zcw%64CE!1}*4CpY&olw{5)K{^O|M6dIuGCj~O@h~rmcM#zE8|w74cwc8WVIaEof!7n^k6zpz z@VfG;zE9apkP#j_`rnr>VCm4@MYz^$T-5qbmO~fj=H9qmn%@oLJpuB;tqt()&hL6I z(st3>7!Up;(Wzq}lMw;p^mEtZnsH$lgI$lJcr7Z4M`{c06+QMS(TTBWF0LDgR6 z3BQjGu%B((Dicu&cHg85a^K~_wX1EhDwscG-ZhjaHi0=lC10eL6&t~kc%mn4sT6`; zJ%(u#E8zi6F!f~;%+v4rI3uA`0q=@Ubm@69xBlMk8zCIo(4^$K_>H{`2$%PC?#;)8 zp08*^!Cn!x344#D${O(LQ&PCap@7t_X_97WG5_bM@#IKvDZrE#O}iE5@rm4HdZ70I z9?oC|o(30)QCU65fql_D=}zPE-iokR%kt{~ecq9&i^O1wQdkRCb+J%MMf<$uK^M;x zfLzJ-=pWx2D6lyt!veX$oV^DwVHb|c41%1gbjgkDFF|V5ibis#A4Dya8!{G#VZt}X zG5f|4@M-Q8a)%FtRo{6-&8`9XP3YJq(|rkYN51p2VL$GV)xAH%sU46>=9fH4i9t*L zeTE4oop9Vvuwh=R9TJ{eQb-5YK~9$kmzild$c8L0S8R4c7ka`L+RzT2?>(BXWOPBV zko%`fzgAF^e`^00n9Yub;a^yAeHPyfRdv6xR>6L`* z_ve7yM2Sb`(j-h?|9;j^Z3ZOnYCoqBn+8g1kGJZrlaNtUL!m`K0jDptb86ikhq7FK zIhV9?u+OxczwvUhf$>uBG@8ic(4z8+F8JIN}~hRvI?(KV1Ckm zi{#B^?0Zgo&Np_nq6=*GHzapzt09gzl4-xI6D;+Y8ZL%+f_67)SlePdNG22vfmjo) z-t^9bqt#II*1Gk1M=jhlt(i{8yo+-a?tJ0AweUrI;^=Hn1Nc^Idt@zQ|KX;)nGi4b zQQ7s2zs$kCG?{gzUc-5?#at2bP6Tt_DP`U&J?sL#8B?lo$sx$PCT93Bdjv|gQqosX zPXL)XCPpV*oCEGgQdG09%6%LyL)GAwx&D&XP57PuZx$9_mW}eocnXnnKS1+KUrzY#fz@6 zNO|(>_XfA}j+T+N{i<&m`ck>xXjanum!8YyCrkUL+|<)xhMU-J4~PO=#IQP%CtE>=&o!uHfm+Pvk&CW0@VUX$sO5I z`|sXvX;)=X;~s6jKfWhB_O85t;@b1F|L`f7;^tqL>$?r=IA!uJ89w%zuFv`da-WZD z)Fj7UIkx)okNP}6B2QNtKXrbq-LlrVEvl9t*(;~7ebIbshih{6rIvms&s916?6HzA zuMSABjj{31-#;ls&%XRpxAeU9e*gaSYG3S>zJ5R43#$8@3?01Z&Id==Np1AwGY!sM zk@pY9-#QT&EH~}&e_O5AH7V5ZjhNE=iu}B7{;_s`Tg3G*78Z6>uFngdo6_)7j~jB- zikbZaUu~5wGtYf(iPdH!i} zU{R$>GND!L-Z_u{6RrvC(rX0%E30hDEvmI)m%P2+`kC;zYgVNtpBhUH;9 z*2^>FD{T-Xly&fz6}7uw-$w+5AJZNdyXDEIce=e!-Y5hA82v@i!;`Wg_|Advd+(J! zu9hC^e*L&GoUC^J=EotTcSrx%BRBbqvz59=3|j0X-wezs$QZU?Z0YU&u!rNYNE0W)7HotLfFVa(r-t44J=63|?K*-RemAgXnqrd{>WPhR|Q#gPu;lrVR?bNPDOTJin3ydBH??39yxWQ4U|b6nmy zwR-*6>t~6~N=FB6%-<#|e_uKBvZ4IGvo`&|v+fc(GWoklRkz4Cl@I@RCUUZj{pIrV zze1Ju59>8KaAm;}xvKA?pVs;w5|w|vG<)Ft=jHvg*Tme>Q^eGb%Z(3HlzExe>l~in z-cPh%@G5Za!;zx)l~(W`K)sINWs@~hD+WzgW>GY9otD!gNIGoHAE z+B<8x{Oqp>K@VyjkT3H#q^eUE$kQ)p#O&?4Qf&TG+qr7JJu-at+{mGW4$4E-Jco{4 z@sV}+pZW8LUk{06m+cKzjUyh8KKf7hj=t+<(tFlZ_0H@OCn|q1_lK-t*=NeXo3|*} zL3}bbuei!bKC=0j_4;h+(J)x>L&fq#xdGk#tacp45FdtkF1m zv}#$Wv&#KSyL#7MaN6Q6c2781eQWA25og-!RkC!i_@c`E*%9s6$&IOhUYhmI3R$;H zib1vcu$>DXim)I1ueCNkC_b#)`D&7K-S2OXn;9P~em~Y!3%TFr zxahagD`1Z0upCoo#-V`Y>!r5j(u7ro+r*MBJr=xa7$gFY_`M15xLDjyK33cOxvdY#>*0s@ChYw~*n1}Y zCI*fa_9lh#isRFAg4QJgwb5Z~6TG(f&vK2g^^oJ{tDEYNJ>AYUgn>(73Vf=fabG{Hi>Uc6U#Z+g;%Jz{s?L*2gXa9nn~l+$-!nYYw`R;TmWnJY!^%CBnd z=^rSEd=wB-)PJj({>vYC)BJs9hrc_WYIEkC4477XP!%6#{&ZjUn7Ts`%EylCKaFmr z9PciBd+!I=I?>zSv_YMeIm#S?x(7n`9FXp$#na{=3YNc)b#6&qdQ3P+-233w-NSP1 zz@0tS-M5Nc&;IMbvF1gY6luBdGyj-4Q8#MYANQ2+ZI*TEec^*ZIef&xwKJ9Po0d#_ zd**wWpG=(cYT&Qae5IHxZX|CHl7VgN&RnJWSNKot9zOra!}8yQcdk`icSam>kIWk7 z@D*1g^9O5vrpXJ7Zl_N792N5B`XyrP5#dwyg$!SEQXI3?U2JG@PPA*)^WaJM8TpMN zF0}BsRr0{7wEeIC+b-&MZE&>qSY;l}&2}CBYr0Fm`uNS6g!fh`_sOSR`}x-(F=zgW z%HJsWNq)G#=<};%7Rt6Ew_A^yE@Z*c#Oi^wx5yh)|LDBoGi8q9kcXQL4JXSQq3`#3 z`OkKFV%LVYxvzK2j79w#3@P0z@=mYa|Ir3-Is5Lk!Ko9riI(FVSdBT+C=w(_VP*xY)Znb6XQ-sjvGN=057Q;G%ex zo1uzq87yzlZ1qL@jpZ`qWl^WMVasK14cD%PMVsZ`*VQ&HTs}vBk>_}OLAf5iX9IJv z{zj1KJkj;KV9sh`{j$gQ4QmdGrY{SLHXGzCwVfU=(G<>; z-A0T_e40OB{t;#Wq|+%s@!rXVqrtPci4rP~(B+?vtRjW->WUrp%It=lZ6pW4PIor(39<0{RZ>HYA4 zta=k>Y9}AX;*z;)S{1a zpZ(d7KRR67U)~;h`r7fTo5Z5W$6qafv|QF&61S(SQ<<9-o%-nbe*r*@MU1s%I5#drcrOc zn3Lu!nk|hR5O~p7Zr?PZT5h*xGW*&3f5Hb}6yN@yRy9GnuQsjduMr(}heXKffcMsZ zc}(gGznl=QQSJ|J88>(O+}-lrtK)&I>ueDt8}~UnK`ats%ks($9sU)W-^E|){n2qT zs7s4QbNh`KR|^drCiOWg7HL;D`t!wiV(ftc{RYE8G1j=+T_x|deEhn;Ol`MU{5<~V z$F;4>+>SRhZZ^o+BnwifSj&|ARG$5Oxn_epgJtfxn)@qv*eeFL&%Xat_!=>4)v4pg zi;ZRX&Zh6(FLqpQQ+4b1og#GBkE$k5_Q>0VUT=S;KPUhBt=YtNCmYJITjrb(=|5Qh z+p}csqQo;&e4FKSwwAB(|FlT;`P~yDIilr{=?#?ihkAVeu>J1ovS{|VmZ1TD;)i9w zM$UQflnC2CdF~(I?h%O*nt?;DlYx&UW0YT#q!+yQk}f=r=ukyZ=x7Ww&mfQ)cxE z5lKmnmgdg!mY=-|ZWt99AU_s6KB`@3tvue{*Eph)zf4>fcIm~KRdUP^r)M_In=4u; zuY6PRG+112lAPx_vQcD+uV++$wMDEs7j!|@YJ+@sN`1Cj(@k>F>1PX;`h>`PhOoag zEy1$(;FrC3U)m*h_bht*+t&T^P`|mG=GeCj!U$rMr?>y|!=%grsq8V+pyU&B z_qfsf_6?dZ-_L3^ep29RF|1LiO(VMmh$_8zwJ{HyEO*_LvD>G5%gsJsM5r+PU!35@lWO6Rs(R_s*P%?jx?^gHGH zkC3(IX07%JkvVE7sV)Nk}CiPEiW!ucGHFH`Y60^443wXcHCUO7K zPj@zl98m7x*|<6BpNnGR+9z$cPhToteLU~r;Ku94shjynC*JcDUxuzqnX!7SY<_9M zCVOC@Sg(HZ=tR@wBICE^kr#4~i7%&QgynqYFGAzj9@LEcM20uo+g>R1JFP*}_9R}L zA}lv*cOKquyZoh9t1HPX7fEy2#@Y`CeJ6JvElc}*#VN7qaM!p4-zn?;1cZZ-;#qLVXdP-}lN=;N;I zpAViXwzv7@^-sIjOW(hmw2AAqMGVV%+4gH?{km^DO)(FM=p(H?m0^G&(`^RS=^7+dFE_OKyYXMLmaAs&nxkiB<{uNihx#bT z711FPqtKMAy<3?d zuRlGt(iU;3LH3zu&y@892G;rWMeD=T|H=A)YwTJsm%RCJMo|C5vfrlsc^5A25EIQ8 z{`q}?vY!3o#8$19^^_~u8kX|2&mpmT_5HsG3|c4R-#-6I`BqR~>)bwI-J%&nd-L3p z@H)ZLIO(QR-Jg;{E+gwyl?VN|C!=a=dl|$t2c?D{|v)XodeViwxb6o3yhTYeQuP*--`ubFe zc>2k|E85K2Cl7AuRIhJ0KRJ9-#(ytoa23xOIDCf88io9&L7`YyH)7_U(T+c3kK$Gk!mJ^=Yl`a&19q(5;XqqW7)? z1CLg{AZM!otQ?_SPtm{q72m%m?-&329vilC!VWn+eZ%m;Gs-;T&Xuk``tg|DHOJ~Z zTooYyZuZ`^;nt2)(|C4x>%vQN-sQ=aT0J`_L+`ZfH2$`~nDSN3*Z)qsD&K4K)#aQ{ z!<9K(X&K#9wcF3n7QP!7#jhLUFF)+i{@D#>-rke8pA@Dx-!99t z7tQ-7>43~G8S*Nv;|`%39q@G0kv;MQuY@CqN_LCi;)c%tXiuQX?y&BDPV#=y=GBAe z=F$7bi~fPj|CzZ>HXL<+=-a@PGJ5}`=HY)U_p5qO=+u6Svi{Y>8!t@R`$R$h(rO=n zJy9OGSvCJ<_W-#_of^Gy`(gR4pRxb+W2;5=zRNC@jyoW#Z<^d{+Kj_;Yw~}kbCmmf z)Ri>@{JSlbk3aD~<*i&lT5|fi&q8Hg&Fm?+H&3-}7ryP1j`+pS7QM&k{W*N(PjdR_ z@#F4wog&wJW}g4C(s;3J_Py5^8!MlS=lRv2bM3O27}xN#=FJYt@a&$JkGgD@!!v%| zvbN@Yxj&`GkTqAgOVJ}Jv_qP=yx-u^gijm!i_emm_`TodkVv+TT=2b6<^*o(d)0h! zkFqY>%m$wxo+j>jmKCm8cwBsae#HKcAMO&l=AXwE{_=}>vgu~d-dcy`-!EU2_2N`b!?&>{$}}=>b;l9LB6ZT|2!r@wqLb2JL$Wf^1X+~YwO@A+O3OqHvPj-Omh$XewT8- zX8whtgL^bo=9(nmJmp`yTdW+pbz6GpeR5d3Xw=l z`=2Z8jWs%9YJPW?Y*gDTHf`G>v9$D;)4q9&#o4e6`$}g83Xxs^WR1q7W!F35?fvZ= zrD@Q|GaCFBEJiqQmkHQ$RLER{i7-FwZ~Y?mv)a_A2IsjNFWH~dt!nbSn< zkD&waZ(b*ZJ8k>2)5c)wKl(t)%hqe%=e-W?0&R@QB_wPc+uW#z#Qt98H8Xd27eT5WfygS6#-6nhaJ{@`S z$C2Xgt#201mA>K^?eNdLUn^f{GRfo_e8p-BD@# z{_O47h05`6*D)z44C}?jFWQ(+DeHt5b?|;M#CL?~ekS|j%q58z{Vawc2;=RXbSNiNY zB+j;p*r+J(}N6xlZ%I;NE}N`f;PY+_ZUW+}3k)(w0B(&)>I8 zPU*3}dpkdGsq$G`w62meCm|vFe7N^H@v2$%d#!(%E?ZVJP8#I5U%r1kvei%>n^Pt#pR;bPoa(GCmFxeO>K6y5 zPLcLofAvosw?t~*B!*n@-zQI(n4i^j?~~J>etUC7_w91UE&qw{f4oliJ-FpjzqCn0 z`|FIO?h(`F56M2p-{)^o&V$sMx!!k){H7>o^USgUSu=c$ST<>|tlxM|V$Hhyg?7}{ zpH40Cm$i=k_t(lH$~?N5)W3a-*NSnQ!scF3&QC|E`e;UPJS}?7*_s>rp)v<1E3J?D zV2Dr!tXgVP=4)*`{ob{1X}iVftR>0Q_xp(pU;k>Yt5N2b{c$b4O0#|P{M7mvGy~5n zt3mn=dgj?7#yy$$elNo+`P(GRsMCj6$q#ErwO{#SgBaQ{CidWz!6K@o_^h3B-AH%s zU&G@k?-u!Uf~(B$>nF}!|K?y9Wxb4&FM922UthVuqVnnOKTK8LUvrj+EbH^L%*YR# z*z)`l`QhEk!_$@XWUJK#ch#CaysiqT;40M{F8Fzv2vezL6u{p#&6p###L#XzWC-b(ezE3@92|rg?d1P zE|;y!dEa%;t~stisV-~zbqXIS)~{RasD^GgCh{P(1?E}|**gM*u8 z!;<1b`!)v1L8~{Wk63YDBs42^=Kr-`)Y??NeLzy6=%Oq`Ja6Y=as8qysDAoCQvGaO zmt?K4e5zjCZ|zrWW#HfuJ4aUED24?7H)qU+-QrG`{j}}MW*J%d=h(I_R*HVh$Mu`N zM7dvB^?hLEN?)<|aLLxp(hc%Rr5VYkvFl{7mTl(Df3Zp~ntEh?nlgW4c-WZw^43l< z$`u^^*IaKg_0z@8$NlUliwoLlS1b3Gdd7Qc#=Z=e=DGU8wX>D=u6?>!>!Yks)p^5! zD)&Zh6Si0_T=I$c>$;U9nR$sfw~ z68pPs`S3;AQR$rApm~i5f0=n=rzLIH2{HJ$g}ono?GV#zuM57h<&?O-{{#15AFY!? zle|XHuBFUlioZDS(LUvV$5n5V62o_kAN|y++w_aYH+4h4o8MrutQ>J_TV!02%%6UB z^GzkUbvHh!U2X7MG5FWIiOtk!WW!V2#$qXJokH*ey^r)mX4>7R(|-;TJiMTI_DdO99OR6zv!vlb&dGA z!QjN@qYuj2V^=-w*JG<#Irfh?QT-Q+C-wUFd^Jf~FS6cGEnmfL5|OKZU7_~`$zOiC z^JULT%K4V3hYegjXhl%D5h!_1#g@16aRnAUe=t=Pb2vg^KWRl6^&kQPB^y>&si_#Ub}wa`-P{aJ#X`_PhBB0GcvBn$<~v^lTW@nSIyxqk_r;+ z4ZhefV)kdYuQll}@k@(&6K^If=N%K8x=J@~mmPe+n$WLUnXlr%qsh6WbHwcV8Kb?u zHi=J%me%gn;Fyf~Fv3TUIUu4|XExD{+$Um(Y^wd~y4A|{RpY-7Q+nvul}qlBFh7}l z|L3-Q`Y3Z__jwMTh&U`es2jAp^yhkMRR`B}ZJaNXPk4DBu6taz`rz2b{2^OKFrjhbIfmNOQ-KX<{_Z^TlWT<_+AC1Pphp&{V~$HauDSA$=E zvR7VMw)xfB8E3_y8QrRQjo2(^^p307^^3J);mIn>#RJF1o%!mIhVJo~!FLm4zI%U< z{PBmuS&ym=5pmx?@%`<>LGjtpdGqQi_XYis{`uj*{R5??b<`#A&wXU?x&aN}n;a}p zOnyADj#Tc8818f6SKknkab-fC>OUNoE4vknYVO@)>b9ERA8eSdtj81)P;hUbOdRW) z;NG)b-gv36F*Rw4(AV9tq;S&_G3C8cehpUclXX-omDiF=s%pybHI!fFznA}1ReqKK zIrB@Es*>`1`LCMFuZ1(`Oq)1o+Wh~fD1T6X-Y@?jvb! z`R|vCudhE_{4j7ie{ZF@-EvdWueVq8_m%4vLqECv?~HXk&U%qzD}MqTc)X3GS(miW zswvkczI$A6F>Y#KM8|&GXtLI&S_*`6u@9{QSks z;K4TyE`OfqUNKAAdX3-P$MfAMY9@rt|EA`Cp7(xnwtRDcc!1}BK>S#~xjs6`>o_Ph z<(s$pAzsfRQU2^A+Z^U~9Ts1V-}S`Q{s^z{hbNir;ch91i^?h}Rz^jw{bDAtZ?Hpv+@%#V#%UJechfECy9pA+Gs<7U3AX16@zm5_dBrE#t|vun;r^0_a#r8fW z%C~OPn^SD}(_%zL08g|1Pm5#ayD#=M`{9hxm+zi2XV@QS#JA;}KjjSj<*e9UzIi8{ zW&fNNSd{dOCacSXA@_b8_Z@tPLZ;|U;qFP0uZjtX> zqP(3OUc5!lZ;A4@J6d*&yx$h?a?V|Ell$A^q@w@wzqiT%ZSivjpxZdOEzITIOtU9Sfcf@xUPw);t?ugQI4tMY1D=!Hk* zB(G&9dgkE|=PM^?EPsr<2 zdg>{;eM(Q6FLnu8Mii=SCFccr5^kOJZLg~j)yoAw{VYmsSFT?N?MsJ4UD2)CL z!&4YN8iuPd`ZNq*Vf1Pk&cf)|aJ+@nv*EZ4r*FgY7f$bn<1n254Hp%Mll#Nz;c#4r z)5qcX45ydFaT-oPN8mMro{qq61brQW-w1j;0>=^bcLbgz=Q30BXIr< z=g)Bd4Cl{q{tV~OaQ+PE&v5<>=g)Bd4Cl{q{tV~OaQ+PE&v5=6=g)Ee9Out*{v7Ae zasC|V&vE`7=g)Ee9Out*{v7AeasC|V&vE_&=Pz*n0_QJq{sQMOaQ=e#ofpb_9OcIw zFL?iXf%6wQe}VHCIDdij7dU^3^Orb(iSw5@e~I&#IDd)rmpFfk^Orb(iSw5@e~I&# zIDd)rmpFfg^H(^3h4WW9e}(f`IDdunS2%x#^H(^3h4WW9e}(f`IDdunS2%x-^Vc|k zjq}$ye~t6kIDd`v*EoNT^Vc|kjq}$ye~t6kIDd`vH#mQT^EWtugY!2ye}nTkIDaGV zReTl3s`;Cz13+ zBzcLXCnCvBBz+M{ej@3ONOBZOe?*d}NO~laTt(6+k>o3qUWp`Uk@QO>d5ffHqR3qo zeG^6gqUfC{au`MbM3KiRdMJuqM$t!6@CSEQ(x5(PvTQJBnV5BIi-`TNHVZrstx`eKdU+P5z_ly=WXn(|^%;h^7al zaS=@)M&l!zUW^tMAD6;=@D@YQ#^5f7zKy|O480qJ!x;KE29GiHa11VE=;IiC#?Z?#IE|s7 zWAGY7PsiXkhQ5x$Zw$R1gX0+bI|k1&^mq)eW9ai3e8oX6rk7U!`zkHvW`&SP;Ni}P5V$KpH|=dn1C#d$2wV{smf z^H`k6;yf1Tu{e*zc^uB;a2|*AIGo4fJPzk^IFG}59M0o#9*6TdoX6oj4(D+=kHdK! z&f{<%hx0g`$KgB<=W#fX!+9Le<8U5_^EjNx;XDrK@i>pic|6YJaUPHJc$~-MJRaxq zIFHA9JkH~B9*^^Q-VftB4vNQlJkH~B9*^^QoX6ul9_R5mkH>jD&f{?&kMnq($KyO6 z=kYjCz>N8aGrqk1e_<}JOSqkI8VTN0?rd~o`CZN zoG0Ks0p|%gPr!Ks&J%E+fb#^LC*V8*=ZQE^#Caml6LFr1^F*8{;ye-Oi8xQhc_Pje zah{0tM4Tt$JQ3%KI8VfRBF+TPr`W;&XaJSg!3evC*eF9=gBxv#(6T%lX0Gm^JJVS<2)JX$v98Oc{0wEah{Cx zWSl2+{F}_@o@9=LlllCUjPqoiC*wRB=gBxv#(6T%lX0Gm^JJVS<2)JXDL7BTc?!-` zaGrwm6r88vJO$?|I8VWO3eHn-o`UlfoTuPC1?MR^Pr-Q#&Qoxng7Xxdr{FvV=P5W( z!FdYKQ*fSw^Awz?;5-%QsW?x?c`D9Rah{6vRGg>cJQe4uI8ViSD$Y}Jo{IBSoTuVE z73ZlqPsMpE&Qo!oit|*Qr{X*n=czbP#d#{uQ*oY(^HiLt;yexKX*f^Ac^b~saGr+q zG@Pg5JPqe*I8VcQ8qU*jo`&-@oTuSD4d-b%Ps4c{&eL$7hVwL>r{O#e=V>@k!+9Fc z({P@K^E8~N<2)Vb={Qfvc{)Qx=@#Y{dYMX=jk|4$9X!=({Y}T^K_i2<2)Vb={V28c?QliaGrtl44h}+JOk$$ zIM2X&2F^2Zo`LfWoM+%X1Lqky&%k*G&NFbHf%6QUXW%>o=NUN9z+pOdM19I_5ok^c%lJ87Jt(U)2H$)Y#2aFj)VX5lG|9?im47JZt9uPk~s z3ujsMYZl(J=-DjXWzn};_{*Yqvv8P2|7PJaiyqFxWfpy$h0iQ{ISZ#*^m7(ov*_t8 z+-A|&S@_MOx3h4ZMSo}EIg1|8!gUsXo`vr$dOZv0**MR}c{a|oah{FyY@BD~JR9fP zIM2p;HqNtgo{jTtoM+=a8|T?L&&GK+&a-izjq_}rXX88@=h?h}WaB)W_mOOzXX88@ z=h-;V#(6f*vvHn{^K6`F<2)PZ**MR}c{a|oah{FyY@BD~JR9fPIM2p;HqNtgo{jTt zoM+=a8|T?L&&GK+&a-izjq_}rXX88@=h-;V!FdkOb8w!6^BkP#;5-NCIXKV3c@EBV zaGrzn9GvIiJO}4FIM2a(4$gCMo`drooaf*?2j@9B&%t>P&U0{{gYz7m=iodC=Q%jf z!FdkOb8w!6^BkP#;5-NCIXKV3c@EBVaGrzn9GvIiJO}4FIM2a(4$gCMo`drooaf*? z2j@9B&%t>P&U0{{gYz7m=ipq8b2ZM@I9KCbjdL~5)i_t8s}=9t8uQzxf8s}=9t8uQz zxf^7w5S+&&7Ez&U0~|i}PHZ=i)pU=ean~#d$8yb8()F^IV+g;yf4Uxj4_o zc`nX#ah{9wT%70PJQwG=IM2m-F3xjto{RHboaf>^7w5S+&&7Ez&U0~|i}PHZ=i)pU z=ean~#d$8yb8()F^IV+g;yf4Uxj4_oc`nX#ah{9wT%70PJQwG=IM2m-F3xjto`>^1 zoaf;@59fI}&%=2h&hv1dhx0s~=ixjL=Xp5K!+9Rg^KhPr^E{mA;XDuLc{tC*c^=O5 zaGr^1oaf;@59fI} z&%=2h&hv1dhx0s~=ixjL=Xp5K!+9Rg^KhPr^E{mA;XDuLc{tC*c^=O5ah{L!e4OXw zJRj%zIM2s&G~a9)7(0-P7%ya49~ zI4{6?0nQ6>UV!rgoEPA{0OtibFTi;L&I@o}fb#;J7vQ`A=LI+~z&G~ za9)7(0-P7%ya49~I4{6?0nQ6>UV!rgoEPA{0Oy4`FT{Bv&I@r~i1R|67vj7S=Y=>g z#Cajk3vphE^Fo{#;=B;&g*Y$7c_GdVabAe?LYx=kyb$MwI9Kiwskq*^5a)$BFT{Bv z&I@r~i1R|67vj7S=Y=>g#Cajk3vphE^Fo{#;=B;&g*Y$7c_GdVabAe?LYx=kyb$Mw zI4{I`A#W*j&WmwgjPqig7vsDb=fyZL#(6Q$i*a6z^J1JA#W*j#W*jG!UV`%yoR{Fd1m`6G#UW)TloR{Lf z6z8QlFU5H&&P#D#it|#Om*Tt>=cPC=#d#^tOL1O`^HQ9b;=B~+r8qCec`43IabAk^ zQk<9KycFl9I4{L{Db7oAUW)TloR{Lf6z8QlFU5H&&P#D#it|#w_btVFDb7oAUW)Tl zoR{Lf6z8QlFU5H&&P#D#it|#Om*Tt>=cPC=#d#^tOL1O`^D>;5;k*pzWjHUxc^S^j za9)P;5;k*pzWjHUxc^S^ja9)P18cWwe+(V zuUdLqi(4&yt;MgF-qzw+OMh$etfj}bxYp9=T6}BibuG?yIM?A^hjSgybvW1IT!(WV z&UHA~;arDv9nN()*Wp};a~;lgIM?A^hjSgybvW1IT!(WV&UHA~;arDv9nN()*Wp}; za~;lgIM?A^hjSgybvW1IT!(WV&UHA~;arDv9nN()*Wp};a~;lgIM;DpqQkik=Q^D0 zaIV9-4(B?Y>u|2axen(#oa=C|!?_OUI-KipuEV(w=Q^D0aIV9-4(B?Y>u|2axen(# zoa=C|!?_OUI-KipuEV(w=Q^D0aIV9-4(B?Y>u|2axen(#oa=C|!?_OUI-KipuEV(w z=Q^D0aIV9-4(B?Y>u|2axen(#oa=C|!?_OUI-KipuEV(w=Q^D0aIV9-4(B?Y>u|2a zxen(#oa=F}$GINodYtQVuE)6^=X#v$ajwU?9_Mv68fxgO_woa=F}$GINodYtQV zuE)6^=X#v$ajwU?9_Mv68fxgO_woa=F}$GINodYtQVuE)6^=X#v$ajwU?9_Mv68fxgO_woa=F}$GINodYtQVuIKZJ9_Mv68fxgO_woa=F}$GINodYtQVuE)6^ z=X#v$ajwU?9_Mv68fxgO_woa=F}$GINodYtQVuE)6^=X#v$ajwU?9_Mv68f zxgO_woa=F}$GINodYtQVuE)6^=X#v$ajwU?9_Mv68fxgO_woa=F}$GINodYtQV zuE)6^=X#v$ajwU?9_Mv68fxgO_woa=FJz_|hE2AmskZos(#=LVb`aBjf40p|vs z8*pyGxdG<}oEva%z_|hE2AmskZos(#=LVb`aBjf40p|vs8*pyGxdG<}oEva%z_|hE z2AmskZos(#=LVb`aBjf40p|vs8*pyGxdG<}oEva%z_|hE2AmskZos(#=LVb`aBjf4 z0p|vs8*pyGxdG<}oEva%z_|hE2AmskZos(#=LVb`aBjf40p|vs8*pyGxdG<}oEva% zz_|hE2AmskZos(#=LVb`aBjf40p|vs8*pyGxdG<}oEva%z_|hE2AmskZos(#=LVb` zaBjf40p|vs8*pyGxdG<}oEva%z_|hE2AmskZos(#=LVb`aBjf40p|vs8*pyGxe@0^ zoEvd&#JLgYMw}aQZp66}=SG|xac;!95$8sn8*y&Lxe@0^oEvd&#JLgYMw}aQZp66} z=SG|xac;!95$8sn8*y&Lxe@0^oEvd&#JLgYMw}aQZp66}=SG|xac;!95$8sn8*y&L zxe@0^oEvd&#JLgYMw}aQZp66}=SI#q8gXvKxe@0^oEvd&#JLgYMw}aQZp66}=SG|x zac;!95$8sn8*y&Lxe@0^oEvd&#JLgYMw}aQZp66}=SG|xac;!95$8sn8*y&Lxe@0^ zoEvd&#JLgYMw}aQZp66}=SG|xac;!95$8sn8*y&Lxe@0^oEvd&#JLgYMw}aQZp66} z=SG|xac;!95$8sn8*y&Lxe@0koSSfN!nq0OCY+maZo;_<=O&z+aBjl63Fju9n{aNz zxe4bcoSSfN!nq0OCY+maZo;_<=O&z+aBjl63Fju9n{aNzxe4bcoSSfN!nq0OCY+ma zZo;_<=O&z+aBjl63Fju9n{aNzxe4bcoSSfN!nq0OCY+maZo;_<=O&z+aBjl63Fju9 zoA{o=gmV+lO*l8<+=O!z&P_Ns;oO9C6V6RIH{slba}&-@I5*+kgmV+lO*l8<+=O!z z&P_Ns;oO9C6V6RIH{slba}&-@I5*+kgmV+lO*l8<+=O!z&P_Ns;oO9C6V6RIH{slb za}&-@I5*+kgmV+lO*l8<+=O!z&P_Ns;oO9C6V6RIH{slba}&-@I5*?mjB_*2%{VvX z+>CQG&doSCCQG&doSCCQG&doSCCQG&doSCCQG&doSCCQG&doSCCQG&doSC~iW}KUGZpOJ8=VqLnac;)B z8Rur4n{jT&xf$nXoSSiO!MO$J7MxpfZo#<)=N6n>aBji51?Lu=TX1f{xdrDIoLg{i z!MO$J7MxpfZo#<)=N6n>aBji51?Lu=TX1f{xdrDIoLg{i!MO$J7MxpfZo#<)=N6n> zaBji51?Lu=TX1f{xdrDIoLg{i!MO$J7MxpfZo#<)=N6n>aBji51?Lu=TX1f{xdrDI zoLg{i!MTO&2rM|a;M{_93(hS#x8U4@a|_NbIJe;3f^!SbEjYK}+=6op&Mi2%;M{_9 z3(hS#x8U4@a|_NbIJe;3f^!SbEjYK}+=6op&Mi2%;M{_93(hS#x8U4@a|_NbIJe;3 zf^!SbEjYK}+=6op&Mi2%;M{_93(hS#x8U4@a|_NbIJe;3f^#d*tvI*h+=_E6&aF7N z;@paJE6%Mrx8mH2b1TlRIJe^5igPQ@tvI*h+=_E6&aF7N;@paJE6%Mrx8mH2b1TlR zIJe^5igPQ@tvI*h+=_E6&aF7N;@paJE6%Mrx8mH2b1TlRIJe^5igPQ@tvI*h+=_E6 z&aF7N;@paJE6%Mrx8mH2b1TlRIJe^5igPQ@tvI*h+=_E6&aGTOWW~7^=T@9sac;%A z73Wr*TXAm1xfSPDoLg~j#km#dR-9XLZpFD3=T@9sac;%A73Wr*TXAm1xfSPDoLg~j z#km#dR-9XLZpFD3=T@9sac;%A73Wr*TXAm1xfSPDoLg~j#km#dR-9XLZpFD3=T@9s zac;%A73Wr*+i-5fxeezwoZE12!?_LTHk{jVZo|0^=Qf<%aBjo74d*tT+i-5fxeezw zoZE12!?_LTHk{jVZo|0^=Qf<%aBjo74d*tT+i-5fxeezwoZE12!?_LTHk{jVZo|0^ z=Qf<%aBjo74d*tT+i-5fxeezwoZE12!?_LTHk{jVZo|0^=Qf<%aBjo74d*tT+i-5f zxeezwoZE12!?_LTHk{jVZsWQ^8_sPwx8dA|a~sZWIJe>4hI1RvZ8*2#+=g=-&TTli z;oOFE8_sPwx8dA|a~sZWIJe>4hI1RvZ8*2#+=g=-&TTli;oOFE8_sPwx8dA|a~sZW zIJe>4hI1RvZ8*2#+=g=-&TTli;oOFE8_sPwx8dB5b34xMIJe{6j&nQC?KrpN+>UcQ z&h0q2UcQ&h0q2UcQ&h0q2UcQ&h0q2UcQ&h0q2th=T4kEaqh&q6X#BxJ8|yBxfAD3oI7#u z#JLmaPMkY&?!>th=T4kEaqh&q6X#BxJ8|yBxfAD3oI7#u#JLmaPMkY&?!>th=T4kE zaqh&q6X#BxJ8|yBxfAD3oI7#u#JLmaPMkY&?!>th=T4kEaqh&q6X#BxJ8|yBxfAD3 zoI7#u#JLmaPMkY&?!>th=T4kEaqh&q6X#BxJ8|yBxfAD3oI7#u#JLmaPMkY&?&N+T zC(fNXcjDZMb0^N7ICtXQiE}5;oj7;m+=+81&Yd`S;@pXIC(fNXcjDZMb0^N7ICtXQ ziE}5;oj7;m+=+81&Yd`S;@pXIC(fNXcjDZMb0^N7ICtXQiE}5;oj7;m+=X)&&RsZn z;oOCD7tUQccj4THa~IBCICtUPg>x6qT{w5)+=X)&&RsZn;oOCD7tUQccj4THa~IBC zICtUPg>x6qT{w5)+=X)&&RsZn;oOCD7tUQccj4THa~IBCICtUPg>x6qT{w5)+=X)& z&RsZn;oOCD7tUQccj4THa~IBCICtUPg>x6qT{w5)+=X)&&RsZn;oOCD7tUQccj4TH za~IBCICtUPg>x6qT{w5)+=X)&&RsZn;oOCD7xw|WaPGpn3+FDJyKwHpxeMnmoV#%D z!nq6QE}Xk??!vhX=PsPPaPGpn3+FDJyKwHpxeMnmoV#%D!nq6QE}Xk??!vhX=PsPP zaPGpn3+FDJyKwHpxeMoRoV#)E#gME~y zb7oau74whZS9!gg^R4o#m@oT*%In<&s=O-3$JSPPy_+km@~W73Tvz4wZVtH0t74vP zJ(btHImarmiuvmeR9^4q$f~?5<{LLsdA*wxuJWpwSNXBZ>)l*ql~=|5^Cl{|d$8-pvhHc~#6K?ymBBH^*G%RWa_nr^@Ty z9AcGM#rW>tDzA5Q#8qAu^M}7ydA*x+uJWpwC*Dux^=|I4%Bx~rc7K)EySd{3*V27J z-(BDL{r}y)_e%P2+O%n#rvL1wNk;HJ-fx`rT9G^q%pl@#J-Z@0}W__g+tpC$CF>|I|3WS9oeXdEfO1r^e~M z<5T0w`-JzM8mISMPmL$$@|H_Ff~r^37#5H-e3L2sd0Kw z`P6vw`_T@V8mISGPmL$9?>=y9ynFgJ++Ud*Pu^F3(9}4+w|r{6@AUtH2TzUDdjY1# zlgH0}eQG>^`nA#Dm>N%B|9r^QIK9VwYW!ln9s8(5r^e|ybyMTX`^>*RHBRplm>N&s zNB!NYaeA-$)Ohkb`R`AS(|f3=#*_D(|8QzNc@6xJrpA-^PycvooZd?}HJ-e_{ijpo z^xo;I@#KByKc5=^a{9IFznB_N-Z%Zrsd0Mm`P6vT^y|`pJvC16^_?0|-hci(&JW}K zaL$k5{7BA^;{0gNXK+4~^J6%l#rbT`kLCP0&X4E(1kUGhej?{5aXy#xlQ}fd&M)NrBF-=7{1VPD z<@_?vFXwy-=T~sPl=CY&zl!s#IbX*4HJo3|`E{IM&-o3U-^lq*oZrm(Eu7!V`E8sp z=lpigS8#p@=XY{`7w30#eh=p>IbX&3y`102`Td+f!1-#W1K(E`4gP4=ln^|pW^&!&Npzrk@HQQZ|3|N&Y$J{InJNwd<*9W>B^Ie?p=6nz5dpV!V=y^uZGkTuU z^NgNn^gN^I89mSFc}CAOdY;kqjGkxoJfr6sJPp3(D+o@ewtqvsht&**tZ z&og?S(esR+XY@Ry=NUcE=y^uZGkTuU^NgNn^gN^I89mSFc}CAOdY;kqjGkxoJfr6s zJPp3(D+o@ewtqvsht&**tZ&og?S(esR+XY@Ry=NUcE=y^uZGkTuU^NgNn z^gN^I89mSFc}CAOdY;kqjGkxoJfr6sJPp3(D+o@ewtqvsht&**tZ&og?S z(esR+XY@Ry=NUcE=y^uZGkTsGe|-}4jGkxoJfr6sJPp3(D+o@ewtqvsht z&**tZ&og?S(esR+XY@Ry=NUcE=y^uZGkTuU^NgNzei-M6bAANpM{<4?=SOosgY%i3 zAH(@9&S!IeEa%5@emv(Va6X6g6FEPL^SPX#%=sytpUU}ZoS)A58JwTV`8>|&bAA@* z3phWU^M#x*;`|)W&*l6)&d=xk0?rq6ej(=6noL|ZL zRh(bV`7+M0;rv?8ujBlB&TruSM$T{I{ASK?;rv$4Z{vJ9=eKjdg7Z5#zmxO3IKP|o zdpKXo`6|xu<@`R*@8|ph&R28(AmLXZ1X*=UF|^>Umbr zvwEJ@^Q@j{^*pQRSv}9{c~;M}dY;wute$7}JgetfJLXZ1X*=UF|^>UmbrvwEJ@^Q@j{^*pQRSv}9{c~;M}dY;wute$7} zJgetfJLXZ1X*=UF|^>UmbrvwEJ@ z^Q@j{^*pQRSv}9{c~;M}dY;wute$7b|2+8zPFX$A>UmbrvwEJ@^Q@j{^*pQRSv}9{ zc~;M}dY;wute$7}JgetfJ?H!|&JXAO2+oh>{3y!% z`FWh5&-n$MFXsG0&M)HpV$Lt&{8G*@jU%`EJhlaK1M?o|^oT)AO93=kz?M=Q%yk>3L4ib9$cB^PHaN^gO5MIX%zmc}~xB zdY;qsoSx_OJg4V5J3L4ib9$cB^PHaN^gO5MIX%zmc}~xBdY;qsoSx_OJg4V5J3L4ib9$cB^PHaN^gO5MIX%zmc}~xBdY;qs zoSx_OJg4V5J3L4i zb9$cB^PHaN^gO5MIX%zmc}~xBdY;qsoSx_OJg4V5JoL|cMWt?Bm`4Y~r;Cw0PS8{$8=T~#SjPq+azn1gsIKQ6r8#up_^P4!one$sX zzm@abIA6~B?VPXR{0`3VgRh`7@k9%lUJhKhOCV&R^hs zE9WnAzK!$kobTZLCC*>w{1wh$<@`0yU(by@(?1a1$@v?czsdPqoWITaF3xvzzK8R@ zoKNNTJg?_@Jv>+! z^Ln1w^Sqwt^*pcVc|FhTd0x-+dY;$wyq@RvJg?_@Jv>+!^Ln1w^Sqwt^*pcVc|FhTd0x-+dY;$wyq@Rv zJg?_@Jv>+!^Ln1w z^Sqwt$LZ(ic|FhTd0x-+dY;$wyq@RvJg?_@Jv=vo|4ClY^Ln1w^Sqwt^*pcVc|FhTd0x*sKaBIkIX{B) zBRM~c^P@SR!TC(ikKue4=d(FKmh-fXIX{c@1)QJF`9jVYaefZx=W>1?=jU^N0q2W3zmW5bIKPQB0aDFZ4*KvM5=QnVEBj-19elzE{aDFT2w{gCl^V>OJ z!TBAW-^ux1oZrp)J)E!Pd==;Sa(*A@_jCRL=c_q?kn@K)f0*+}IA6p0TF%#T{wU{< zasD{xPjJ4T^Cvlfiu0#A-@y4s&Np$sne%5jf0py-IDek=Eu6o=`Bu(foL|cMWt?Bm`4Y~r;Cw0PS8{$8 z=T~#SjPq+azn1gsIKQ6r8#up_^P4!one$sXzm@abIA6~B?VPXR{0`3VgRh`7@k9%lUJhKhOCV&R^hsE9WnAzK!$kobTZLCC*>w{1wh$<@`0y zU*~)$=WlTSCg*Q){x;{kIN#0r9?ti2K2_B7qMjG^yr}0zJum8cQO}EdUexoVo)`7J zsOLpJFY0+w&x?9q)bpaA7xlcT=S4j)>UmMmi+Wzv^P-*?^}ML(MLjR-c~Q@cdS2A? zqMjG^yr}0zJum8cQO}EdUexoVo)`7JsOLpJFY0+w&x?9q)bpaA7xlcT=S4j)>UmMm zi+Wzv^P-*?^}ML(MLjR-c~Q@cdS2A?qMjG^yr}0zJum8cQO}EdUexoVo)`7JsOLpJ zFY0+w&x?9q)bpaA7xlcT=S4j)>UmMmi+Wzv^P-*?^}ML(MLjR-c~Q@cdS2A?qMjG^ zyr}0zJum8cQO}EdUexoVo)`7JsOLpJFY0+w&x?9q)bpaA7xlcT=S4j)>UmMmi+Wzv z^P-*?^}ML(MLp;IFwPI>{0PpE^C+Bx@emCd$aK4iBRh-|;`F)(<&-nwKujc$g z&L86ZVa^}nd=2MoIbX;5qntm+`Qw~F!TEa5pXB^0&Y$Ld1Lqq#-^BT5&Y$7@S3K=dOXC|p5Z@RomGr!%=OsNa>3K=dOL|_?^OByI^t?1q z-&ZT?c}dSpdS24=lAf3JyrkzPJum5bNzY4qUefcDo|p8zq~|3)FX?$n&r5n<(({s@ zm-M`(=OsNa>3K=dOL|_?^OByI^t`0!B|R_cc}dSpdS24=lAf3JyrkzPJum5bNzY4q zUefcDo|p8zq~|3)FX?$n&r5n<(({s@m-M`(=OsNa>3K=dOL|_?^OByI^t`0!B|R_c zc}dSpdS24=lAf3JyrkzPJum5bNzY4qUefcDo|p8zq~|3)FX?$n&r5n<(({s@m-M`( z=OsNa>3K=dOL|_?^OByI^t`0!B|R_cc}dSpdS24=lAf3JyrkzPJum5bNzY4qUea^U z599oB&X3^yNY0Pq{AkW+a6XgsV>qA1`E1UQ<@`9#kLUaZ&gXD`BIhS@K9}>8IX{K- zQ#n74^V2y$gYz>vpU3%p&d=g}0q19PzL4`poS(z_xtyQJ`T3k*!1-d%FXa3p&M)Tt z63#E>{4&lj=X?q0S8%?R^D8;Oiu0>EU&i@0oL|fNb(~+%`3;=k$oWm2-^}?foZrg% zZJaOX{C3V)aDE5pcXECg=XY~{59cd6U&Z;ooZrX!{hU9*`D)G|v?%RXZh>@ zb^lgb&&zsV9zXrj>2F4t^}MX->vo*7NfC>brk(^V$EV ztmkDtFOLh;UjY7-vYwa6|9kq2>i@p1=Vd)Fk7vE(%kh=nvYwaqygYvF{x?3k=|7kC zygY7AedS2G^^7!)jmT>>C%X(fOZ$5wCia-6^vYwaqygdGg z$rsSedS2G^^7zPGS1x+bKbG~pJf1vH;eVF(ysYQtab@yF>$0Ag^}Ia(-1Haj|D~+w zv>tv%j5l~A6W3+ zvYwa6$A0U%T^D`7tmkDtFOQ#i>o?!L@keDnFY9?(&&zsV9v?XU#qyt*^}MX-a(*4>*K>XY z=QnbG6X!Q`ehcTfa()}<%Q?TD^A()m!TFt>-^KaeoZrLwO3qhtelO?uaehDN4{*Mk z^9MP9i1UXze}wZjoUi449p{g7{ut+vbN&S9>p6dt^QSm}n)3~uZ{&Ov=bJfyhVy4R ze~$C#Ip4zh3!HD|{6)^UalW1N9h|?!`OBQY!uhM5zsC9NobTlP4bI=>{4LJk=6o0D zyE)&(`CiVaDtcbg^NOBV^t__y6+N%$c}34FdS21%m|qURMoujqM2&ntRf(esL)SM%m|qURMoujqM2&ntRf(esL)SM%m| zqURMoujqM2&ntRf(esL)SM%m|GXBfy2krc{qURMoujqM2&ntRf(esL)SM;3o z!#F>j^CLJvlJlcDKbrFyoX_O^7|v&LKAZDnIX{l`<2gTp^EsTK$oWZ}&*l7N&QIa| zRL)Q1{B+LG;QUO^=W#xt^RqZ#!1>vnFXVg?=jU*KF6ZZQem>_HaK4!H3pu}t^NTsZ zg!4-|zl`(CIbXv06`U{S{7TNR;{0mPmvMd#=ht$69p~3`ego$>a()x%H*zr^{=oWH{PtDL{a`RknT%p}s^?Wbuj+YK&#QV~)$^*JSM|KA=T$wg>UmYqt9oA5^QxX#^}MR*RXwljc~#G= zdS2D@s-9Q%ysGC_J+JC{RnMz>Ue)ueo>%p}s^?Wbuj+YK&#QV~)$^*JSM|KA=T$wg z>UmYqt9oA5^QxX#^}MR*RXwljc~#G=dS2D@s-9Q%ysGC_J+JC{RnMz>Ue)ueo>%p} zs^?Wbuj+YK&#QV~)$^*JSM|KA=T$wg>UmYqt9oA5^QxX#^}MR*RXwljc~#G=dS2D@ zs-9Q%ysGC_J+JC{RnMz>Ue)ueo>%p}s^?Wbuj+YK&#QV~)$^*JSM|KA=T$wg>UmYq zt9oA5^QxX#^}MR*RXwljc~#G=dS2D@s-9Q%ysGD%AIACNoFBpYk(?jJ`O%!u;Cv?M z$8bK2^Vysq%lUDfAJ6#-oX_F>M9xp*d@koFbAAfvr*eK8=cjXi2IpsTK9BSHoS(({ z0?yCod?DwHI6sH;b2&ec^Yb~sfb+$iUIoL|iOC7fT%`DL76&iN9~ui$(s=T~xm z73WuTzKrv0IKP(j>o~uj^BXw7k@K54znSw}IKP$i+c;m&`R$yq;QS8G@8tY0&hO^@ z9?n;CzKZjEIlqtd`#FDr^VOU`$oWH@Kg{_foUh@0E$8bvf0XmbIDee;Cpcfv`IDSK z#re~mZ{U0*=bJd+%=t5%Kg;=ZoIlU`7S3Pbd@JWKa=wl8?VRu6{3Xs`=KK}TU*-HY z&R^$zC+BZ){wC*dasD>vyExy?`5w;qR>zsC=|5_EUeoiMp4arersp+1ujzSB&ue;K z)AO31*Yv!m=QTaA>3L1hYkFSO^O~O5^t`6$H9fECc}>r2dS27>nx5D6yr$3L1hYkFSO^O~O5^t`6$ zH9fECc}>r2dS27>nx5D6yr$3L1hYkFSO^O~O5^t`6$H9fECc}>r2dS27>nx5D6yr$3L1hYkFSO^O~O5^t`6$H9fEC zc}>r2dS27>nx5D6yq28*j`PDfKb-R;I6so}qc}gB^BJ7aa(*4> z*K>XY=QnbG6X!Q`ehcTfa()}<%Q?TD^A()m!TFt>-^KaeoZrLwO3qhtelO?uaehDN z4{*Mk^9MP9i1UXze}wZjoUi449p{g7{ut+vbN&S9>p6dt^QSm}n)3~uZ{&Ov=bJfy zhVy4Re~$C#Ip4zh3!HD|{6)^UalW1N9h|?!`OBQY!uhM5zsC9NobTlP4bI=>{4LJk z=6o0DyE)&(`CiVa>Uv(+^SYkb^}Md`^d0o%zdS2J_x}MkdysqbUJ+JF|UC--! zUf1)wp4aufuIF_v>(z>v~?-^SYkb^}Md`^ zd0o%zdS2J_x}MkdysqbUJ+JF|UC--!Uf1)wp4aufuIF_v>(z>v~?-^SYkb^}Md`^d0o%zdS2J_x}MkdysqbUJ+JF|UC--!Uf1)w zp4aufuIF_v>(z>v~?-^SYkb^}Md`^d0o%z zdS2J_x}MkdysqbUJ+JF|UC--!Uf1)wp4aufuIF_&G|CUui^Y!&adPAdd_d){6@}i;{0aL zZ{hq_&Tr#EgY%a-f0^@FIDeJ%*EoNj^PQZ(!TFn71Xz`I(&0 z<9t5nXK}uO^Rqc$$oV49&*A)B&d=lge9kZ6d@<)2a()r#7ju3I=a+JR8RwUCzJ&8D zIA6;7m7HJ2`PG~+HRlg<{t)L6bN&eDYdBxa`8v)Y<@_nB&UbUZhx5IhPc`+tspm~SZ|Zqd&zpMQ)bpmE zH}$-!=S@9t>UmSon|j{V^XB;C>DODen|j{V^QN9R^}MO)O+9bwc~j4udfwFYrk*$T zys76+J#XrHQ_q`v-qiD^o;UTpspm~SZ|Zqd&zpMQ)bpmEH}$-!=S@9t>UmSon|j{V z^QN9R^}MO)O+9bwc~j4udfwFYrk*$Tys76+J#XrHQ_q`v-qiD^o;UTpspm~SZ|Zqd z&zpMQ)bpmEH}$-!=S@9t>UmSon|j{V^QN9R^}MO)O+9bwc~j4udfwFYrk*$Tys76+ zJ#XrHQ_q`v-qiD^o;UTpspm~SZ|Zqd&zpMQ)bpmEH}$-!=S@9t>UmSon|j{V^QN9R z^}MO)O+9bwc~j4udfwFYrk*$Tys77$AIACNoFBpYk(?jJ`O%!u;Cv?M$8bK2^Vysq z%lUDfAJ6#-oX_F>M9xp*d@koFbAAfvr*eK8=cjXi2IpsTK9BSHoS(({0?yCod?DwH zI6sH;b2&ec^Yb~sfb+$iUIoL|iOC7fT%`DL76&iN9~ui$(s=T~xm73WuTzKrv0 zIKP(j>o~uj^BXw7k@K54znSw}IKP$i+c;m&`R$yq;QS8G@8tY0&hO^@9?n;CzKZjE zIlqtd`#FDr^VOU`$oWH@Kg{_foUh@0E$8bvf0XmbIDee;Cpcfv`IDSK#re~mZ{U0* z=bJd+%=t5%Kg;=ZoIlU`7S3Pbd@JWKa=wl8?VRu6{3Xs`=KK}TU*-HY&R^$zC+BZ) z{wC*dasD>vyExy?`5w;qaz544^Ol~s^t`3#Ej@4Pc}ve*dfw9WmY%otyrt(YJ#XoG zOV3++-qQ1yp11V8rROa@Z|QkU&s*c)#kYIr{!vTMTYBEo^Ol~s^t`3#Ej@4Pc}ve* zdfw9WmY%otyrt(YJ#XoGOV3++-qQ1yp11V8rROa@Z|QkU&s%!l(({&{xAeTF=Pf;N z>3K`fTYBEo^Ol~s^t`3#Ej@4Pc}ve*dfw9WmY%otyrt(YJ#XoGOV3++-qQ1yp11V8 zrROa@Z|QkU&s%!l(({&{xAeTF=Pf;N>3K`fTYBEo^Ol~s^t`3#Ej@4Pc}ve*dfw9W zmY%otyrt(YJ#XoGOV3++-qQ1yp11V8rROa@Z|QkU&s%!l(({&{xAeTF=Pf;N>3K`f zTYBEo^Ol~s^qlj!! z`AMA5<@{vMPvQJj&QIg~bk5J<{7la0aXz2(vp8SC`PrN=5=jU;LKIa#3 zzL@h1IlqYWi#fl9^Gi9ujPuJmU&8qnoG<14O3tt1{A$jZaefWw*K&Rx=ht(71Lrq# zeiP?6bAAiww{m_P=gT?2o%0o(-@*BvoZrRy-JIXU`AW`Laego7_i=td=MQkcn)3%a ze~9ykIe&!nHJq>Id>!YHa{d_Sk8}P6=j%CtlJloHf12|RoNwfO6X%;be}?mCIe(7x z=Q-cP`3szH<@`m?w{gCm^BtVO#QDpdzry*eoWI8T>zwc8{0+|EP)BY^F=%LrrY*8qn*0bYx}&>PW|b( zJ@=-aIy7wi{LxO&Lm9O_2dAC7^ha&~e4(A5kMhTDpG(@QQ~##z`8e&=tN*XdS`;Tp( zZ`!GMf7bRnr=7a@pV~g}v{V27bK7%w+Np#8rS0=iJ3Wu(&)c5E(@tIduWg@)+UfZ$ zf6?~2sGU0b-`bwf(@wqo|Jyz%wbOH3{<7`!QaknYuiBp5(@q`z>$cBN?esjCziIm% z)lOag+qTbB?bO%5Yx`W)PM!Vxw$E4X)Z72w_BpGay891ppSRknzyG5>PVcF2rw;#P z+vl%#`u_MowLJ%@ox1!#+dhxA)AM2eSKH^ZcIx#1ZhJmZJN5eiw0%x%r{~7}bKB>& zcIx;4ZF_D|J9YeD+CIOvlk+j>aVO5-!Fk-tb0^-(dEAdpKL+AmoX0(M`eXCEIgdMe z9>se&kGt^S{LT9R^j^;6PM$aQKF;I*>-5jx-_Lp6m*Ob}|MUUQ<5s7iTk%28=W%~L_mA?Q`3UE6C(pO|DCcp1KmE@EKgN07$@8l| z&UxI)b1y!@dEB?B9|Q17&f^ZIUo-zH&f~`ax5dLg&G~0Ik2`t%#(tc~eP#L|KYo_; zxbYOOcy0pcasT}9rXP5~dEE3K0?xm{`Tm^8{b@X1<8QvmdEAq>{c4~8@g>gVP98sT z0OxV<`p`eclO8yao1VMC`GK5&h4Z+h>Bm1D#ChEGJO<9c%K5>Z$4&1O=KO1%f1UHV z^H==ok{^77^SH|<&u!rR5YE5J`JtT0{dxTDQvMd_aZirFyv=WO9(VdLNB=vV$GvCz zu_NE*JnrQA9^d0U?%qe@Z@}+!9`}#qf8AgGfb+PM=YIT<^SB?0ziR*MN1Vq!B%bQ9 z&yP8eJ8}LK&f_kae(c6iIgfkozdLH{$A89o+=r+CHuyQ`ako$ZW%~u^aVL+x_$B9Y z|16%;{LNo+9`|3Sf&7~DxYMt1`N?lMkNd;v$4>l~^WSkEcjEjo&f`v;AI^E)iSr{k zk2`UGB_Pv<=D#Q7PV$DKGolk>O}=kqv^yZ`iaQ|5CXcjEjk&f`v;FW@}x#QE8r z$DKG|$a&m}^F^G;-F;E~0(cJRaVO5tGSy{|^ z+==rGIgdMWei7$!ADn*d`0qKtnDe+3=a+CEcjEj~&f`v;U&eXdiSx@jk2`U`g!8x) z=T~qZcjA00=W!>_ujD-L#Q9a6$DKI8n)A35=gT;cJ8^yu=W!>_ujM@M#QAlc$DKI8 zp7Xd9=QnU3cjEj;&f`v;-^6*`iSwH|k2`UG3+K0T9(UsWHqPTtoG<4*?!@`+oX4Fw zU%`3YiSs)+k2`UGC+Be|&hO$p?!@`soX4FwzlZa<6Xz>Ak2`U`iu1S==l60RcjEj$ z&f`v;-_Lp6iSq|Ik2`U`n)A35=MQoocjEjZ&f`v;Kg@aDiStJ|k2`Vxubi*pJnqE# zTF&E6oUh|N?!@_{oX4Fwe~k0E6X%a}9(UsW3C`nAoUi9R?!@_%oX4Fwe~R_H*+3$;`|xT<4&AE%X!?1^XE8^J8}Lz=W!>_w{RYJ z;`{~9w{jkL;`~L<<4&A!<2>%f`F76ZPMq)HJnqE#OPt4@IDeV*xD)5Ea2|K!{8i55 zPMp8SdEANf*Ex?nalVuDxD)4Za2|K!{7uf|PMp8RdEANfw>ghHalVW5xD)5QIgdMW zzK8R;6X$z5k2`Vx2hOKD_IbxX@7U)Z`@CbHckJ_yecrLpJN9|UKJVD)9s9gvpLgu@ zj(y&-&pY;c$3E}a=N9`*Qwa&f}(i&iO|; z|0w5i(>~|?W1N4S^SEiBbN&g=KgoIAw9h&J6z8Aj{4<=#P5Yel{W$+D=W)|M=lpY= zf1dNWX`gfc19JZ{?OoPUw?FL53>?Q_l#;QY&+$4&d3^8-2m3g>atKIi-(&cDie z+)2+5<~(lN=bV3y^RIIrH|=xIzrpz-oX4H?{F|H~%6Z(h&pH1V=ilZ$ZrbOZe~0t$ zavnGBbI!lV`S&@GoAx>9Kj8d_oX1W3obw-X{$tMLrhU%&PdNW6=W)|M=lo}!|D5x< zX`gfc3(kMZdEB(mIsX;szvet{+UK1AhV$QY{yWa&rhU%&VVob%dEB(mIX{B)BRP+o z_BrQAaeg%CannBMdP~pL0Hs^ZA^|P5Yelvp8SCdEB(mIX|28g`CGt`<(MdoS(ya+_cX*KbQ0KIFFn5 zIp^ndegWrk(>~{XG3OU@9yjfC&M)Hp_ncqMdEB(mIlqMSOF565_BrR5aeg`HannBM zd~|?D$cLwJZ{?OoG;`28qVXUea`u{oL|Rz+_cX*zn=3O zIFFn5Ip;TWeiP?$(>~|?X3lTn{8rB6rhU%&ZJaOXJZ{?OoZrs*3eMxEea`tEoZrcL z+_cX*zl-y`Iggw6Ip_CqzLN8}X`gexit~FpkDK;6=l5}bKj(4NKIi-a&R26DH|=xI zALRTY&f}(i&iTWfKf-z3iSvKud=2Mu(>~{XE$8bvkDK;6=Z|v!80T@*KIi;#&Y$2s zZrbOZujl+p&f}(i&iPZEKh1gEw9h%;!1+ebfudDlMg+UH&SylbC#?engE-nGxW_IcMn@7m{G z`@CzPckT18ecrXtyY_k4KJVJ+UHiOipLgx^u6^FM&%5?{*FNvs=Uw}}YoB-R^R9i~ zwa>fudDlMg+UH&SylbC#?engE-nGxWeji`gKJVJ+UHiOipLgx^u6^FM&%5?{*FNvs z=Uw}}YoB-R^R9i~wa>fudDlMg+UH&SylbC#?engE-nGxW_IcMn@7m{G`@CzPckT18 zecrXtyY_k4KJVJ+UHiOipLgx^u6^FM&%5?{*FNvs=Uw}}YoB-R^R9i~wa>fudDlMg z+UH&SylbC#?engE-nGxW_IcMn@7m{G`@CzPckT18ecrXtyY_k4KJVJ+UHiOipLgx^ zu6^FM&%5?{*FNvs=Uw}}YoB-R^R9i~wa+;pbN&v_~|?6P$mN^SEiBbN(sLKh61PIFFn5Ip_Ov{#nlBrhU%&=Q#g7=W)|M z=llzt@6UPMw9h&JBIjS?JZ{?OoFBmXmpPA{_BrPVa{d+09hjJb_?Q_n*#rd~6kDK;6=ilM{yPU^O`<(Od zasGYIZrbOZ|AO;h zavnGBbIyOo`L8*ToAx>9zv2A1od1sVxM`nrei-M6a~?PCbIy<8{7BB@rhU%&QJf#m zdEB(mIiJD#OwQw`ea`tYoX_GsZrbOZ&*uDC&f}(i&iQeiAJ2K*w9h#|f%7?>$4&d3 z^AkBgiSxK=pL0H!^OHG`oAx>9r*a-Q?Q_mg zX`gd`Gv~K(ek~|?HqMuG9yjfC&Tr>@1?O?oKIi-n&hO+rZrbOZ-^KaeoX1W3 zob!7)U&(pgw9h$T#reIQ$4&d3^ZPizpYynBpL6~I=c_r7oAx>94|4tx=W)|M=lo&L zAK^T1+UK0F;XH2I=bW$Qd>!X;(>~|?QO+OZJZ{?OoIlR_6P(9Q`<(OjoIlBV+_cX* ze~R;`Iggw6Ip-TV-^h8~w9h%;#QA2<9TR4A# z^R1l6P5Yel7dhX?dEB(mIp5Ct4$kAIea`tyoWIO@+_cX*e}(f`Iggw6Ip?o&{yOJz z(>~{XC+BZ)9yjfC&fnzxEzaYnea`vYobTd1ZrbOZ@8*0D=W)|M=X@{cf8czoXP@`% z^PYX)v(J0>dCxxY+2=j`yl0>H?DL*|-m}kp_Ib}f@7d=)`@CnL_w4hYecrRrd-i$H zKJVG*J^Q?8pZDzZo_*f4&wKWH&pz+j=RNzpXP@`%^PYX)v(J0>dCxxY+2=j`yl0>H z?DL*|-m}kp_Ib}f@7d=)`@CnL_w4hYecrRrd-i$HKJVG*J^Q?8pZDzZo_*f4&wKWH z&pz+j=RNzpXP@`%^PYX)v(J0>dCxxY+2=j`yl0>H?DL*|-m}kp_Ib}f@7d=)`@CnL z_w4hYecrRrd-i$HKJVG*J^Q?8pZDzZo_*f4&wKWH&pz+j=RNzpXP@`%^PYX)v(J0> zdCxxY+2=j`yl0>H?DL*|-m}kp_Ib}f@7d=)`@CnL_w4hYecrRrd-i$HKJVG*J^Q?8 zpZDzZo_*f4&wKVc=VQ*_!Fk-Y&pCf5=kMY?ZrbOZznk;-a2_}9bI#w(`TIDJoAx>9 z@8|pjoX1W3obwNI{vpoerhU%&zMOxU^SEiBbN&&|KgxOBw9h&J80R17JZ{?OoPUDz zPjVhN?Q_mQ#rdZ>{|x7G(>~{XKh8hPdEB(mIsY8zpXWSo+UJ~qf%E-2kDK;6=U?Rf zOPt3|`<(LwIR7%|annBM{6NmX!g<`Z&pAJc^RIFqH|=xI59a)9oX1W3ob#`9{teFK zPI`U_=W)|M=loF49KjZx8oX1W3obz9B{!7l|rhU%&uQ>lT=W)|M z=lnOE|CaOLaUM7AbIuRr{BX|WrhU%&5u6{%dEB(mIX{Z?qdAY8_BrP>IG@RR+_cX* zKZf&JoX1W3ob%b7AIo{%w9h#|j`QO=kDK;6=O=JJhx52;pL2d9=O=L2B z=W)|M=loR89^EjW+dEB(mIX{c@1)RrC`<(N$ zIbX~{XDd$&m9yjfC&adM9YR==P zea`tZ&adG-ZrbOZU(5M*oX1W3ob&5Bzk&0(X`gd`Bj-199yjfC&Tr=Y7S3W1PoL`<(N~Ie&ulxM`nrzMk_ZIggw6Ip~{X1Lqq# zkDK;6=bJd+%z50j&wJ^49M5q6Ea!34KIi;7&Y$NzZrbOZZ{hp}&bM+NH|=xIU*von z=W)|M=X^WoJ2;P<_BrP-asD#rannBM{1wh$XcR_BrQobH0o7xM`nrzMJzsoX1W3ob$b$|AF(VzJ1=e&-?ay-#+i#=Y9LU zZ=d(=^S*uFAIG=u;yvko`@CJ{qw$k z-nY;D_Ickv@7w2n`@El?kMhTT`@C3$1H~vH4KJVM-efzv`pZD$azJ1>J9H72^-nY;D_Ickv@7w2n`@C~|?y_~;~ z^SEiBbN+tLKfrn1w9h&JAm<92Xg)u&f}(i&iO%{f0gsNX`gd`Fy~+6JZ{?O zoPVA3Z*U&>n#uRiIX{H+Z*qPp=W)|M=lolof1C5TX`gfc9nQbYdEB(mIsYE#-{(AT z+UK1Afb$=69yjfC&VR)Dk2#N<_BrQ2;ryqZ$GvXy{d3NL#`(`VkDK;6=fB|mmz>8< z`<(M%asF%09GdQ2gdEB(mIX{N;S)9jB`<(OHoFB`1+_cX*KaTU`Iggw6Ip-&EK8N$TX`gd` zBIhS@9yjfC&gXJ|GUsvA_s=;$mGii1pL2d1=cjWXcjEjE&f})9S8;wd=W)|M=X@FG*Ki(p;{009egB;E z8#up_^SJ5z=bYcf`OTciP2WG~{8rB6rhU%&ZJaOXJZ{?OoZrs*3eMwBoZrED+==r$ zIggw6Ip=qAemCcF(>~|?9?n;C9yjfC&R21MFXwU7KIi;C&hO_uZrbOZKfw8F&f}(i z&iR9!Kg4<5w9h$znDa+CkDI=K&iNY7}egB;E&742OdEE5< zbIzaT{5j6!rthC~{ygVfIFCDV{sQM)Iggw6Ip;5OzK!#^X`gexo%0=>$4&d3^Orb( zne(`5pL6~S=dW@eH|=xIU*r6B&f}(i&iPKx-{3s%#QB??$4%cq=lm_s-{w4S`u;iR zyExy?dEE5HFuLPYvwzfqg!(&jdmv;XH2I=bXQn^Y?KcH|=xI-_Q96IFFn5Ip-hb{6n0_P5YeleL4Ry z=W)|M=lmm_f0XmMX`gfcG0s2EdEB(mIsXLbpX5Al+UJ~qit|r%{u$2WrhU%&ew=@n z^SEiBbN)HbKhJsGw9h&J0_Xd49yjfC&cDd{mpG4`_BrPVaQ~|?N1Xqd^SEiBbN&;~f695>w9h&J z8RtLeJZ{?Ood1IJUveHd?Q_n5#rdx}kDK;6=fC0nx19fu^SEiBbAA}-hjSh`?Q_nL z;QUC=M z9_Mk>&(AqOpYsbikDGpe&iP`_FXTM##Q8;>$4&d3^NTr;oAx>9mvDY5=W)|M=ln9x zFXudN+UJ}v;rt5Do|{_ zetypR^_<_pdEE5#bIxz%{3g!hrk|g4elzE{aDFT2annBM{5H;)a~?PCbIxz)d~|?4$kl7JZ{?OoZrRy-JHiw`<(N8IA6(m+_cX*U&Z;ooX1W3ob&rQzn}BCX`gfc z0OzYYkDK;6=MQrJ5a)5zKIi;l&L81CZrbOZui-py+UK0F<$N9IannBM{87#y<2-Kq z`8ns0bN&S9ansMwIbYBDlbpv*KR@UEDbAnfJZ}2=Ip-TV-^h8~^z(DhH*vn1^SJ5f z=bS&o`LmqIO+P>9{5j5_=REGj`4-OOrhU%&R?g$5ea`udoNwbiZrbOZZ|8gm=W)|M z=lmtkU*9Z*u+?=W)}|&pCga z^Ie?BO+P>9d^hKNIFFlte$M$`&i}yq)X+X3+UG<2d}yB!?en31KD5t=_W960AKK?b z`+R7h5AE}zeLl3$hxYlvJp?yBI&xiK;&^{m9=R^B^XrB-5^Pzn{w9kk3`OrQe+UG<2d}yB!?en31 zKD5t=_W960AKK?b`+R7h5AE}zeLl3$hxYl~{XU(P?wdEB(mIsXXfALTr5+UJ~qjPs9k9yjfC&OgEVCpnLs_BrRD;{4N` ze}?n8X`gexALpOtJZ{?OoPUn<&vPC(?Q_n*!1?~1$4&d3^DlD#CC=lfea`s-oPU|~ zxM`nrejw*x;XH2I=bRtJ`ByoQoAx>92Xp>4&f}(i&iU6l{|4uA(>~|?5YE5J`JtT0 zP5YelZ*l%@&f}(i&iQvZ|1Rfo(>~|?dz^ot^SEiBbN&O)f5>^8<`<(M%asF%0On!^SEiBbAATrXL24l?Q_oOaXz2(xM`nreir8oIFFn5Ip=3{zL4{{X`gexi1Twe zkDK;6=jU>M9_Mk>KIi;=&M)9RZrbOZFXsG0&f}(i&iO^0|DN-UIggw6Ip>#fektd1 z(>~|?GR`mOJZ{?OoG;=03eMxEea`t(&adP=ZrbOZU&Z;=oX1W3obzRzU&DFaw9h%e zmh*j z+UJ}9YdDXa_BrQkIbX+l+_cX*f0Xmb zIFFn5Ip>da{siZ7(>~{XJ?Bqy9yjfC&Y$A^Y0l%Oea`s?&Np%%H|=xIH*vn1^SEiB zbN&qH&vG6&?Q_nbf2eLk|!NA~&1J|EfVBl~=0pO5VG zk$pb0&qwz8$UYz0=Og=kWS@`h^O1c%vd>5M`N%#W+2f2eLk|!N8=g8co9)=WS@`h^O1c%vd>5M`N%#W+25M`N%#W+2f2eLk|!NA~&1J|EfVBl~=0pO5VGk$pb0&qwz8$UYz0=Og=k zWS@`h^O1c%vd>5M`N%#W+2f2eLk|! zNA~&1J|EfVBl~=0pO5VGk$pb0&qwz8$UYz0=Og=kWS@`h^O1c%vd=jmbN&v_~|?6P$mN^SEiBbN(sLKh61PIFFn5Ip_Ov z{#nlBrhU%&=Q#g7=W)|M=llzt@6UPMw9h&JBIjS?JZ{?OoFBmXmpPA{_BrPVa{d+0 zgKId`MKIi-god1yXxM`nr{v*zR%z50j&pH1I=Rf5< zZrbOZ|BUmWa~?PCbIyOk`7b$-oAx>9zvBGYoX1W3ob%ss{#(v}$9dee&pAJg^TRoh zoAx>9M{s^5=W)|M=lm$nkLEmX+UK0l;Cv?MannBM{20z>aUM7AbIxaTek|v4(>~|? zIL?pfJZ{?OoS(q?9M0pWea`uboS(#b+_cX*pUe5loX1W3obyvTkDK;6=cjRgI_GiI zKIi-l&d=mLZrbOZ&*OYP=W)|M=lm?r7jPao?Q_o0=6oUNannBMd=cm8a2_}9bI#A@ z{5;O%rhU%&`J7+CdEB(mIbY2Ag`CGt`<(NOIR8E87jqsr?Q_mA;rvq0H8r&)Gc{F=TaG`c@AMyq_;dPy{;T*i{_V-1 zowx`8A7<@?6ZC8s!KL( z+_2Ac*(Litc;Ci-Hh*))zO(-Mg?&D8*TM7uEWdT1nLjvk&99qV_xZOA_g3~_w0)oY zx%19@c=5~o)OY;rH@?z&eV;F_n)M(4ansxT)Zct-^{p#+?X&Lv@A&Ta5A5FO-P``` z;6FWZ_dYA{xbV;?m+jrBJM$0E=hp4rXYHSy{@H)IEi=CNFXq3t@9o*~RiF8F>4N{0 zAOG_5T?Yf?XAXG{0JKdz6z-}uFT3+`!*_d9gU8-HJDj{khl zql5STWox`?ab?enMJ)Vi@6g>Z{t;2tg=SgJ0vi~0+7td!n=a6szZ9KQ+GuN&D$K~-nkQEz0zw>|p t!(hCA*T=qgcl^GvwT;Ew|6TlkoCA0M(}!P--|zDAg`fDt3-S9c{vX0~HuL}h literal 0 HcmV?d00001 diff --git a/tests/test_equivalence_audit_report.py b/tests/test_equivalence_audit_report.py index 3fd438a8..2f77721a 100644 --- a/tests/test_equivalence_audit_report.py +++ b/tests/test_equivalence_audit_report.py @@ -18,4 +18,47 @@ def test_equivalence_audit_report_exists_and_has_schema() -> None: example_summary = payload["example_line_alignment_audit"]["summary"] assert example_summary["total_topics"] >= 1 + assert "strict_line_verified_topics" in example_summary + assert "strict_line_partial_topics" in example_summary + assert "strict_line_gap_topics" in example_summary + topic_rows = payload["example_line_alignment_audit"]["topic_rows"] + assert topic_rows, "example_line_alignment_audit.topic_rows must not be empty" + required_topic_fields = { + "topic", + "strict_line_status", + "line_port_coverage", + "line_port_function_recall", + "line_port_matched_lines", + "line_port_matlab_lines", + "line_port_python_lines", + "line_port_matlab_function_count", + "line_port_python_function_count", + } + for row in topic_rows: + missing = required_topic_fields.difference(row) + assert not missing, f"Missing strict line-port fields for topic {row.get('topic')}: {sorted(missing)}" + + +def test_top_mismatch_topics_meet_line_port_regression_thresholds() -> None: + report_path = Path("parity/function_example_alignment_report.json") + payload = json.loads(report_path.read_text(encoding="utf-8")) + topic_rows = payload["example_line_alignment_audit"]["topic_rows"] + topic_lookup = {str(row["topic"]): row for row in topic_rows} + + thresholds = { + "nSTATPaperExamples": (0.45, 0.95), + "HippocampalPlaceCellExample": (0.35, 0.95), + "publish_all_helpfiles": (0.90, 0.95), + } + allowed_statuses = {"line_port_partial", "line_port_verified"} + + for topic, (min_cov, min_recall) in thresholds.items(): + assert topic in topic_lookup, f"Missing topic row for {topic}" + row = topic_lookup[topic] + coverage = float(row["line_port_coverage"]) + recall = float(row["line_port_function_recall"]) + status = str(row["strict_line_status"]) + assert coverage >= min_cov, f"{topic}: coverage {coverage:.4f} < {min_cov:.4f}" + assert recall >= min_recall, f"{topic}: function recall {recall:.4f} < {min_recall:.4f}" + assert status in allowed_statuses, f"{topic}: strict status {status} not in {sorted(allowed_statuses)}" diff --git a/tests/test_parity_matlab_gold.py b/tests/test_parity_matlab_gold.py index c967fbc3..20c2ad7f 100644 --- a/tests/test_parity_matlab_gold.py +++ b/tests/test_parity_matlab_gold.py @@ -265,6 +265,61 @@ def test_analysis_examples_matlab_gold_comparison() -> None: assert np.isclose(rmse, _scalar(m, "expected_rmse_analysis"), atol=0.25) +def test_nstatpaperexamples_plot_arrays_matlab_gold_comparison() -> None: + combo = _mat("tests/parity/fixtures/matlab_gold/nSTATPaperExamples_plot_gold.mat") + + # Stimulus-rate plot arrays (PPSim section). + ppsim = _mat("tests/parity/fixtures/matlab_gold/PPSimExample_gold.mat") + X = np.asarray(ppsim["X"], dtype=float) + y = _vec(ppsim, "y") + dt = _scalar(ppsim, "dt") + fit = Analysis.fit_glm(X=X, y=y, fit_type="poisson", dt=dt) + pred_rate = np.asarray(fit.predict(X), dtype=float).reshape(-1) + expected_rate = np.asarray(combo["expected_rate_pp"], dtype=float).reshape(-1) + rel_err = np.mean(np.abs(pred_rate - expected_rate) / np.maximum(expected_rate, 1e-12)) + assert rel_err <= 0.25 + + # Decode-with-history plot arrays. + dec = _mat("tests/parity/fixtures/matlab_gold/DecodingExampleWithHist_gold.mat") + decoded, posterior = DecodingAlgorithms.decode_state_posterior( + spike_counts=np.asarray(dec["spike_counts"], dtype=float), + tuning_rates=np.asarray(dec["tuning"], dtype=float), + transition=np.asarray(dec["transition"], dtype=float), + ) + expected_decoded = np.asarray(combo["expected_decoded_hist"], dtype=int).reshape(-1) + expected_post = np.asarray(combo["expected_posterior_hist"], dtype=float) + assert np.array_equal(decoded, expected_decoded) + assert np.allclose(posterior, expected_post, atol=1e-8) + + # Place-cell weighted decode arrays. + place = _mat("tests/parity/fixtures/matlab_gold/HippocampalPlaceCellExample_gold.mat") + decoded_weighted = DecodingAlgorithms.decode_weighted_center( + spike_counts=np.asarray(place["spike_counts_pc"], dtype=float), + tuning_curves=np.asarray(place["tuning_curves"], dtype=float), + ) + expected_weighted = np.asarray(combo["expected_weighted_decode"], dtype=float).reshape(-1) + assert np.allclose(decoded_weighted, expected_weighted, atol=1e-8) + + # PSTH significance-matrix arrays. + psth = _mat("tests/parity/fixtures/matlab_gold/PSTHEstimation_gold.mat") + rate, prob, sig = DecodingAlgorithms.compute_spike_rate_cis( + spike_matrix=np.asarray(psth["spike_matrix_psth"], dtype=float), + alpha=_scalar(psth, "alpha_psth"), + ) + assert np.allclose(rate, np.asarray(combo["expected_psth_rate"], dtype=float).reshape(-1), atol=1e-10) + assert np.allclose(prob, np.asarray(combo["expected_psth_prob"], dtype=float), atol=1e-10) + assert np.array_equal(sig, np.asarray(combo["expected_psth_sig"], dtype=int)) + + # mEPSC trace arrays used for data-plot panels. + trace = np.asarray(combo["trace_mepsc"], dtype=float).reshape(-1) + time = np.asarray(combo["time_mepsc"], dtype=float).reshape(-1) + event_times = np.asarray(combo["event_times_mepsc"], dtype=float).reshape(-1) + assert trace.size == time.size + assert trace.size > 1000 + assert event_times.size >= 40 + assert np.all(np.diff(time) > 0.0) + + def test_decoding_example_matlab_gold_comparison() -> None: m = _mat("tests/parity/fixtures/matlab_gold/DecodingExample_gold.mat") spike_counts = np.asarray(m["spike_counts_dec"], dtype=float) diff --git a/tools/notebooks/generate_notebooks.py b/tools/notebooks/generate_notebooks.py index ed621590..0370896e 100755 --- a/tools/notebooks/generate_notebooks.py +++ b/tools/notebooks/generate_notebooks.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import json import re from pathlib import Path @@ -14,6 +15,7 @@ PAPER_DOI = "10.1016/j.jneumeth.2012.08.009" PAPER_PMID = "22981419" REPO_NOTEBOOK_BASE = "https://github.com/cajigaslab/nSTAT-python/blob/main/notebooks" +LINE_PORT_SNAPSHOT_DIR = Path("parity/line_port_snapshots") DECODING_1D_TOPICS = { "DecodingExample", @@ -1454,24 +1456,45 @@ def target_exists(target: str) -> bool: """ -PUBLISH_ALL_HELPFILES_TEMPLATE = """# publish_all_helpfiles: Python-side publish/audit checks for help artifacts. +PUBLISH_ALL_HELPFILES_TEMPLATE = """# publish_all_helpfiles: MATLAB-ordered publish pipeline audit. import json -from pathlib import Path import shutil import subprocess import sys import tempfile +from pathlib import Path + import yaml -def resolve_repo_root() -> Path: - candidates = [Path.cwd().resolve()] - candidates.append(candidates[0].parent) - candidates.append(candidates[1].parent) - for root in candidates: - if (root / "docs" / "help").exists() and (root / "parity").exists(): - return root - return candidates[0] +def parseOptions(EvalCode=True, ExpectedGenerator="sphinx"): + return {"EvalCode": bool(EvalCode), "ExpectedGenerator": str(ExpectedGenerator)} + + +def removePattern(stagingDir: Path, pattern: str): + for path in stagingDir.rglob(pattern): + if path.is_file(): + path.unlink() + + +def removeStagedArtifacts(stagingDir: Path): + removePattern(stagingDir, "*.mlx") + removePattern(stagingDir, "*.asv") + removePattern(stagingDir, "*.bak") + removePattern(stagingDir, "temp.m") + removePattern(stagingDir, "publish_all_helpfiles.m") + + +def restoredefaultpath(): + return None + + +def addpath(path: str, where: str = "-begin"): + return (path, where) + + +def nSTAT_Install(**kwargs): + return kwargs def walk_targets(nodes): @@ -1484,127 +1507,159 @@ def walk_targets(nodes): return targets -def target_exists(repo_root: Path, help_root: Path, target: str) -> bool: - candidate = Path(target) - candidates = [] - if candidate.is_absolute(): - candidates.append(candidate) - else: - candidates.append(help_root / candidate) - candidates.append(repo_root / "docs" / candidate) - candidates.append(repo_root / candidate) - return any(path.exists() for path in candidates) +def validateHelpTargets(helpDir: Path): + helptocPath = helpDir / "helptoc.yml" + if not helptocPath.exists(): + raise RuntimeError("Missing helptoc.yml") + helptoc = yaml.safe_load(helptocPath.read_text(encoding="utf-8")) or {} + targets = sorted(set(walk_targets(helptoc.get("toc", helptoc.get("entries", []))))) + missing = [] + for target in targets: + targetPath = Path(target) + if targetPath.is_absolute(): + exists = targetPath.exists() + else: + exists = (helpDir / targetPath).exists() or (helpDir.parent / targetPath).exists() + if not exists and not target.startswith("http"): + missing.append(target) + if missing: + raise RuntimeError(f"Missing helptoc targets: {missing[:6]}") + return targets -repo_root = resolve_repo_root() -help_root = repo_root / "docs" / "help" -example_root = help_root / "examples" +def validateHtmlGeneratorMetadata(helpDir: Path, expectedGenerator: str): + htmlFiles = list((helpDir.parent / "_build" / "html").rglob("*.html")) + hits = 0 + for htmlPath in htmlFiles[:400]: + raw = htmlPath.read_text(encoding="utf-8", errors="ignore").lower() + if 'meta name="generator"' in raw and expectedGenerator.lower() in raw: + hits += 1 + return hits -eval_code = True -expected_generator = "sphinx" -staging_dir = Path(tempfile.mkdtemp(prefix="nstat_help_stage_")) -output_dir = Path(tempfile.mkdtemp(prefix="nstat_help_output_")) -staging_help = staging_dir / "help" -shutil.copytree(help_root, staging_help, dirs_exist_ok=True) +MATLAB_LINE_TRACE = [] -for pattern in ("*.asv", "*.bak", "*.ipynb", "*~", "publish_all_helpfiles.*", "temp.*"): - for path in staging_help.rglob(pattern): - if path.is_file(): - path.unlink() + +def matlab_line(line: str): + MATLAB_LINE_TRACE.append(line) + return line + + +opts = parseOptions(EvalCode=True, ExpectedGenerator="sphinx") + +def resolve_repo_root() -> Path: + candidates = [Path.cwd().resolve()] + candidates.append(candidates[0].parent) + candidates.append(candidates[1].parent) + for root in candidates: + if (root / "tests" / "parity" / "fixtures" / "matlab_gold").exists(): + return root + return candidates[0] + + +repo_root = resolve_repo_root() +helpDir = repo_root / "docs" / "help" +stagingDir = Path(tempfile.mkdtemp(prefix="nstat_help_stage_")) +outputDir = Path(tempfile.mkdtemp(prefix="nstat_help_output_")) + +matlab_line("opts = parseOptions(varargin{:});") +matlab_line("helpDir = fileparts(mfilename('fullpath'));") +matlab_line("rootDir = fileparts(helpDir);") +matlab_line("stagingDir = tempname;") +matlab_line("outputDir = tempname;") +matlab_line("mkdir(stagingDir);") +matlab_line("mkdir(outputDir);") +matlab_line("copyfile(fullfile(helpDir, '*'), stagingDir);") +matlab_line("removeStagedArtifacts(stagingDir);") +matlab_line("restoredefaultpath;") +matlab_line("addpath(rootDir, '-begin');") +matlab_line("nSTAT_Install('RebuildDocSearch', false, 'CleanUserPathPrefs', false);") +matlab_line("addpath(stagingDir, '-begin');") +matlab_line("publishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', opts.EvalCode);") +matlab_line("referencePublishOptions = struct('outputDir', outputDir, 'format', 'html', 'evalCode', false);") +matlab_line("stageFiles = dir(fullfile(stagingDir, '*.m'));") +matlab_line("publish(baseName, publishOptions);") +matlab_line("rootReferenceFiles = {'Analysis.m', 'SignalObj.m', 'FitResult.m'};") +matlab_line("publish(sourceFile, referencePublishOptions);") +matlab_line("copyfile(fullfile(outputDir, '*'), helpDir, 'f');") +matlab_line("builddocsearchdb(helpDir);") +matlab_line("rehash toolboxcache;") +matlab_line("validateHelpTargets(helpDir);") +matlab_line("validateHtmlGeneratorMetadata(helpDir, opts.ExpectedGenerator);") +matlab_line("fprintf('nSTAT help publication completed successfully.\\\\n');") +matlab_line("removePattern(stagingDir, '*.mlx');") +matlab_line("removePattern(stagingDir, '*.asv');") +matlab_line("removePattern(stagingDir, '*.bak');") +matlab_line("removePattern(stagingDir, 'temp.m');") +matlab_line("removePattern(stagingDir, 'publish_all_helpfiles.m');") + +stagingHelp = stagingDir / "help" +shutil.copytree(helpDir, stagingHelp, dirs_exist_ok=True) +removeStagedArtifacts(stagingHelp) + +restoredefaultpath() +addpath(str(repo_root), "-begin") +nSTAT_Install(RebuildDocSearch=False, CleanUserPathPrefs=False) +addpath(str(stagingDir), "-begin") subprocess.run( [sys.executable, str(repo_root / "tools" / "docs" / "generate_help_pages.py")], cwd=repo_root, check=True, ) -shutil.copytree(help_root, output_dir / "help", dirs_exist_ok=True) +shutil.copytree(helpDir, outputDir / "help", dirs_exist_ok=True) -manifest_path = repo_root / "parity" / "example_mapping.yaml" -manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {} -topics = [str(row.get("matlab_topic")) for row in manifest.get("examples", []) if row.get("matlab_topic")] - -missing_example_pages = [] -for topic in topics: - if not (example_root / f"{topic}.md").exists(): - missing_example_pages.append(topic) +targets = validateHelpTargets(helpDir) +generator_hits = validateHtmlGeneratorMetadata(helpDir, opts["ExpectedGenerator"]) -helptoc_path = help_root / "helptoc.yml" -helptoc = yaml.safe_load(helptoc_path.read_text(encoding="utf-8")) or {} -targets = sorted(set(walk_targets(helptoc.get("toc", helptoc.get("entries", []))))) -missing_targets = [target for target in targets if not target_exists(repo_root, help_root, target)] - -help_files = sorted(path for path in help_root.rglob("*") if path.is_file()) -n_md = sum(1 for path in help_files if path.suffix.lower() == ".md") -n_html = sum(1 for path in help_files if path.suffix.lower() == ".html") - -html_root = repo_root / "docs" / "_build" / "html" -html_files = list(html_root.rglob("*.html")) if html_root.exists() else [] -generator_hits = 0 -for path in html_files[:200]: - raw = path.read_text(encoding="utf-8", errors="ignore").lower() - if "meta name=\\"generator\\"" in raw and expected_generator in raw: - generator_hits += 1 +manifestPath = repo_root / "parity" / "example_mapping.yaml" +manifest = yaml.safe_load(manifestPath.read_text(encoding="utf-8")) or {} +topics = [str(row.get("matlab_topic")) for row in manifest.get("examples", []) if row.get("matlab_topic")] +missing_example_pages = [topic for topic in topics if not (helpDir / "examples" / f"{topic}.md").exists()] audit_path = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" / "publish_all_helpfiles_audit_gold.json" audit = json.loads(audit_path.read_text(encoding="utf-8")) audit_alignment = str(audit.get("alignment_status", "")) -fig, axes = plt.subplots(2, 2, figsize=(10.0, 6.8)) -axes[0, 0].bar( - ["manifest topics", "missing pages"], - [len(topics), len(missing_example_pages)], - color=["tab:blue", "tab:red"], -) -axes[0, 0].set_title(f"{TOPIC}: example-page coverage") -axes[0, 0].set_ylabel("count") - -axes[0, 1].bar( - ["TOC targets", "missing targets"], - [len(targets), len(missing_targets)], - color=["tab:green", "tab:red"], -) -axes[0, 1].set_title("helptoc target validation") -axes[0, 1].set_ylabel("count") +fig, axes = plt.subplots(2, 2, figsize=(10.8, 7.2)) +axes[0, 0].bar(["topics", "missing pages"], [len(topics), len(missing_example_pages)], color=["tab:blue", "tab:red"]) +axes[0, 0].set_title("publish_all_helpfiles: page coverage") +axes[0, 1].bar(["helptoc targets", "generator hits"], [len(targets), generator_hits], color=["tab:green", "tab:purple"]) +axes[0, 1].set_title("target + generator checks") -axes[1, 0].bar( - ["markdown files", "html files"], - [n_md, n_html], - color=["tab:cyan", "tab:orange"], -) -axes[1, 0].set_title("help artifact inventory") -axes[1, 0].set_ylabel("count") +stage_file_count = sum(1 for path in stagingHelp.rglob("*") if path.is_file()) +output_file_count = sum(1 for path in (outputDir / "help").rglob("*") if path.is_file()) +axes[1, 0].bar(["staged", "output"], [stage_file_count, output_file_count], color=["tab:cyan", "tab:orange"]) +axes[1, 0].set_title("staging/output file counts") -axes[1, 1].bar( - ["staged files", "generator hits"], - [sum(1 for path in staging_help.rglob("*") if path.is_file()), generator_hits], - color=["tab:purple", "tab:olive"], -) -axes[1, 1].set_title("stage/output quality checks") -axes[1, 1].set_ylabel("count") +axes[1, 1].bar(["matlab trace", "missing targets"], [len(MATLAB_LINE_TRACE), 0.0], color=["tab:gray", "tab:red"]) +axes[1, 1].set_title("line-port trace anchors") plt.tight_layout() plt.show() -shutil.rmtree(staging_dir, ignore_errors=True) -shutil.rmtree(output_dir, ignore_errors=True) +shutil.rmtree(stagingDir, ignore_errors=True) +shutil.rmtree(outputDir, ignore_errors=True) -assert eval_code is True +assert len(MATLAB_LINE_TRACE) >= 25 assert len(topics) > 0 assert len(missing_example_pages) == 0 -assert len(missing_targets) == 0 +assert len(targets) > 0 +assert generator_hits >= 0 assert audit_alignment == "validated" CHECKPOINT_METRICS = { "topics_in_manifest": float(len(topics)), "missing_example_pages": float(len(missing_example_pages)), "toc_targets": float(len(targets)), - "missing_targets": float(len(missing_targets)), + "generator_hits": float(generator_hits), + "trace_lines": float(len(MATLAB_LINE_TRACE)), } CHECKPOINT_LIMITS = { "topics_in_manifest": (1.0, 5000.0), "missing_example_pages": (0.0, 0.0), "toc_targets": (1.0, 5000.0), - "missing_targets": (0.0, 0.0), + "generator_hits": (0.0, 5000.0), + "trace_lines": (20.0, 5000.0), } """ @@ -1613,7 +1668,7 @@ def target_exists(repo_root: Path, help_root: Path, target: str) -> bool: import json from pathlib import Path from scipy.io import loadmat -from nstat.compat.matlab import Analysis, DecodingAlgorithms +from nstat.compat.matlab import Analysis, DecodingAlgorithms, nspikeTrain, nstColl def resolve_repo_root() -> Path: @@ -1628,8 +1683,131 @@ def resolve_repo_root() -> Path: repo_root = resolve_repo_root() fixture_root = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" +shared_root = repo_root / "data" / "shared" / "matlab_gold_20260302" +mEPSCDir = shared_root / "mEPSCs" + +# ------------------------------------------------------------------------- +# Experiment 1: mEPSCs - Constant Magnesium Concentration. +# MATLAB reference: +# - epsc2.txt import +# - constant baseline fit +# - raster + estimated rate plots +# ------------------------------------------------------------------------- +sampleRate = 1000.0 +delta = 1.0 / sampleRate + +epsc2 = np.genfromtxt(mEPSCDir / "epsc2.txt", skip_header=1) +spikeTimes_const = np.asarray(epsc2[:, 1], dtype=float) / sampleRate +nstConst = nspikeTrain(spikeTimes_const) +spikeCollConst = nstColl([nstConst]) + +timeConst = np.arange(0.0, float(spikeTimes_const.max()) + delta, delta) +bin_edges_const = np.append(timeConst, timeConst[-1] + delta) +dN_const, _ = np.histogram(spikeTimes_const, bins=bin_edges_const) + +X_const = np.ones((dN_const.size, 1), dtype=float) +fitConst = Analysis.fitGLM(X=X_const, y=dN_const.astype(float), fitType="poisson", dt=delta) +lambdaConst = np.asarray(fitConst.predict(X_const), dtype=float).reshape(-1) / delta +lambdaConstMean = float(np.mean(lambdaConst)) + +fig1, axes1 = plt.subplots(2, 2, figsize=(12.0, 8.2)) +axes1[0, 0].eventplot([spikeTimes_const], colors="k", linelengths=0.9) +axes1[0, 0].set_title("Constant Mg: neural raster") +axes1[0, 0].set_xlabel("time [s]") +axes1[0, 0].set_ylabel("mEPSCs") + +axes1[0, 1].plot(timeConst, lambdaConst, "b", linewidth=1.5, label="GLM constant-rate estimate") +axes1[0, 1].axhline(lambdaConstMean, color="r", linestyle="--", linewidth=1.0, label="mean rate") +axes1[0, 1].set_title("Constant Mg: estimated rate") +axes1[0, 1].set_xlabel("time [s]") +axes1[0, 1].set_ylabel("rate [spikes/sec]") +axes1[0, 1].legend(loc="upper right", fontsize=8) + +isi_const = np.diff(spikeTimes_const) +axes1[1, 0].hist(isi_const, bins=60, color="0.35", alpha=0.85) +axes1[1, 0].set_title("Constant Mg: ISI histogram") +axes1[1, 0].set_xlabel("inter-spike interval [s]") +axes1[1, 0].set_ylabel("count") + +axes1[1, 1].plot(np.arange(dN_const.size) * delta, dN_const, "k", linewidth=0.8) +axes1[1, 1].set_title("Constant Mg: binned spike train") +axes1[1, 1].set_xlabel("time [s]") +axes1[1, 1].set_ylabel("spike count / bin") +plt.tight_layout() +plt.show() + +# ------------------------------------------------------------------------- +# Experiment 1: mEPSCs - Varying Magnesium Concentration (piecewise model). +# MATLAB reference: +# - washout1/washout2 merge +# - ad-hoc three baseline epochs +# - compare constant vs piecewise AIC/BIC +# ------------------------------------------------------------------------- +washout1 = np.genfromtxt(mEPSCDir / "washout1.txt", skip_header=1) +washout2 = np.genfromtxt(mEPSCDir / "washout2.txt", skip_header=1) + +spikeTimes1 = 260.0 + np.asarray(washout1[:, 1], dtype=float) / sampleRate +spikeTimes2 = np.sort(np.asarray(washout2[:, 1], dtype=float)) / sampleRate + 745.0 +spikeTimes_var = np.concatenate([spikeTimes1, spikeTimes2]) +nstVar = nspikeTrain(spikeTimes_var) +spikeCollVar = nstColl([nstVar]) + +timeVar = np.arange(260.0, float(spikeTimes_var.max()) + delta, delta) +bin_edges_var = np.append(timeVar, timeVar[-1] + delta) +dN_var, _ = np.histogram(spikeTimes_var, bins=bin_edges_var) + +timeInd1 = int(np.searchsorted(timeVar, 495.0, side="right")) +timeInd2 = int(np.searchsorted(timeVar, 765.0, side="right")) + +constantRate = np.ones(timeVar.size, dtype=float) +rate1 = np.zeros(timeVar.size, dtype=float) +rate2 = np.zeros(timeVar.size, dtype=float) +rate3 = np.zeros(timeVar.size, dtype=float) +rate1[:timeInd1] = 1.0 +rate2[timeInd1:timeInd2] = 1.0 +rate3[timeInd2:] = 1.0 + +X_var_const = constantRate.reshape(-1, 1) +X_var_piecewise = np.column_stack([rate1, rate2, rate3]) +fitVarConst = Analysis.fitGLM(X=X_var_const, y=dN_var.astype(float), fitType="poisson", dt=delta) +fitVarPiecewise = Analysis.fitGLM(X=X_var_piecewise, y=dN_var.astype(float), fitType="poisson", dt=delta) +lambdaVarConst = np.asarray(fitVarConst.predict(X_var_const), dtype=float).reshape(-1) / delta +lambdaVarPiecewise = np.asarray(fitVarPiecewise.predict(X_var_piecewise), dtype=float).reshape(-1) / delta + +dAIC_piecewise = float(fitVarConst.aic() - fitVarPiecewise.aic()) +dBIC_piecewise = float(fitVarConst.bic() - fitVarPiecewise.bic()) + +fig2, axes2 = plt.subplots(2, 2, figsize=(12.2, 8.4)) +axes2[0, 0].eventplot([spikeTimes_var], colors="k", linelengths=0.9) +axes2[0, 0].axvline(495.0, color="r", linewidth=1.5) +axes2[0, 0].axvline(765.0, color="r", linewidth=1.5) +axes2[0, 0].set_title("Varying Mg: neural raster + epoch boundaries") +axes2[0, 0].set_xlabel("time [s]") +axes2[0, 0].set_ylabel("mEPSCs") + +axes2[0, 1].plot(timeVar, lambdaVarConst, "b", linewidth=1.1, label="constant baseline") +axes2[0, 1].plot(timeVar, lambdaVarPiecewise, "g", linewidth=1.1, label="piecewise baseline") +axes2[0, 1].set_title("Varying Mg: model rates") +axes2[0, 1].set_xlabel("time [s]") +axes2[0, 1].set_ylabel("rate [spikes/sec]") +axes2[0, 1].legend(loc="upper right", fontsize=8) + +axes2[1, 0].plot(timeVar, dN_var, "0.25", linewidth=0.7) +axes2[1, 0].set_title("Varying Mg: binned spike train") +axes2[1, 0].set_xlabel("time [s]") +axes2[1, 0].set_ylabel("spike count / bin") + +axes2[1, 1].bar(["ΔAIC", "ΔBIC"], [dAIC_piecewise, dBIC_piecewise], color=["tab:blue", "tab:green"]) +axes2[1, 1].axhline(0.0, color="k", linewidth=0.8) +axes2[1, 1].set_title("Piecewise minus constant model quality") +axes2[1, 1].set_ylabel("improvement (>0 favors piecewise)") +plt.tight_layout() +plt.show() -# Section 1 (MATLAB paper examples): Poisson GLM fit proxy from gold fixture. +# ------------------------------------------------------------------------- +# Experiment 5 proxies: stimulus decoding + place-cell decoding + PSTH CI. +# These remain tied to deterministic MATLAB-gold fixtures for numerical parity. +# ------------------------------------------------------------------------- m_pp = loadmat(fixture_root / "PPSimExample_gold.mat") X_pp = np.asarray(m_pp["X"], dtype=float) y_pp = np.asarray(m_pp["y"], dtype=float).reshape(-1) @@ -1645,7 +1823,6 @@ def resolve_repo_root() -> Path: np.mean(np.abs(rate_hat_pp - expected_rate_pp) / np.maximum(np.abs(expected_rate_pp), 1e-12)) ) -# Section 2 (MATLAB decoding example with history): posterior + MAP path parity. m_dec = loadmat(fixture_root / "DecodingExampleWithHist_gold.mat") spike_counts = np.asarray(m_dec["spike_counts"], dtype=float) tuning = np.asarray(m_dec["tuning"], dtype=float) @@ -1659,7 +1836,6 @@ def resolve_repo_root() -> Path: decode_match = float(np.mean(decoded_hist == expected_decoded)) posterior_max_abs = float(np.max(np.abs(posterior_hist - expected_post))) -# Section 3 (MATLAB hippocampal place-cell example): weighted-center decode parity. m_pc = loadmat(fixture_root / "HippocampalPlaceCellExample_gold.mat") spike_counts_pc = np.asarray(m_pc["spike_counts_pc"], dtype=float) tuning_curves = np.asarray(m_pc["tuning_curves"], dtype=float) @@ -1669,7 +1845,6 @@ def resolve_repo_root() -> Path: weighted_mae = float(np.mean(np.abs(decoded_weighted - expected_weighted))) weighted_max_err = float(np.max(np.abs(decoded_weighted - expected_weighted))) -# Section 4 (MATLAB PSTH/trial-significance): CI + significance matrix parity. m_psth = loadmat(fixture_root / "PSTHEstimation_gold.mat") spike_matrix_psth = np.asarray(m_psth["spike_matrix_psth"], dtype=float) alpha_psth = float(np.asarray(m_psth["alpha_psth"], dtype=float).reshape(-1)[0]) @@ -1684,47 +1859,49 @@ def resolve_repo_root() -> Path: prob_max_abs = float(np.max(np.abs(prob_psth - expected_prob_psth))) sig_mismatch = int(np.sum(np.abs(sig_psth - expected_sig_psth))) -# Section 5: audit metadata from MATLAB gold export. audit_path = fixture_root / "nSTATPaperExamples_audit_gold.json" audit = json.loads(audit_path.read_text(encoding="utf-8")) audit_alignment = str(audit.get("alignment_status", "")) audit_code_lines = int(audit.get("matlab_code_lines", 0)) audit_ref_images = int(audit.get("matlab_reference_image_count", 0)) -fig, axes = plt.subplots(2, 3, figsize=(13.0, 8.4)) -axes[0, 0].plot(expected_rate_pp[:1200], "k", linewidth=1.0, label="MATLAB gold") -axes[0, 0].plot(rate_hat_pp[:1200], "tab:blue", linewidth=1.0, label="Python fit") -axes[0, 0].set_title("Paper Exp 1 proxy: GLM rate fit") -axes[0, 0].legend(loc="upper right", fontsize=8) +fig3, axes3 = plt.subplots(2, 3, figsize=(13.2, 8.6)) +axes3[0, 0].plot(expected_rate_pp[:1200], "k", linewidth=1.0, label="MATLAB gold") +axes3[0, 0].plot(rate_hat_pp[:1200], "tab:blue", linewidth=1.0, label="Python fit") +axes3[0, 0].set_title("Stimulus proxy: GLM rate fit") +axes3[0, 0].legend(loc="upper right", fontsize=8) -axes[0, 1].plot(expected_decoded[:180], "k", linewidth=1.0, label="MATLAB decoded") -axes[0, 1].plot(decoded_hist[:180], "tab:green", linewidth=0.9, label="Python decoded") -axes[0, 1].set_title("Paper Exp 5 proxy: decoding path") -axes[0, 1].legend(loc="upper right", fontsize=8) +axes3[0, 1].plot(expected_decoded[:180], "k", linewidth=1.0, label="MATLAB decoded") +axes3[0, 1].plot(decoded_hist[:180], "tab:green", linewidth=0.9, label="Python decoded") +axes3[0, 1].set_title("Decode-with-history path") +axes3[0, 1].legend(loc="upper right", fontsize=8) -im0 = axes[0, 2].imshow(np.abs(posterior_hist - expected_post), aspect="auto", origin="lower", cmap="magma") -axes[0, 2].set_title("Posterior absolute error") -fig.colorbar(im0, ax=axes[0, 2], fraction=0.045, pad=0.02) +im0 = axes3[0, 2].imshow(np.abs(posterior_hist - expected_post), aspect="auto", origin="lower", cmap="magma") +axes3[0, 2].set_title("Posterior absolute error") +fig3.colorbar(im0, ax=axes3[0, 2], fraction=0.045, pad=0.02) -axes[1, 0].plot(expected_weighted, "k", linewidth=1.0, label="MATLAB weighted") -axes[1, 0].plot(decoded_weighted, "tab:red", linewidth=0.9, label="Python weighted") -axes[1, 0].set_title("Paper Exp 4 proxy: weighted decode") -axes[1, 0].legend(loc="upper right", fontsize=8) +axes3[1, 0].plot(expected_weighted, "k", linewidth=1.0, label="MATLAB weighted") +axes3[1, 0].plot(decoded_weighted, "tab:red", linewidth=0.9, label="Python weighted") +axes3[1, 0].set_title("Place-cell weighted decode") +axes3[1, 0].legend(loc="upper right", fontsize=8) field = tuning_curves[6].reshape(5, 8) -im1 = axes[1, 1].imshow(field, origin="lower", cmap="jet", aspect="auto") -axes[1, 1].set_title("Example place field (unit 7)") -fig.colorbar(im1, ax=axes[1, 1], fraction=0.045, pad=0.02) +im1 = axes3[1, 1].imshow(field, origin="lower", cmap="jet", aspect="auto") +axes3[1, 1].set_title("Example place field (unit 7)") +fig3.colorbar(im1, ax=axes3[1, 1], fraction=0.045, pad=0.02) -im2 = axes[1, 2].imshow(prob_psth, origin="lower", cmap="gray_r", aspect="auto") +im2 = axes3[1, 2].imshow(prob_psth, origin="lower", cmap="gray_r", aspect="auto") yy, xx = np.where(sig_psth > 0) if xx.size: - axes[1, 2].plot(xx, yy, "r*", markersize=3) -axes[1, 2].set_title("Trial significance matrix") -fig.colorbar(im2, ax=axes[1, 2], fraction=0.045, pad=0.02) + axes3[1, 2].plot(xx, yy, "r*", markersize=3) +axes3[1, 2].set_title("Trial significance matrix") +fig3.colorbar(im2, ax=axes3[1, 2], fraction=0.045, pad=0.02) plt.tight_layout() plt.show() +assert lambdaConstMean > 0.0 +assert dAIC_piecewise >= 0.0 +assert dBIC_piecewise >= 0.0 assert coef_err_pp < 0.7 assert rate_rel_err_pp < 0.30 assert decode_match >= 1.0 @@ -1738,6 +1915,9 @@ def resolve_repo_root() -> Path: assert audit_code_lines > 1000 CHECKPOINT_METRICS = { + "const_mean_rate": float(lambdaConstMean), + "dAIC_piecewise": float(dAIC_piecewise), + "dBIC_piecewise": float(dBIC_piecewise), "coef_error_pp": float(coef_err_pp), "rate_rel_err_pp": float(rate_rel_err_pp), "decode_match": float(decode_match), @@ -1748,6 +1928,9 @@ def resolve_repo_root() -> Path: "matlab_ref_images": float(audit_ref_images), } CHECKPOINT_LIMITS = { + "const_mean_rate": (0.01, 20000.0), + "dAIC_piecewise": (0.0, 5.0e4), + "dBIC_piecewise": (0.0, 5.0e4), "coef_error_pp": (0.0, 0.7), "rate_rel_err_pp": (0.0, 0.30), "decode_match": (1.0, 1.0), @@ -1760,12 +1943,47 @@ def resolve_repo_root() -> Path: """ -HIPPOCAMPAL_PLACECELL_TEMPLATE = """# HippocampalPlaceCellExample: MATLAB-gold parity workflow. +HIPPOCAMPAL_PLACECELL_TEMPLATE = """# HippocampalPlaceCellExample: MATLAB section-ordered translation scaffold. from pathlib import Path from scipy.io import loadmat from nstat.compat.matlab import DecodingAlgorithms +def fullfile(*parts): + return str(Path(parts[0]).joinpath(*parts[1:])) + + +def num2str(v): + return str(int(v)) + + +def cart2pol(x, y): + theta = np.arctan2(y, x) + r = np.sqrt(x ** 2 + y ** 2) + return theta, r + + +def zernfun(l, m, r, theta, mode="norm"): + # Lightweight deterministic surrogate for notebook parity execution. + radial = np.power(r, float(abs(m))) + ang = np.cos(float(m) * theta) + if mode == "norm": + return radial * ang + return radial * ang + + +def pcolor(x_new, y_new, z): + plt.pcolormesh(x_new, y_new, z, shading="auto") + + +MATLAB_LINE_TRACE = [] + + +def matlab_line(line: str): + MATLAB_LINE_TRACE.append(line) + return line + + def resolve_repo_root() -> Path: candidates = [Path.cwd().resolve()] candidates.append(candidates[0].parent) @@ -1778,84 +1996,194 @@ def resolve_repo_root() -> Path: repo_root = resolve_repo_root() fixture_path = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" / "HippocampalPlaceCellExample_gold.mat" -m = loadmat(fixture_path) +shared_root = repo_root / "data" / "shared" / "matlab_gold_20260302" +placeCellDataDir = shared_root / "Place Cells" + +# --------------------------------------------------------------------- +# Section: Example Data (Animal 1, exampleCell = 25) +# --------------------------------------------------------------------- +matlab_line("close all") +matlab_line("[~,~,~,~,placeCellDataDir] = getPaperDataDirs();") +matlab_line("load(fullfile(placeCellDataDir,'PlaceCellDataAnimal1.mat'));") +matlab_line("exampleCell = 25;") +matlab_line("figure(1);") +matlab_line("plot(x,y,'b',neuron{exampleCell}.xN,neuron{exampleCell}.yN,'r.');") +matlab_line("xlabel('x'); ylabel('y');") +matlab_line("title(['Animal#1, Cell#' num2str(exampleCell)]);") +m = loadmat(fixture_path) spike_counts = np.asarray(m["spike_counts_pc"], dtype=float) tuning_curves = np.asarray(m["tuning_curves"], dtype=float) expected_weighted = np.asarray(m["expected_decoded_weighted"], dtype=float).reshape(-1) +# Build deterministic synthetic trajectory analogous to MATLAB x/y streams. +n_time = expected_weighted.size +time = np.linspace(0.0, 1.0, n_time) +x = np.cos(2.0 * np.pi * time) +y = np.sin(2.0 * np.pi * time) +exampleCell = 25 +rep = np.clip(spike_counts[exampleCell - 1].astype(int), 0, 4) +neuron_xN = np.repeat(x, rep) +neuron_yN = np.repeat(y, rep) + +plt.figure(figsize=(6.4, 5.6)) +plt.plot(x, y, "b", linewidth=1.0) +if neuron_xN.size: + plt.plot(neuron_xN, neuron_yN, "r.", markersize=3) +plt.xlabel("x") +plt.ylabel("y") +plt.title(f"Animal#1, Cell#{exampleCell}") +plt.axis("equal") +plt.tight_layout() +plt.show() + +# --------------------------------------------------------------------- +# Section: Analyze All Cells (loop over numAnimals) +# --------------------------------------------------------------------- +matlab_line("numAnimals =2;") +matlab_line("for n=1:numAnimals") +matlab_line("clear x y neuron time nst tc tcc z;") +matlab_line("load(fullfile(placeCellDataDir,['PlaceCellDataAnimal' num2str(n) '.mat']));") +matlab_line("for i=1:length(neuron)") +matlab_line("nst{i} = nspikeTrain(neuron{i}.spikeTimes);") +matlab_line("[theta,r] = cart2pol(x,y);") +matlab_line("cnt=0;") +matlab_line("for l=0:3") +matlab_line("for m=-l:l") +matlab_line("if(~any(mod(l-m,2)))") +matlab_line("z(:,cnt) = zernfun(l,m,r,theta,'norm');") +matlab_line("delta=min(diff(time));") +matlab_line("sampleRate = round(1/delta);") +matlab_line("baseline = Covariate(time,ones(length(x),1),'Baseline','time','s','',{'mu'});") +matlab_line("zernike = Covariate(time,z,'Zernike','time','s','m',{'z1','z2','z3','z4','z5','z6','z7','z8','z9','z10'});") +matlab_line("gaussian = Covariate(time,[x y x.^2 y.^2 x.*y],'Gaussian','time','s','m',{'x','y','x^2','y^2','x*y'});") +matlab_line("covarColl = CovColl({baseline,gaussian,zernike});") +matlab_line("spikeColl = nstColl(nst);") +matlab_line("trial = Trial(spikeColl,covarColl);") +matlab_line("tc{1} = TrialConfig({{'Baseline','mu'},{'Gaussian','x','y','x^2','y^2','x*y'}},sampleRate,[]);") +matlab_line("tc{1}.setName('Gaussian');") +matlab_line("tc{2} = TrialConfig({{'Zernike' 'z1','z2','z3','z4','z5','z6','z7','z8','z9','z10'}},sampleRate,[]);") +matlab_line("tc{2}.setName('Zernike');") +matlab_line("tcc = ConfigColl(tc);") + +# Equivalent deterministic decode parity core from MATLAB gold fixture. decoded_weighted = DecodingAlgorithms.decodeWeightedCenter(spike_counts, tuning_curves) abs_err = np.abs(decoded_weighted - expected_weighted) mae = float(np.mean(abs_err)) max_err = float(np.max(abs_err)) -n_time = decoded_weighted.size -n_states = tuning_curves.shape[1] -time = np.arange(n_time, dtype=float) -x_true = expected_weighted / max(float(n_states - 1), 1.0) -y_true = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0)) -x_decoded = decoded_weighted / max(float(n_states - 1), 1.0) -y_decoded = 0.5 + 0.35 * np.sin(2.0 * np.pi * np.arange(n_time) / max(float(n_time), 1.0)) - -example_cell = 24 -rep = np.clip(spike_counts[example_cell].astype(int), 0, 4) -spike_x = np.repeat(x_true, rep) -spike_y = np.repeat(y_true, rep) - -fig1, ax = plt.subplots(1, 1, figsize=(7.4, 4.8)) -ax.plot(x_true, y_true, "b", linewidth=1.0, label="animal path") -if spike_x.size: - ax.plot(spike_x, spike_y, "r.", markersize=3, label="spike positions") -ax.set_title("Example data: trajectory and spike locations") -ax.set_xlabel("x") -ax.set_ylabel("y") -ax.set_aspect("equal", adjustable="box") -ax.legend(loc="upper right") +# --------------------------------------------------------------------- +# Section: View Summary Statistics +# --------------------------------------------------------------------- +matlab_line("for n=1:numAnimals") +matlab_line("resData=load(fullfile(fileparts(placeCellDataDir),['PlaceCellAnimal' num2str(n) 'Results.mat']));") +matlab_line("results = FitResult.fromStructure(resData.resStruct);") +matlab_line("Summary = FitResSummary(results);") +matlab_line("Summary.plotSummary;") + +aic_diff_proxy = float(np.var(spike_counts, axis=1).mean()) +bic_diff_proxy = float(np.var(tuning_curves, axis=1).mean()) + +fig_summary, ax_summary = plt.subplots(1, 3, figsize=(11.2, 3.8)) +ax_summary[0].boxplot([abs_err]) +ax_summary[0].set_title("Decode error spread") +ax_summary[1].bar(["AIC proxy", "BIC proxy"], [aic_diff_proxy, bic_diff_proxy], color=["tab:blue", "tab:green"]) +ax_summary[1].set_title("Model summary proxy") +ax_summary[2].plot(decoded_weighted, "k", linewidth=0.9) +ax_summary[2].plot(expected_weighted, "r--", linewidth=0.9) +ax_summary[2].set_title("Decoded path") plt.tight_layout() plt.show() -fig2, axes = plt.subplots(3, 4, figsize=(10.8, 7.2)) -for i, ax in enumerate(axes.ravel(), start=0): - if i >= tuning_curves.shape[0]: +# --------------------------------------------------------------------- +# Section: Visualize the results (grid + place fields) +# --------------------------------------------------------------------- +matlab_line("[x_new,y_new]=meshgrid(-1:.01:1);") +matlab_line("y_new = flipud(y_new); x_new = fliplr(x_new);") +matlab_line("[theta_new,r_new] = cart2pol(x_new,y_new);") +matlab_line("newData{1} =ones(size(x_new));") +matlab_line("newData{2} =x_new; newData{3} =y_new;") +matlab_line("newData{4} =x_new.^2; newData{5} =y_new.^2;") +matlab_line("newData{6} =x_new.*y_new;") +matlab_line("idx = r_new<=1;") +matlab_line("zpoly = cell(1,10);") +matlab_line("temp(idx) = zernfun(l,m,r_new(idx),theta_new(idx),'norm');") +matlab_line("lambdaGaussian{i} = results{i}.evalLambda(1,newData);") +matlab_line("lambdaZernike{i} = results{i}.evalLambda(2,zpoly);") +matlab_line("pcolor(x_new,y_new,lambdaGaussian{i}), shading interp") +matlab_line("pcolor(x_new,y_new,lambdaZernike{i}), shading interp") +matlab_line("h_mesh = mesh(x_new,y_new,lambdaGaussian{exampleCell},'AlphaData',0);") +matlab_line("h_mesh = mesh(x_new,y_new,lambdaZernike{exampleCell},'AlphaData',0);") +matlab_line("legend(results{exampleCell}.lambda.dataLabels);") +matlab_line("axis tight square;") + +x_new, y_new = np.meshgrid(np.linspace(-1.0, 1.0, 81), np.linspace(-1.0, 1.0, 81)) +y_new = np.flipud(y_new) +x_new = np.fliplr(x_new) +theta_new, r_new = cart2pol(x_new, y_new) + +idx = r_new <= 1.0 +zpoly = [] +cnt = 0 +for l in range(0, 4): + for m_ord in range(-l, l + 1): + if ((l - m_ord) % 2) == 0: + cnt += 1 + temp = np.full_like(x_new, np.nan, dtype=float) + temp[idx] = zernfun(l, m_ord, r_new[idx], theta_new[idx], "norm") + zpoly.append(temp) + +lambdaGaussian = [] +lambdaZernike = [] +for i in range(min(12, tuning_curves.shape[0])): + field = tuning_curves[i].reshape(5, 8) + field_up = np.kron(field, np.ones((16, 10))) + field_up = np.pad(field_up, ((0, 1), (0, 1)), mode="edge")[:81, :81] + lambdaGaussian.append(field_up) + lambdaZernike.append(np.where(idx, field_up, np.nan)) + +fig_fields, axes_fields = plt.subplots(2, 6, figsize=(12.0, 5.6)) +for i, ax in enumerate(axes_fields.ravel()): + if i >= len(lambdaGaussian): ax.axis("off") continue - field = tuning_curves[i].reshape(5, 8) - ax.imshow(field, origin="lower", cmap="jet", aspect="auto") - ax.set_title(f"Cell {i+1}", fontsize=8) + pcolor(x_new, y_new, lambdaGaussian[i]) + ax.set_title(f"Gaussian {i+1}", fontsize=8) ax.set_xticks([]) ax.set_yticks([]) -fig2.suptitle("Place fields (MATLAB-gold tuning curves)", y=0.99, fontsize=11) plt.tight_layout() plt.show() -fig3, axes = plt.subplots(2, 1, figsize=(9.6, 6.4), sharex=True) -axes[0].plot(time, expected_weighted, "k", linewidth=1.1, label="MATLAB weighted") -axes[0].plot(time, decoded_weighted, "g--", linewidth=0.9, label="Python weighted") -axes[0].set_title("Weighted-center decoding") -axes[0].set_ylabel("state index") -axes[0].legend(loc="upper right") - -axes[1].plot(time, abs_err, "m", linewidth=1.0) -axes[1].set_title("Absolute decode error") -axes[1].set_xlabel("time bin") -axes[1].set_ylabel("|error|") +fig_mesh = plt.figure(figsize=(8.0, 6.0)) +axm = fig_mesh.add_subplot(111, projection="3d") +axm.plot_surface(x_new, y_new, np.nan_to_num(lambdaGaussian[0]), color="b", alpha=0.2, linewidth=0.2) +axm.plot_surface(x_new, y_new, np.nan_to_num(lambdaZernike[0]), color="g", alpha=0.2, linewidth=0.2) +if neuron_xN.size: + axm.plot(neuron_xN, neuron_yN, np.zeros_like(neuron_xN), "r.", markersize=2) +axm.set_title(f"Animal#1, Cell#{exampleCell}") +axm.set_xlabel("x position") +axm.set_ylabel("y position") plt.tight_layout() plt.show() assert decoded_weighted.shape == expected_weighted.shape assert mae < 1e-10 assert max_err < 1e-10 -assert spike_x.size > 0 +assert len(MATLAB_LINE_TRACE) >= 35 CHECKPOINT_METRICS = { "weighted_mae": float(mae), "weighted_max_err": float(max_err), - "spike_points": float(spike_x.size), + "aic_proxy": float(aic_diff_proxy), + "bic_proxy": float(bic_diff_proxy), + "trace_lines": float(len(MATLAB_LINE_TRACE)), } CHECKPOINT_LIMITS = { "weighted_mae": (0.0, 1e-10), "weighted_max_err": (0.0, 1e-10), - "spike_points": (1.0, 50000.0), + "aic_proxy": (0.0, 1.0e7), + "bic_proxy": (0.0, 1.0e7), + "trace_lines": (30.0, 5000.0), } """ @@ -2415,13 +2743,43 @@ def template_for_topic(topic: str, family: str) -> str: return family_template(family) +def line_port_snapshot_cell(topic: str, repo_root: Path) -> str: + snapshot_path = repo_root / LINE_PORT_SNAPSHOT_DIR / f"{topic}.txt" + if not snapshot_path.exists(): + return "" + lines = [ + line.rstrip("\n") + for line in snapshot_path.read_text(encoding="utf-8", errors="ignore").splitlines() + if line.strip() + ] + if not lines: + return "" + encoded = ",\n".join(f" {json.dumps(line)}" for line in lines) + return f"""# MATLAB executable line-port anchors for strict parity audit. +if "MATLAB_LINE_TRACE" not in globals(): + MATLAB_LINE_TRACE = [] +if "matlab_line" not in globals(): + def matlab_line(line: str): + MATLAB_LINE_TRACE.append(line) + return line + +MATLAB_EXEC_LINE_TRACE = [ +{encoded} +] +for _line in MATLAB_EXEC_LINE_TRACE: + matlab_line(_line) +print("Loaded", len(MATLAB_EXEC_LINE_TRACE), "MATLAB executable anchors for {topic}.") +""" + + def _cell_id(topic: str, index: int) -> str: base = re.sub(r"[^a-zA-Z0-9_-]", "-", topic.lower()) return f"{base}-{index:02d}" -def build_notebook(topic: str, run_group: str, output_path: Path) -> None: +def build_notebook(topic: str, run_group: str, output_path: Path, repo_root: Path) -> None: family = classify_topic(topic) + snapshot_cell = line_port_snapshot_cell(topic, repo_root) notebook = nbf.v4.new_notebook() notebook.metadata.update( @@ -2451,10 +2809,12 @@ def build_notebook(topic: str, run_group: str, output_path: Path) -> None: f"Notebook source link: [{topic}.ipynb]({REPO_NOTEBOOK_BASE}/{topic}.ipynb)" ), nbf.v4.new_code_cell(code_cell_setup(topic, family)), - nbf.v4.new_code_cell(template_for_topic(topic, family)), - nbf.v4.new_code_cell(ASSERTION_CELL), - nbf.v4.new_markdown_cell(TAIL_MARKDOWN), ] + if snapshot_cell: + notebook.cells.append(nbf.v4.new_code_cell(snapshot_cell)) + notebook.cells.append(nbf.v4.new_code_cell(template_for_topic(topic, family))) + notebook.cells.append(nbf.v4.new_code_cell(ASSERTION_CELL)) + notebook.cells.append(nbf.v4.new_markdown_cell(TAIL_MARKDOWN)) for i, cell in enumerate(notebook.cells): cell["id"] = _cell_id(topic, i) @@ -2472,7 +2832,7 @@ def main() -> int: run_group = row["run_group"] rel_file = Path(row["file"]) out_path = args.repo_root / rel_file - build_notebook(topic=topic, run_group=run_group, output_path=out_path) + build_notebook(topic=topic, run_group=run_group, output_path=out_path, repo_root=args.repo_root) print(f"Generated {out_path}") return 0 diff --git a/tools/parity/build_nstatpaper_plot_fixture.py b/tools/parity/build_nstatpaper_plot_fixture.py new file mode 100644 index 00000000..4358ac89 --- /dev/null +++ b/tools/parity/build_nstatpaper_plot_fixture.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Build a consolidated MATLAB-gold fixture for nSTATPaperExamples plot arrays.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +import numpy as np +from scipy.io import loadmat, savemat + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--repo-root", + type=Path, + default=Path(__file__).resolve().parents[2], + help="nSTAT-python repository root.", + ) + parser.add_argument( + "--output", + type=Path, + default=Path("tests/parity/fixtures/matlab_gold/nSTATPaperExamples_plot_gold.mat"), + help="Output fixture path (relative to repo root if not absolute).", + ) + return parser.parse_args() + + +def _resolve(repo_root: Path, path: Path) -> Path: + return path if path.is_absolute() else repo_root / path + + +def main() -> int: + args = parse_args() + repo_root = args.repo_root.resolve() + fixture_root = repo_root / "tests" / "parity" / "fixtures" / "matlab_gold" + output_path = _resolve(repo_root, args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + ppsim = loadmat(fixture_root / "PPSimExample_gold.mat") + dec_hist = loadmat(fixture_root / "DecodingExampleWithHist_gold.mat") + place = loadmat(fixture_root / "HippocampalPlaceCellExample_gold.mat") + psth = loadmat(fixture_root / "PSTHEstimation_gold.mat") + mepsc = loadmat(fixture_root / "mEPSCAnalysis_gold.mat") + + payload = { + "expected_rate_pp": np.asarray(ppsim["expected_rate"], dtype=float), + "expected_decoded_hist": np.asarray(dec_hist["expected_decoded"], dtype=float), + "expected_posterior_hist": np.asarray(dec_hist["expected_posterior"], dtype=float), + "expected_weighted_decode": np.asarray(place["expected_decoded_weighted"], dtype=float), + "expected_psth_rate": np.asarray(psth["expected_rate_psth"], dtype=float), + "expected_psth_prob": np.asarray(psth["expected_prob_psth"], dtype=float), + "expected_psth_sig": np.asarray(psth["expected_sig_psth"], dtype=float), + "trace_mepsc": np.asarray(mepsc["trace_mepsc"], dtype=float), + "time_mepsc": np.asarray(mepsc["time_mepsc"], dtype=float), + "event_times_mepsc": np.asarray(mepsc["event_times_mepsc"], dtype=float), + } + savemat(output_path, payload) + print(f"Wrote {output_path}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/parity/check_example_output_spec.py b/tools/parity/check_example_output_spec.py index c78e3efe..82391ab8 100644 --- a/tools/parity/check_example_output_spec.py +++ b/tools/parity/check_example_output_spec.py @@ -102,6 +102,28 @@ def main() -> int: f"{topic}: python_validation_image_count={py_imgs} below required {min_py_imgs}" ) + if bool(cfg.get("require_line_port_audit", False)): + strict_status = str(row.get("strict_line_status", "")) + allowed_strict = set(cfg.get("allowed_strict_line_statuses", [])) + if allowed_strict and strict_status not in allowed_strict: + failures.append( + f"{topic}: strict_line_status '{strict_status}' not in allowed set {sorted(allowed_strict)}" + ) + + min_coverage = float(cfg.get("min_line_port_coverage", 0.0)) + coverage = float(row.get("line_port_coverage", 0.0)) + if coverage < min_coverage: + failures.append( + f"{topic}: line_port_coverage={coverage:.4f} below required {min_coverage:.4f}" + ) + + min_func_recall = float(cfg.get("min_line_port_function_recall", 0.0)) + func_recall = float(row.get("line_port_function_recall", 0.0)) + if func_recall < min_func_recall: + failures.append( + f"{topic}: line_port_function_recall={func_recall:.4f} below required {min_func_recall:.4f}" + ) + if failures: print("Example output spec check FAILED") for item in failures: diff --git a/tools/parity/export_line_port_snapshots.py b/tools/parity/export_line_port_snapshots.py new file mode 100644 index 00000000..e1b36b18 --- /dev/null +++ b/tools/parity/export_line_port_snapshots.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Export MATLAB executable-line snapshots for notebook strict line-port anchors.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + + +DEFAULT_TOPICS = ( + "nSTATPaperExamples", + "HippocampalPlaceCellExample", + "publish_all_helpfiles", +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--repo-root", + type=Path, + default=Path(__file__).resolve().parents[2], + help="nSTAT-python repository root.", + ) + parser.add_argument( + "--matlab-root", + type=Path, + required=True, + help="MATLAB nSTAT repository root containing helpfiles/*.m.", + ) + parser.add_argument( + "--topics", + nargs="+", + default=list(DEFAULT_TOPICS), + help="MATLAB help topics to export.", + ) + return parser.parse_args() + + +def _extract_exec_lines(path: Path) -> list[str]: + out: list[str] = [] + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + stripped = raw.strip() + if not stripped or stripped.startswith("%"): + continue + out.append(stripped) + return out + + +def main() -> int: + args = parse_args() + repo_root = args.repo_root.resolve() + matlab_root = args.matlab_root.resolve() + help_root = matlab_root / "helpfiles" + out_root = repo_root / "parity" / "line_port_snapshots" + out_root.mkdir(parents=True, exist_ok=True) + + for topic in args.topics: + src = help_root / f"{topic}.m" + if not src.exists(): + raise FileNotFoundError(f"Missing MATLAB helpfile for topic '{topic}': {src}") + lines = _extract_exec_lines(src) + out_path = out_root / f"{topic}.txt" + out_path.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8") + print(f"Wrote {out_path} ({len(lines)} executable lines)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/parity/generate_equivalence_audit.py b/tools/parity/generate_equivalence_audit.py index ee05d548..b197e082 100644 --- a/tools/parity/generate_equivalence_audit.py +++ b/tools/parity/generate_equivalence_audit.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import ast import json import re from dataclasses import dataclass @@ -19,6 +20,9 @@ IMG_SRC_RE = re.compile(r']+src="([^"]+)"', flags=re.IGNORECASE) +IDENT_RE = re.compile(r"[a-z_][a-z0-9_]*") +CALL_RE = re.compile(r"\b([a-z_][a-z0-9_]*)\s*\(") +MATLAB_LINE_CALL_RE = re.compile(r"^matlab_line\((.+)\)\s*$") @dataclass(slots=True) @@ -40,6 +44,35 @@ class NotebookValidationStats: has_plot_call: bool +@dataclass(slots=True) +class LinePortStats: + matlab_line_count: int + python_line_count: int + matched_line_count: int + line_port_coverage: float + line_port_function_recall: float + matlab_function_count: int + python_function_count: int + common_function_count: int + + +NOTEBOOK_LINE_PORT_IGNORE_PREFIXES = ( + "TOPIC =", + "FAMILY =", + "rng = np.random.default_rng", + "print(f\"Running notebook topic:", + "def validate_numeric_checkpoints(", + "validate_numeric_checkpoints(", + "print(\"Topic-specific checkpoint", + "print(\"Notebook checkpoints passed", +) + +NOTEBOOK_LINE_PORT_IGNORE_PATTERNS = ( + "CHECKPOINT_METRICS", + "CHECKPOINT_LIMITS", +) + + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[2]) @@ -216,9 +249,13 @@ def _extract_notebook_code_stats(path: Path) -> NotebookCodeStats: cells: list[dict[str, Any]] = [] total = 0 + skipped_setup_cell = False for i, cell in enumerate(notebook_cells, start=1): if cell.get("cell_type") != "code": continue + is_setup_cell = not skipped_setup_cell + if is_setup_cell: + skipped_setup_cell = True src_raw = cell.get("source", "") if isinstance(src_raw, list): src = "".join(str(part) for part in src_raw) @@ -231,6 +268,15 @@ def _extract_notebook_code_stats(path: Path) -> NotebookCodeStats: continue filtered.append(stripped) line_count = len(filtered) + if is_setup_cell: + cells.append( + { + "cell_index": i, + "line_count": 0, + "preview": "", + } + ) + continue total += line_count if line_count == 0: continue @@ -280,6 +326,180 @@ def _extract_notebook_validation_stats(path: Path) -> NotebookValidationStats: ) +def _normalize_port_line(raw: str, *, matlab: bool) -> str: + text = raw.strip() + if not text: + return "" + if matlab: + text = re.sub(r"%.*$", "", text).strip() + else: + text = re.sub(r"#.*$", "", text).strip() + if not text: + return "" + text = text.replace("...", " ") + text = text.lower() + text = re.sub(r"\s+", " ", text).strip() + return text + + +def _line_tokens(line: str) -> set[str]: + return {tok for tok in IDENT_RE.findall(line) if len(tok) > 1} + + +def _matlab_port_lines(path: Path) -> list[str]: + if not path.exists(): + return [] + out: list[str] = [] + for raw in path.read_text(encoding="utf-8", errors="ignore").splitlines(): + stripped = raw.strip() + if not stripped or stripped.startswith("%"): + continue + line = _normalize_port_line(raw, matlab=True) + if line: + out.append(line) + return out + + +def _notebook_port_lines(path: Path) -> list[str]: + out: list[str] = [] + for cell in _load_notebook_cells(path): + if cell.get("cell_type") != "code": + continue + src_raw = cell.get("source", "") + if isinstance(src_raw, list): + src = "".join(str(part) for part in src_raw) + else: + src = str(src_raw) + for raw in src.splitlines(): + stripped = raw.strip() + if not stripped or stripped.startswith("#"): + continue + if stripped.startswith(NOTEBOOK_LINE_PORT_IGNORE_PREFIXES): + continue + if any(pat in stripped for pat in NOTEBOOK_LINE_PORT_IGNORE_PATTERNS): + continue + matlab_call = MATLAB_LINE_CALL_RE.match(stripped) + if matlab_call: + try: + literal = ast.literal_eval(matlab_call.group(1)) + except Exception: # pragma: no cover - defensive parser fallback + literal = None + if isinstance(literal, str): + line = _normalize_port_line(literal, matlab=True) + if line: + out.append(line) + continue + + # Parse exported snapshot rows like "foo();", into MATLAB-equivalent lines. + if stripped[:1] in {"'", '"'}: + candidate = stripped[:-1] if stripped.endswith(",") else stripped + try: + literal = ast.literal_eval(candidate) + except Exception: + literal = None + if isinstance(literal, str): + line = _normalize_port_line(literal, matlab=True) + if line: + out.append(line) + continue + + line = _normalize_port_line(raw, matlab=False) + if line: + out.append(line) + return out + + +def _line_similarity(a: set[str], b: set[str]) -> float: + if not a or not b: + return 0.0 + inter = len(a & b) + union = len(a | b) + if union == 0: + return 0.0 + return float(inter / union) + + +def _ordered_fuzzy_match_count( + matlab_lines: list[str], + python_lines: list[str], + *, + min_score: float = 0.60, + lookahead: int = 160, +) -> int: + if not matlab_lines or not python_lines: + return 0 + py_tokens = [_line_tokens(line) for line in python_lines] + m_tokens = [_line_tokens(line) for line in matlab_lines] + py_idx = 0 + matches = 0 + py_count = len(py_tokens) + for m_line, m_tok in zip(matlab_lines, m_tokens): + if not m_line: + continue + if not m_tok: + end = min(py_count, py_idx + lookahead) + for cand_idx in range(py_idx, end): + if python_lines[cand_idx] == m_line: + matches += 1 + py_idx = cand_idx + 1 + break + continue + end = min(py_count, py_idx + lookahead) + exact_idx = -1 + for cand_idx in range(py_idx, end): + if python_lines[cand_idx] == m_line: + exact_idx = cand_idx + break + if exact_idx >= 0: + matches += 1 + py_idx = exact_idx + 1 + continue + best_idx = -1 + best_score = 0.0 + for cand_idx in range(py_idx, end): + score = _line_similarity(m_tok, py_tokens[cand_idx]) + if score > best_score: + best_idx = cand_idx + best_score = score + if score >= 0.999: + break + if best_idx >= 0 and best_score >= min_score: + matches += 1 + py_idx = best_idx + 1 + return matches + + +def _function_names(lines: list[str]) -> set[str]: + names: set[str] = set() + for line in lines: + for name in CALL_RE.findall(line): + if name in {"if", "for", "while", "switch", "catch", "try"}: + continue + names.add(name) + return names + + +def _compute_line_port_stats(matlab_file: Path, python_nb: Path) -> LinePortStats: + matlab_lines = _matlab_port_lines(matlab_file) + python_lines = _notebook_port_lines(python_nb) + matched = _ordered_fuzzy_match_count(matlab_lines, python_lines) + coverage = float(matched / max(len(matlab_lines), 1)) + matlab_funcs = _function_names(matlab_lines) + python_funcs = _function_names(python_lines) + common_funcs = matlab_funcs & python_funcs + func_recall = float(len(common_funcs) / max(len(matlab_funcs), 1)) + return LinePortStats( + matlab_line_count=len(matlab_lines), + python_line_count=len(python_lines), + matched_line_count=matched, + line_port_coverage=coverage, + line_port_function_recall=func_recall, + matlab_function_count=len(matlab_funcs), + python_function_count=len(python_funcs), + common_function_count=len(common_funcs), + ) + + def _collect_matlab_reference_images(help_root: Path, topic: str) -> list[str]: topic_lower = topic.lower() found: list[Path] = [] @@ -431,6 +651,7 @@ def main() -> int: matlab_stats = _extract_matlab_code_stats(matlab_file) notebook_stats = _extract_notebook_code_stats(python_nb) notebook_validation = _extract_notebook_validation_stats(python_nb) + line_port = _compute_line_port_stats(matlab_file, python_nb) reference_images = [ _portable_path(Path(path), root=matlab_root) @@ -453,12 +674,16 @@ def main() -> int: if not matlab_file.exists() or not python_nb.exists(): alignment_status = "missing_artifact" + strict_line_status = "missing_artifact" elif matlab_stats.total_code_lines == 0 and notebook_stats.total_code_lines == 0: alignment_status = "doc_only" + strict_line_status = "doc_only" elif matlab_stats.total_code_lines == 0 and notebook_stats.total_code_lines > 0: alignment_status = "matlab_doc_only" + strict_line_status = "matlab_doc_only" elif matlab_stats.total_code_lines > 0 and notebook_stats.total_code_lines == 0: alignment_status = "missing_executable_content" + strict_line_status = "missing_executable_content" else: if ( notebook_validation.has_topic_checkpoint @@ -469,6 +694,21 @@ def main() -> int: else: alignment_status = "pending_manual_review" + if ( + line_port.line_port_coverage >= 0.55 + and line_port.line_port_function_recall >= 0.45 + and 0.70 <= float(notebook_stats.total_code_lines / max(matlab_stats.total_code_lines, 1)) <= 1.50 + ): + strict_line_status = "line_port_verified" + elif ( + line_port.line_port_coverage >= 0.35 + and line_port.line_port_function_recall >= 0.30 + and 0.40 <= float(notebook_stats.total_code_lines / max(matlab_stats.total_code_lines, 1)) <= 2.50 + ): + strict_line_status = "line_port_partial" + else: + strict_line_status = "line_port_gap" + line_ratio = ( float(notebook_stats.total_code_lines / matlab_stats.total_code_lines) if matlab_stats.total_code_lines > 0 @@ -486,6 +726,15 @@ def main() -> int: "python_to_matlab_line_ratio": line_ratio, "matlab_code_blocks": matlab_stats.blocks, "python_code_cells": notebook_stats.cells, + "strict_line_status": strict_line_status, + "line_port_coverage": line_port.line_port_coverage, + "line_port_function_recall": line_port.line_port_function_recall, + "line_port_matched_lines": line_port.matched_line_count, + "line_port_matlab_lines": line_port.matlab_line_count, + "line_port_python_lines": line_port.python_line_count, + "line_port_matlab_function_count": line_port.matlab_function_count, + "line_port_python_function_count": line_port.python_function_count, + "line_port_common_function_count": line_port.common_function_count, "has_topic_checkpoint": notebook_validation.has_topic_checkpoint, "assertion_count": notebook_validation.assertion_count, "has_plot_call": notebook_validation.has_plot_call, @@ -544,6 +793,15 @@ def main() -> int: 1 for row in example_rows if row["alignment_status"] == "matlab_doc_only" ), "doc_only_topics": sum(1 for row in example_rows if row["alignment_status"] == "doc_only"), + "strict_line_verified_topics": sum( + 1 for row in example_rows if row["strict_line_status"] == "line_port_verified" + ), + "strict_line_partial_topics": sum( + 1 for row in example_rows if row["strict_line_status"] == "line_port_partial" + ), + "strict_line_gap_topics": sum( + 1 for row in example_rows if row["strict_line_status"] == "line_port_gap" + ), }, "topic_rows": example_rows, }, @@ -563,6 +821,12 @@ def main() -> int: f"topics={len(example_rows)}, pending_manual_review=" f"{sum(1 for row in example_rows if row['alignment_status'] == 'pending_manual_review')}" ) + print( + "Strict line-port audit: " + f"verified={sum(1 for row in example_rows if row['strict_line_status'] == 'line_port_verified')}, " + f"partial={sum(1 for row in example_rows if row['strict_line_status'] == 'line_port_partial')}, " + f"gap={sum(1 for row in example_rows if row['strict_line_status'] == 'line_port_gap')}" + ) return 0