canns 0.14.1__py3-none-any.whl → 0.14.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,10 @@ class AbstractAnalysisMode(ABC):
29
29
  """Apply neuron/time ranges based on loaded data."""
30
30
  return None
31
31
 
32
+ def apply_language(self, lang: str) -> None:
33
+ """Apply localized tooltips/text."""
34
+ return None
35
+
32
36
 
33
37
  def configure_form_layout(form: QFormLayout) -> None:
34
38
  """Apply consistent spacing/alignment for analysis parameter forms."""
@@ -54,3 +54,14 @@ class CohoMapMode(AbstractAnalysisMode):
54
54
  self.num_circ.setValue(2)
55
55
  elif preset == "hd":
56
56
  self.num_circ.setValue(1)
57
+
58
+ def apply_language(self, lang: str) -> None:
59
+ is_zh = str(lang).lower().startswith("zh")
60
+ if is_zh:
61
+ self.decode_version.setToolTip("解码版本(推荐 v2)。")
62
+ self.num_circ.setToolTip("解码圆数(grid 常用 2,hd 常用 1)。")
63
+ self.subsample.setToolTip("CohoMap 绘制下采样步长。")
64
+ else:
65
+ self.decode_version.setToolTip("Decode version (recommend v2).")
66
+ self.num_circ.setToolTip("Number of circles to decode (grid=2, hd=1).")
67
+ self.subsample.setToolTip("Subsample step for CohoMap plotting.")
@@ -192,3 +192,40 @@ class CohoSpaceMode(AbstractAnalysisMode):
192
192
  val = self.neuron_id.value() + int(delta)
193
193
  val = max(self.neuron_id.minimum(), min(self.neuron_id.maximum(), val))
194
194
  self.neuron_id.setValue(val)
195
+
196
+ def apply_language(self, lang: str) -> None:
197
+ is_zh = str(lang).lower().startswith("zh")
198
+ if is_zh:
199
+ self.dim_mode.setToolTip("解码维度模式(1D/2D)。")
200
+ self.dim.setToolTip("1D 解码维度索引。")
201
+ self.dim1.setToolTip("2D 解码维度 1。")
202
+ self.dim2.setToolTip("2D 解码维度 2。")
203
+ self.mode.setToolTip("spike 或 fr 模式。")
204
+ self.top_percent.setToolTip("活跃点百分比阈值。")
205
+ self.view.setToolTip("显示单神经元或群体。")
206
+ self.subsample.setToolTip("轨迹下采样步长。")
207
+ self.unfold.setToolTip("展开方式(square / skew)。")
208
+ self.skew_show_grid.setToolTip("skew 模式下显示网格。")
209
+ self.skew_tiles.setToolTip("skew 平铺次数。")
210
+ self.enable_score.setToolTip("计算 CohoScore 并选 top-K。")
211
+ self.top_k.setToolTip("Top-K 神经元数量。")
212
+ self.neuron_id.setToolTip("单神经元编号。")
213
+ self.use_best.setToolTip("使用 CohoScore 最小的神经元。")
214
+ self.btn_show.setToolTip("显示当前 neuron 结果。")
215
+ else:
216
+ self.dim_mode.setToolTip("Decode dimension mode (1D/2D).")
217
+ self.dim.setToolTip("1D decoded dimension index.")
218
+ self.dim1.setToolTip("2D decoded dimension 1.")
219
+ self.dim2.setToolTip("2D decoded dimension 2.")
220
+ self.mode.setToolTip("spike or fr mode.")
221
+ self.top_percent.setToolTip("Active percentile threshold.")
222
+ self.view.setToolTip("Show single neuron or population.")
223
+ self.subsample.setToolTip("Trajectory subsample step.")
224
+ self.unfold.setToolTip("Unfold mode (square / skew).")
225
+ self.skew_show_grid.setToolTip("Show grid in skew mode.")
226
+ self.skew_tiles.setToolTip("Skew tiling count.")
227
+ self.enable_score.setToolTip("Compute CohoScore and top-K.")
228
+ self.top_k.setToolTip("Top-K neuron count.")
229
+ self.neuron_id.setToolTip("Single neuron id.")
230
+ self.use_best.setToolTip("Use neuron with lowest CohoScore.")
231
+ self.btn_show.setToolTip("Show current neuron result.")
@@ -50,3 +50,16 @@ class DecodeMode(AbstractAnalysisMode):
50
50
  self.num_circ.setValue(2)
51
51
  elif preset == "hd":
52
52
  self.num_circ.setValue(1)
53
+
54
+ def apply_language(self, lang: str) -> None:
55
+ is_zh = str(lang).lower().startswith("zh")
56
+ if is_zh:
57
+ self.decode_version.setToolTip("解码版本(推荐 v2)。")
58
+ self.num_circ.setToolTip("解码圆数(grid=2,hd=1)。")
59
+ self.real_ground.setToolTip("是否使用 real_ground。")
60
+ self.real_of.setToolTip("是否使用 real_of。")
61
+ else:
62
+ self.decode_version.setToolTip("Decode version (recommend v2).")
63
+ self.num_circ.setToolTip("Number of circles to decode (grid=2, hd=1).")
64
+ self.real_ground.setToolTip("Use real_ground if available.")
65
+ self.real_of.setToolTip("Use real_of if available.")
@@ -79,3 +79,20 @@ class FRMode(AbstractAnalysisMode):
79
79
  self.time_end.setRange(0, total_steps)
80
80
  if self.time_end.value() == 0:
81
81
  self.time_end.setValue(total_steps)
82
+
83
+ def apply_language(self, lang: str) -> None:
84
+ is_zh = str(lang).lower().startswith("zh")
85
+ if is_zh:
86
+ self.normalize.setToolTip("归一化方式;none 表示不归一化。")
87
+ self.mode.setToolTip("fr 需要预处理;spike 直接用事件。")
88
+ self.neuron_start.setToolTip("神经元起始索引。")
89
+ self.neuron_end.setToolTip("神经元结束索引(不包含)。")
90
+ self.time_start.setToolTip("时间起始索引。")
91
+ self.time_end.setToolTip("时间结束索引(不包含)。")
92
+ else:
93
+ self.normalize.setToolTip("Normalization method; none = no normalization.")
94
+ self.mode.setToolTip("fr requires preprocess; spike uses events directly.")
95
+ self.neuron_start.setToolTip("Start neuron index.")
96
+ self.neuron_end.setToolTip("End neuron index (exclusive).")
97
+ self.time_start.setToolTip("Start time index.")
98
+ self.time_end.setToolTip("End time index (exclusive).")
@@ -86,6 +86,23 @@ class FRMMode(AbstractAnalysisMode):
86
86
  return
87
87
  self.neuron_id.setRange(0, max(0, neuron_count - 1))
88
88
 
89
+ def apply_language(self, lang: str) -> None:
90
+ is_zh = str(lang).lower().startswith("zh")
91
+ if is_zh:
92
+ self.neuron_id.setToolTip("要查看的神经元编号。")
93
+ self.bin_size.setToolTip("空间分箱大小。")
94
+ self.min_occupancy.setToolTip("最小占据数。")
95
+ self.smoothing.setToolTip("是否启用平滑。")
96
+ self.smooth_sigma.setToolTip("平滑强度(sigma)。")
97
+ self.mode.setToolTip("fr 需要预处理;spike 直接用事件。")
98
+ else:
99
+ self.neuron_id.setToolTip("Neuron index to inspect.")
100
+ self.bin_size.setToolTip("Spatial bin size.")
101
+ self.min_occupancy.setToolTip("Minimum occupancy.")
102
+ self.smoothing.setToolTip("Enable smoothing.")
103
+ self.smooth_sigma.setToolTip("Smoothing strength (sigma).")
104
+ self.mode.setToolTip("fr requires preprocess; spike uses events directly.")
105
+
89
106
  def _shift(self, delta: int) -> None:
90
107
  val = self.neuron_id.value() + int(delta)
91
108
  val = max(self.neuron_id.minimum(), min(self.neuron_id.maximum(), val))
@@ -117,6 +117,29 @@ class GridScoreMode(AbstractAnalysisMode):
117
117
  if "neuron_id" in meta:
118
118
  self.neuron_id.setValue(int(meta["neuron_id"]))
119
119
 
120
+ def apply_language(self, lang: str) -> None:
121
+ is_zh = str(lang).lower().startswith("zh")
122
+ if is_zh:
123
+ self.neuron_start.setToolTip("神经元起始索引。")
124
+ self.neuron_end.setToolTip("神经元结束索引(不包含)。")
125
+ self.bins.setToolTip("空间分箱数。")
126
+ self.min_occupancy.setToolTip("最小占据数。")
127
+ self.smoothing.setToolTip("是否启用平滑(需 scipy)。")
128
+ self.sigma.setToolTip("平滑强度(sigma)。")
129
+ self.overlap.setToolTip("自相关重叠比例。")
130
+ self.mode.setToolTip("fr 需要预处理;spike 直接用事件。")
131
+ self.score_thr.setToolTip("可视化阈值(仅显示)。")
132
+ else:
133
+ self.neuron_start.setToolTip("Start neuron index.")
134
+ self.neuron_end.setToolTip("End neuron index (exclusive).")
135
+ self.bins.setToolTip("Spatial bin count.")
136
+ self.min_occupancy.setToolTip("Minimum occupancy.")
137
+ self.smoothing.setToolTip("Enable smoothing (requires scipy).")
138
+ self.sigma.setToolTip("Smoothing strength (sigma).")
139
+ self.overlap.setToolTip("Autocorrelation overlap ratio.")
140
+ self.mode.setToolTip("fr requires preprocess; spike uses events directly.")
141
+ self.score_thr.setToolTip("Visualization threshold only.")
142
+
120
143
 
121
144
  class GridScoreInspectMode(GridScoreMode):
122
145
  name = "gridscore_inspect"
@@ -10,6 +10,7 @@ from PySide6.QtWidgets import (
10
10
  QHBoxLayout,
11
11
  QLabel,
12
12
  QLineEdit,
13
+ QPushButton,
13
14
  QSpinBox,
14
15
  QWidget,
15
16
  )
@@ -49,7 +50,7 @@ class PathCompareMode(AbstractAnalysisMode):
49
50
  self.dim2.setValue(2)
50
51
 
51
52
  self.use_box = QCheckBox("Use coordsbox / times_box")
52
- self.use_box.setChecked(False)
53
+ self.use_box.setChecked(True)
53
54
 
54
55
  self.interp_full = QCheckBox("Interpolate to full trajectory")
55
56
  self.interp_full.setChecked(True)
@@ -57,8 +58,13 @@ class PathCompareMode(AbstractAnalysisMode):
57
58
 
58
59
  self.coords_key = QLineEdit()
59
60
  self.coords_key.setPlaceholderText("coords / coordsbox (optional)")
61
+ self.btn_coordsbox = QPushButton("coordsbox")
62
+ self.btn_coordsbox.clicked.connect(lambda: self.coords_key.setText("coordsbox"))
63
+
60
64
  self.times_key = QLineEdit()
61
65
  self.times_key.setPlaceholderText("times_box (optional)")
66
+ self.btn_times_box = QPushButton("times_box")
67
+ self.btn_times_box.clicked.connect(lambda: self.times_key.setText("times_box"))
62
68
 
63
69
  self.slice_mode = PopupComboBox()
64
70
  self.slice_mode.addItem("Time (tmin/tmax)", userData="time")
@@ -125,8 +131,20 @@ class PathCompareMode(AbstractAnalysisMode):
125
131
  form.addRow(self._dims2d_label, dims_2d)
126
132
  form.addRow(self.use_box)
127
133
  form.addRow(self.interp_full)
128
- form.addRow("coords key", self.coords_key)
129
- form.addRow("times key", self.times_key)
134
+ coords_row = QWidget()
135
+ coords_layout = QHBoxLayout(coords_row)
136
+ coords_layout.setContentsMargins(0, 0, 0, 0)
137
+ coords_layout.addWidget(self.coords_key, 1)
138
+ coords_layout.addWidget(self.btn_coordsbox)
139
+
140
+ times_row = QWidget()
141
+ times_layout = QHBoxLayout(times_row)
142
+ times_layout.setContentsMargins(0, 0, 0, 0)
143
+ times_layout.addWidget(self.times_key, 1)
144
+ times_layout.addWidget(self.btn_times_box)
145
+
146
+ form.addRow("coords key", coords_row)
147
+ form.addRow("times key", times_row)
130
148
  form.addRow("Slice mode", self.slice_mode)
131
149
  form.addRow("tmin (sec, -1=auto)", self.tmin)
132
150
  form.addRow("tmax (sec, -1=auto)", self.tmax)
@@ -150,6 +168,10 @@ class PathCompareMode(AbstractAnalysisMode):
150
168
  def _refresh_enabled() -> None:
151
169
  use_box = bool(self.use_box.isChecked())
152
170
  self.interp_full.setEnabled(use_box)
171
+ self.coords_key.setEnabled(use_box)
172
+ self.times_key.setEnabled(use_box)
173
+ self.btn_coordsbox.setEnabled(use_box)
174
+ self.btn_times_box.setEnabled(use_box)
153
175
 
154
176
  def _refresh_slice_mode() -> None:
155
177
  is_time = self.slice_mode.currentData() == "time"
@@ -197,3 +219,50 @@ class PathCompareMode(AbstractAnalysisMode):
197
219
  "no_wrap": bool(self.no_wrap.isChecked()),
198
220
  "animation_format": "gif" if self.save_gif.isChecked() else "none",
199
221
  }
222
+
223
+ def apply_language(self, lang: str) -> None:
224
+ is_zh = str(lang).lower().startswith("zh")
225
+ if is_zh:
226
+ self.angle_scale.setToolTip("角度尺度:auto / rad / deg / unit。")
227
+ self.dim_mode.setToolTip("解码维度模式(1D/2D)。")
228
+ self.dim.setToolTip("1D 解码维度索引。")
229
+ self.dim1.setToolTip("2D 解码维度 1。")
230
+ self.dim2.setToolTip("2D 解码维度 2。")
231
+ self.use_box.setToolTip("使用 coordsbox / times_box 对齐(推荐速度过滤后开启)。")
232
+ self.interp_full.setToolTip("插值回完整轨迹。")
233
+ self.coords_key.setToolTip("可选:解码坐标键(默认 coords/coordsbox)。")
234
+ self.times_key.setToolTip("可选:times_box 键名。")
235
+ self.btn_coordsbox.setToolTip("填入 coordsbox。")
236
+ self.btn_times_box.setToolTip("填入 times_box。")
237
+ self.slice_mode.setToolTip("按时间或索引裁剪。")
238
+ self.tmin.setToolTip("起始时间(秒),-1 自动。")
239
+ self.tmax.setToolTip("结束时间(秒),-1 自动。")
240
+ self.imin.setToolTip("起始索引,-1 自动。")
241
+ self.imax.setToolTip("结束索引,-1 自动。")
242
+ self.stride.setToolTip("采样步长。")
243
+ self.tail.setToolTip("尾迹长度(帧)。")
244
+ self.fps.setToolTip("动画帧率。")
245
+ self.no_wrap.setToolTip("禁用角度环绕。")
246
+ self.save_gif.setToolTip("保存 GIF 动画。")
247
+ else:
248
+ self.angle_scale.setToolTip("Angle scale: auto / rad / deg / unit.")
249
+ self.dim_mode.setToolTip("Decode dimension mode (1D/2D).")
250
+ self.dim.setToolTip("1D decoded dimension index.")
251
+ self.dim1.setToolTip("2D decoded dimension 1.")
252
+ self.dim2.setToolTip("2D decoded dimension 2.")
253
+ self.use_box.setToolTip("Use coordsbox/times_box alignment (recommended with speed_filter).")
254
+ self.interp_full.setToolTip("Interpolate back to full trajectory.")
255
+ self.coords_key.setToolTip("Optional decode coords key (default coords/coordsbox).")
256
+ self.times_key.setToolTip("Optional times_box key.")
257
+ self.btn_coordsbox.setToolTip("Fill coordsbox.")
258
+ self.btn_times_box.setToolTip("Fill times_box.")
259
+ self.slice_mode.setToolTip("Slice by time or index.")
260
+ self.tmin.setToolTip("Start time (sec), -1 = auto.")
261
+ self.tmax.setToolTip("End time (sec), -1 = auto.")
262
+ self.imin.setToolTip("Start index, -1 = auto.")
263
+ self.imax.setToolTip("End index, -1 = auto.")
264
+ self.stride.setToolTip("Sampling stride.")
265
+ self.tail.setToolTip("Trail length (frames).")
266
+ self.fps.setToolTip("Animation FPS.")
267
+ self.no_wrap.setToolTip("Disable angle wrap.")
268
+ self.save_gif.setToolTip("Save GIF animation.")
@@ -110,3 +110,30 @@ class TDAMode(AbstractAnalysisMode):
110
110
  self.maxdim.setValue(2)
111
111
  elif preset == "hd":
112
112
  self.maxdim.setValue(1)
113
+
114
+ def apply_language(self, lang: str) -> None:
115
+ is_zh = str(lang).lower().startswith("zh")
116
+ if is_zh:
117
+ self.dim.setToolTip("PCA 维度(常见起点 6–12)。")
118
+ self.num_times.setToolTip("时间下采样步长;越大越快但可能丢细节。")
119
+ self.active_times.setToolTip("选取最活跃时间点数;过小不稳,过大更慢。")
120
+ self.k.setToolTip("采样/去噪相关参数,影响速度与稳定性。")
121
+ self.n_points.setToolTip("点云代表点数量,越大越慢。")
122
+ self.metric.setToolTip("距离度量;推荐 cosine。")
123
+ self.nbs.setToolTip("邻域规模参数,影响稳定性与速度。")
124
+ self.maxdim.setToolTip("最大同调维度;先 1 再 2。")
125
+ self.coeff.setToolTip("有限域系数(默认 47)。")
126
+ self.do_shuffle.setToolTip("显著性检验;代价高,建议少量。")
127
+ self.num_shuffles.setToolTip("Shuffle 次数(越多越慢)。")
128
+ else:
129
+ self.dim.setToolTip("PCA dimension (typical 6–12).")
130
+ self.num_times.setToolTip("Time downsampling step; larger is faster but less detail.")
131
+ self.active_times.setToolTip("Number of most active points; too small is unstable.")
132
+ self.k.setToolTip("Sampling/denoising parameter; affects speed/stability.")
133
+ self.n_points.setToolTip("Number of representative points; larger is slower.")
134
+ self.metric.setToolTip("Distance metric; recommend cosine.")
135
+ self.nbs.setToolTip("Neighborhood parameter; affects stability and speed.")
136
+ self.maxdim.setToolTip("Max homology dimension; start with 1, then 2.")
137
+ self.coeff.setToolTip("Finite field coefficient (default 47).")
138
+ self.do_shuffle.setToolTip("Significance test; expensive, keep small.")
139
+ self.num_shuffles.setToolTip("Number of shuffles (more is slower).")
@@ -14,6 +14,14 @@ QLabel#muted {
14
14
  color: #9ca3af;
15
15
  }
16
16
 
17
+ QToolTip {
18
+ color: #e5e7eb;
19
+ background-color: #111827;
20
+ border: 1px solid #4b5563;
21
+ border-radius: 4px;
22
+ padding: 4px 8px;
23
+ }
24
+
17
25
  QFrame#card, QGroupBox#card {
18
26
  background-color: #1f2937;
19
27
  border: 1px solid #374151;
@@ -14,6 +14,14 @@ QLabel#muted {
14
14
  color: #6b7280;
15
15
  }
16
16
 
17
+ QToolTip {
18
+ color: #111827;
19
+ background-color: #fef3c7;
20
+ border: 1px solid #f59e0b;
21
+ border-radius: 4px;
22
+ padding: 4px 8px;
23
+ }
24
+
17
25
  QFrame#card, QGroupBox#card {
18
26
  background-color: #ffffff;
19
27
  border: 1px solid #e5e7eb;
@@ -206,6 +206,9 @@ class AnalysisPage(QWidget):
206
206
  self.run_btn.setText("运行分析" if is_zh else "Run Analysis")
207
207
  self.stop_btn.setText("停止" if is_zh else "Stop")
208
208
  self.logs_label.setText("日志" if is_zh else "Logs")
209
+ self.analysis_mode.setToolTip(
210
+ "选择分析模块" if is_zh else "Select an analysis mode to run."
211
+ )
209
212
 
210
213
  if self._last_state is not None:
211
214
  self._update_info(self._last_state)
@@ -216,6 +219,10 @@ class AnalysisPage(QWidget):
216
219
  else "Mode=— | preset=— | preprocess=— | spike_main_shape=—"
217
220
  )
218
221
 
222
+ for mode in self._modes.values():
223
+ if hasattr(mode, "apply_language"):
224
+ mode.apply_language(self._lang)
225
+
219
226
  def load_state(self, state) -> None:
220
227
  self._last_state = state
221
228
  self._update_info(state)
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
6
 
7
+ import numpy as np
8
+
7
9
  from PySide6.QtCore import QSettings, Qt, Signal
8
10
  from PySide6.QtGui import QColor
9
11
  from PySide6.QtWidgets import (
@@ -15,6 +17,7 @@ from PySide6.QtWidgets import (
15
17
  QGroupBox,
16
18
  QHBoxLayout,
17
19
  QLabel,
20
+ QLineEdit,
18
21
  QProgressBar,
19
22
  QPushButton,
20
23
  QSpinBox,
@@ -86,6 +89,12 @@ class PreprocessPage(QWidget):
86
89
  self.preset.addItems(["grid", "hd", "none"])
87
90
  self.preset.setToolTip("Preset hints apply to analysis mode defaults.")
88
91
 
92
+ self.input_source = PopupComboBox()
93
+ self.input_source.addItem("Local file", userData="local")
94
+ self.input_source.addItem("CANNs dataset", userData="dataset")
95
+ self.input_source.addItem("URL", userData="url")
96
+ self.input_source.currentIndexChanged.connect(self._toggle_input_source)
97
+
89
98
  self.label_mode = QLabel("Mode")
90
99
  top_row.addWidget(self.label_mode)
91
100
  top_row.addWidget(self.input_mode)
@@ -93,6 +102,10 @@ class PreprocessPage(QWidget):
93
102
  self.label_preset = QLabel("Preset")
94
103
  top_row.addWidget(self.label_preset)
95
104
  top_row.addWidget(self.preset)
105
+ top_row.addSpacing(16)
106
+ self.label_source = QLabel("Source")
107
+ top_row.addWidget(self.label_source)
108
+ top_row.addWidget(self.input_source)
96
109
  top_row.addStretch(1)
97
110
  input_layout.addLayout(top_row)
98
111
 
@@ -126,6 +139,46 @@ class PreprocessPage(QWidget):
126
139
  input_layout.addLayout(neuron_row)
127
140
  input_layout.addLayout(traj_row)
128
141
 
142
+ self.dataset_group = QGroupBox("Dataset")
143
+ self.dataset_group.setObjectName("card")
144
+ dataset_form = QFormLayout(self.dataset_group)
145
+
146
+ self.dataset_key = PopupComboBox()
147
+ self.dataset_key.addItem("grid_1", userData="grid_1")
148
+ self.dataset_key.addItem("grid_2", userData="grid_2")
149
+ self.dataset_key.addItem("roi_data", userData="roi_data")
150
+ self.dataset_key.addItem("left_right_data_of", userData="left_right_data_of")
151
+ self.dataset_key.currentIndexChanged.connect(self._update_dataset_hint)
152
+
153
+ self.dataset_session = QLineEdit()
154
+ self.dataset_session.setPlaceholderText("e.g. 26034_3")
155
+
156
+ self.dataset_filename = QLineEdit()
157
+ self.dataset_filename.setPlaceholderText("e.g. 26034_3_ASA_full.npz")
158
+
159
+ self.dataset_url = QLineEdit()
160
+ self.dataset_url.setPlaceholderText("https://.../data.npz")
161
+
162
+ self.dataset_hint = QLabel("")
163
+ self.dataset_hint.setObjectName("muted")
164
+ self.dataset_hint.setWordWrap(True)
165
+
166
+ dataset_form.addRow("Dataset", self.dataset_key)
167
+ self.label_dataset = dataset_form.labelForField(self.dataset_key)
168
+ dataset_form.addRow("Session id", self.dataset_session)
169
+ self.label_session = dataset_form.labelForField(self.dataset_session)
170
+ dataset_form.addRow("Filename", self.dataset_filename)
171
+ self.label_filename = dataset_form.labelForField(self.dataset_filename)
172
+ url_row = QWidget()
173
+ url_layout = QHBoxLayout(url_row)
174
+ url_layout.setContentsMargins(0, 0, 0, 0)
175
+ url_layout.addWidget(self.dataset_url, 1)
176
+ dataset_form.addRow("URL", url_row)
177
+ self.label_url = dataset_form.labelForField(self.dataset_url)
178
+ dataset_form.addRow(self.dataset_hint)
179
+
180
+ input_layout.addWidget(self.dataset_group)
181
+
129
182
  top_layout.addWidget(input_group)
130
183
 
131
184
  # Preprocess group
@@ -244,6 +297,8 @@ class PreprocessPage(QWidget):
244
297
  self.asa_browse.clicked.connect(self._update_run_enabled)
245
298
 
246
299
  self._toggle_input_mode()
300
+ self._toggle_input_source()
301
+ self._update_dataset_hint()
247
302
  self._toggle_embed_params()
248
303
  self._update_run_enabled()
249
304
  self._apply_card_effects([input_group, preprocess_group, preclass_group])
@@ -270,6 +325,7 @@ class PreprocessPage(QWidget):
270
325
 
271
326
  self.label_mode.setText("模式" if is_zh else "Mode")
272
327
  self.label_preset.setText("预设" if is_zh else "Preset")
328
+ self.label_source.setText("来源" if is_zh else "Source")
273
329
  if self.label_method is not None:
274
330
  self.label_method.setText("方法" if is_zh else "Method")
275
331
  if self.label_preclass is not None:
@@ -317,6 +373,72 @@ class PreprocessPage(QWidget):
317
373
  self.stop_btn.setText("停止" if is_zh else "Stop")
318
374
  self.logs_label.setText("日志" if is_zh else "Logs")
319
375
 
376
+ self.dataset_group.setTitle("数据集" if is_zh else "Dataset")
377
+ if self.label_dataset is not None:
378
+ self.label_dataset.setText("数据集" if is_zh else "Dataset")
379
+ if self.label_session is not None:
380
+ self.label_session.setText("Session id" if not is_zh else "会话 id")
381
+ if self.label_filename is not None:
382
+ self.label_filename.setText("Filename" if not is_zh else "文件名")
383
+ if self.label_url is not None:
384
+ self.label_url.setText("URL")
385
+
386
+ self.input_source.setToolTip(
387
+ "选择本地文件、内置数据集或 URL"
388
+ if is_zh
389
+ else "Choose local file, built-in dataset, or URL."
390
+ )
391
+ self.dataset_key.setToolTip(
392
+ "选择内置数据集" if is_zh else "Select a built-in dataset."
393
+ )
394
+ self.dataset_session.setToolTip(
395
+ "Left-Right 数据集的会话 id。" if is_zh else "Session id for Left-Right dataset."
396
+ )
397
+ self.dataset_filename.setToolTip(
398
+ "Left-Right 数据集文件名。" if is_zh else "Filename within Left-Right dataset."
399
+ )
400
+ self.dataset_url.setToolTip(
401
+ "直接加载 .npz 链接。" if is_zh else "Load a .npz URL directly."
402
+ )
403
+
404
+ self.input_mode.setToolTip(
405
+ "仅支持 ASA .npz 输入" if is_zh else "Only ASA .npz input is supported in this GUI."
406
+ )
407
+ self.preprocess_method.setToolTip(
408
+ "嵌入会生成稠密矩阵供 TDA/FR 使用"
409
+ if is_zh
410
+ else "Embedding builds a dense spike matrix for TDA/FR."
411
+ )
412
+ self.embed_res.setToolTip(
413
+ "时间分箱分辨率(与 t 单位一致)。" if is_zh else "Time bin resolution (same unit as t)."
414
+ )
415
+ self.embed_dt.setToolTip(
416
+ "时间步长(与 t 单位一致)。" if is_zh else "Time step (same unit as t)."
417
+ )
418
+ self.embed_sigma.setToolTip(
419
+ "高斯平滑尺度,越大越平滑。" if is_zh else "Gaussian smoothing scale."
420
+ )
421
+ self.embed_smooth.setToolTip("是否启用平滑。" if is_zh else "Enable smoothing.")
422
+ self.embed_speed_filter.setToolTip(
423
+ "过滤低速时间点(常见于 grid 数据)。"
424
+ if is_zh
425
+ else "Remove low-speed samples (common for grid data)."
426
+ )
427
+ self.embed_min_speed.setToolTip(
428
+ "速度阈值(与 t/x/y 单位一致)。"
429
+ if is_zh
430
+ else "Speed threshold (same unit as t/x/y)."
431
+ )
432
+
433
+ self.dataset_session.setPlaceholderText("例如 26034_3" if is_zh else "e.g. 26034_3")
434
+ self.dataset_filename.setPlaceholderText(
435
+ "例如 26034_3_ASA_full.npz" if is_zh else "e.g. 26034_3_ASA_full.npz"
436
+ )
437
+ self.dataset_url.setPlaceholderText(
438
+ "https://.../data.npz" if not is_zh else "https://.../data.npz"
439
+ )
440
+ self._update_dataset_hint()
441
+
320
442
  def _show_help(self) -> None:
321
443
  lang = str(QSettings("canns", "asa_gui").value("lang", "en"))
322
444
  title = (
@@ -357,6 +479,63 @@ class PreprocessPage(QWidget):
357
479
  self.traj_zone.setVisible(not use_asa)
358
480
  self.traj_browse.setVisible(not use_asa)
359
481
 
482
+ def _toggle_input_source(self) -> None:
483
+ source = self.input_source.currentData() or "local"
484
+ use_local = source == "local"
485
+ self.asa_zone.setVisible(use_local)
486
+ self.asa_browse.setVisible(use_local)
487
+ self.asa_hint.setVisible(use_local)
488
+ self.dataset_group.setVisible(not use_local)
489
+ self._update_dataset_hint()
490
+ self._update_run_enabled()
491
+
492
+ def _update_dataset_hint(self) -> None:
493
+ source = self.input_source.currentData() or "local"
494
+ key = self.dataset_key.currentData() or self.dataset_key.currentText()
495
+ is_left_right = key == "left_right_data_of"
496
+ is_zh = str(self._lang).lower().startswith("zh")
497
+ show_dataset = source == "dataset"
498
+ if self.label_session is not None:
499
+ self.label_session.setVisible(is_left_right and show_dataset)
500
+ self.dataset_session.setVisible(is_left_right and show_dataset)
501
+ if self.label_filename is not None:
502
+ self.label_filename.setVisible(is_left_right and show_dataset)
503
+ self.dataset_filename.setVisible(is_left_right and show_dataset)
504
+ if self.label_dataset is not None:
505
+ self.label_dataset.setVisible(show_dataset)
506
+ self.dataset_key.setVisible(show_dataset)
507
+ if self.label_url is not None:
508
+ self.label_url.setVisible(source == "url")
509
+ self.dataset_url.setVisible(source == "url")
510
+
511
+ hint = ""
512
+ if source == "url":
513
+ hint = (
514
+ "加载包含 spike/x/y/t 的 .npz 链接。"
515
+ if is_zh
516
+ else "Load a .npz URL that contains spike/x/y/t."
517
+ )
518
+ elif source == "dataset":
519
+ try:
520
+ from canns.data import datasets as _datasets
521
+
522
+ info = _datasets.DATASETS.get(str(key))
523
+ if info:
524
+ size = info.get("size_mb", "?")
525
+ desc = info.get("description", "")
526
+ hint = f"{desc} (size ~{size} MB)" if not is_zh else f"{desc} (约 {size} MB)"
527
+ except Exception:
528
+ hint = ""
529
+ if is_left_right:
530
+ hint = (
531
+ (hint + "\n") if hint else ""
532
+ ) + (
533
+ "左/右数据集需要 session id 和文件名。"
534
+ if is_zh
535
+ else "Left-right dataset requires session id + filename."
536
+ )
537
+ self.dataset_hint.setText(hint)
538
+
360
539
  def _toggle_embed_params(self) -> None:
361
540
  method = self.preprocess_method.currentData() or "none"
362
541
  self.embed_params.setVisible(method == "embed_spike_trains")
@@ -391,6 +570,96 @@ class PreprocessPage(QWidget):
391
570
  }
392
571
  return params
393
572
 
573
+ def _slugify(self, text: str) -> str:
574
+ out = []
575
+ for ch in text:
576
+ if ch.isalnum() or ch in ("-", "_"):
577
+ out.append(ch)
578
+ else:
579
+ out.append("_")
580
+ return "".join(out).strip("_") or "dataset"
581
+
582
+ def _normalize_npz_payload(self, data: dict) -> dict:
583
+ payload: dict = {}
584
+ for key, value in data.items():
585
+ if isinstance(value, dict):
586
+ payload[key] = np.array(value, dtype=object)
587
+ else:
588
+ payload[key] = value
589
+ return payload
590
+
591
+ def _prepare_dataset_asa(self) -> str | None:
592
+ source = self.input_source.currentData() or "local"
593
+ if source == "local":
594
+ return self.asa_zone.path()
595
+
596
+ data = None
597
+ label = ""
598
+ if source == "url":
599
+ url = self.dataset_url.text().strip()
600
+ if not url:
601
+ self.log_box.log("Please provide a dataset URL.")
602
+ return None
603
+ label = self._slugify(Path(url).stem or "dataset_url")
604
+ try:
605
+ from canns.data import datasets as _datasets
606
+
607
+ data = _datasets.load(url, file_type="numpy")
608
+ except Exception as exc:
609
+ self.log_box.log(f"Failed to load URL dataset: {exc}")
610
+ return None
611
+ elif source == "dataset":
612
+ key = self.dataset_key.currentData() or self.dataset_key.currentText()
613
+ label = self._slugify(str(key))
614
+ try:
615
+ from canns.data import loaders as _loaders
616
+ from canns.data import datasets as _datasets
617
+
618
+ if key == "roi_data":
619
+ data = _loaders.load_roi_data()
620
+ elif key == "left_right_data_of":
621
+ session_id = self.dataset_session.text().strip()
622
+ filename = self.dataset_filename.text().strip()
623
+ if not session_id or not filename:
624
+ self.log_box.log("Left-Right dataset requires session id + filename.")
625
+ return None
626
+ data = _loaders.load_left_right_npz(session_id=session_id, filename=filename)
627
+ label = self._slugify(f"{session_id}_{filename}")
628
+ elif str(key).startswith("grid_"):
629
+ data = _loaders.load_grid_data(dataset_key=str(key))
630
+ else:
631
+ path = _datasets.download_dataset(str(key))
632
+ if path is not None and path.exists():
633
+ data = dict(np.load(path, allow_pickle=True))
634
+ except Exception as exc:
635
+ self.log_box.log(f"Failed to load dataset '{key}': {exc}")
636
+ return None
637
+
638
+ if data is None:
639
+ self.log_box.log("Dataset load failed or returned empty data.")
640
+ return None
641
+
642
+ if not isinstance(data, dict):
643
+ self.log_box.log("Dataset is not an ASA .npz dict (missing spike/x/y/t).")
644
+ return None
645
+
646
+ payload = self._normalize_npz_payload(data)
647
+ if "spike" not in payload or "t" not in payload:
648
+ self.log_box.log("Dataset does not contain required keys: spike, t.")
649
+ return None
650
+
651
+ out_dir = Path.cwd() / "Results" / "asa_gui_datasets"
652
+ out_dir.mkdir(parents=True, exist_ok=True)
653
+ out_path = out_dir / f"{label}.npz"
654
+ try:
655
+ np.savez_compressed(out_path, **payload)
656
+ except Exception as exc:
657
+ self.log_box.log(f"Failed to save dataset as npz: {exc}")
658
+ return None
659
+
660
+ self.asa_zone.set_path(str(out_path))
661
+ return str(out_path)
662
+
394
663
  def _run_preprocess(self) -> None:
395
664
  if self._workers.is_running():
396
665
  self.log_box.log("A task is already running.")
@@ -404,6 +673,9 @@ class PreprocessPage(QWidget):
404
673
  neuron_file = self.neuron_zone.path() if input_mode != "asa" else None
405
674
  traj_file = self.traj_zone.path() if input_mode != "asa" else None
406
675
 
676
+ if input_mode == "asa":
677
+ asa_file = self._prepare_dataset_asa()
678
+
407
679
  if not self._validate_inputs(asa_file):
408
680
  return
409
681
 
@@ -480,13 +752,17 @@ class PreprocessPage(QWidget):
480
752
  return True
481
753
 
482
754
  def _update_run_enabled(self) -> None:
755
+ source = self.input_source.currentData() or "local"
483
756
  asa_file = self.asa_zone.path()
484
757
  valid = False
485
- if asa_file:
758
+ if source == "local" and asa_file:
486
759
  path = Path(asa_file)
487
760
  valid = path.exists() and path.suffix.lower() == ".npz"
488
761
  self.run_btn.setEnabled(True)
489
- if valid:
490
- self.run_btn.setToolTip("")
762
+ if source == "local":
763
+ if valid:
764
+ self.run_btn.setToolTip("")
765
+ else:
766
+ self.run_btn.setToolTip("Select a valid ASA .npz file to run preprocessing.")
491
767
  else:
492
- self.run_btn.setToolTip("Select a valid ASA .npz file to run preprocessing.")
768
+ self.run_btn.setToolTip("Dataset/URL will be loaded on Run.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: canns
3
- Version: 0.14.1
3
+ Version: 0.14.3
4
4
  Summary: A Python Library for Continuous Attractor Neural Networks
5
5
  Project-URL: Repository, https://github.com/routhleck/canns
6
6
  Author-email: Sichao He <sichaohe@outlook.com>
@@ -44,6 +44,7 @@ Requires-Dist: imageio; extra == 'gui'
44
44
  Requires-Dist: pillow; extra == 'gui'
45
45
  Requires-Dist: pyside6>=6.6.0; extra == 'gui'
46
46
  Requires-Dist: qtawesome; extra == 'gui'
47
+ Requires-Dist: requests>=2.31.0; extra == 'gui'
47
48
  Provides-Extra: tpu
48
49
  Requires-Dist: brainpy[tpu]; (platform_system == 'Linux') and extra == 'tpu'
49
50
  Description-Content-Type: text/markdown
@@ -86,16 +86,16 @@ canns/pipeline/asa_gui/__main__.py,sha256=2UOQtIE5oXkcq9HcuY13M3Jk6-uaDu8A0VJfvr
86
86
  canns/pipeline/asa_gui/app.py,sha256=Wd-tVGNPE1mQ0S9bET-cyjfj5UWsTIFFHOQRu0lngBs,833
87
87
  canns/pipeline/asa_gui/main_window.py,sha256=kQp8DTmp7SuYVCglh5lVja7DyQ7hAOxPgCEUmjKHgbk,7019
88
88
  canns/pipeline/asa_gui/analysis_modes/__init__.py,sha256=m5tra-3lOcKi09HV3WxvTnDuF5lgV92C_fHH_4V87zw,990
89
- canns/pipeline/asa_gui/analysis_modes/base.py,sha256=-xuzrr5-XItZldl7DUYfeLLvvXi-i7wpzk1IkTaPAzQ,1220
89
+ canns/pipeline/asa_gui/analysis_modes/base.py,sha256=SqZhsD6umV0ecHGca5FcCx7G2eeskE8HZToRBc3Y5AA,1335
90
90
  canns/pipeline/asa_gui/analysis_modes/batch_mode.py,sha256=sua3cD6G4vTGT7fUSrdvYNwldA3KZ1O4MCCsSOX4knc,531
91
- canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py,sha256=RCZv2TZ7_BmaQZFFradsLlumB7MgfiMENxWjxGFZB8Q,1748
92
- canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py,sha256=HBb7nrdfKz8hY5QkY8YhKVWxtvRyyneFo6fN7dumVSw,7115
93
- canns/pipeline/asa_gui/analysis_modes/decode_mode.py,sha256=wPuuoXA2eQt_tv4Nh57cBNvzrW87cqRlKS6ckVgt280,1586
94
- canns/pipeline/asa_gui/analysis_modes/fr_mode.py,sha256=mS6FpLBwJB-ugYKgPBsVP6X4cKMlw6S0gsT5Q8qTNls,2928
95
- canns/pipeline/asa_gui/analysis_modes/frm_mode.py,sha256=OYl0CkUjqy2azu_T7mFWJ58lSTaFCsFFfBhmg1nRebE,3038
96
- canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py,sha256=xcq-HLJxqYFu5Nck9cLN6Rp2g8eUDjSGI8_3AkEXNYg,4321
97
- canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py,sha256=5aZpClyO813tXQsbUuBchR2uNHyjQXQeNEINNaZyfrM,6877
98
- canns/pipeline/asa_gui/analysis_modes/tda_mode.py,sha256=jNlRwpzsYIKUKFqRyjDVChoJib7h_5iz2te-mK7Qf1I,4119
91
+ canns/pipeline/asa_gui/analysis_modes/cohomap_mode.py,sha256=kIB9zzh_hmhxSXFrv8FLabFLkOp_FhBOs1t_2gPass4,2360
92
+ canns/pipeline/asa_gui/analysis_modes/cohospace_mode.py,sha256=yfLpw0-Q9oMiCIJg-ca_v93__OgXfrGeEFd9DsOc0a4,9368
93
+ canns/pipeline/asa_gui/analysis_modes/decode_mode.py,sha256=uEe3lfWAA0pqmCXzNpaA3A1nrDedTN43_f1R_ssg_A8,2304
94
+ canns/pipeline/asa_gui/analysis_modes/fr_mode.py,sha256=xzx1RhGVDbx6huEtEHGfUqWgRN_C6Sf-Ycj9BzIgTRY,3961
95
+ canns/pipeline/asa_gui/analysis_modes/frm_mode.py,sha256=8rgh_P7dxYJfx2TxrhD00Ja6tK0q6NboqwZ7n0Sw_2U,3992
96
+ canns/pipeline/asa_gui/analysis_modes/gridscore_mode.py,sha256=XC-O2lMx3NPxUkSoZo_69g7B_yFAYUnKIPKLj9-gKM4,5712
97
+ canns/pipeline/asa_gui/analysis_modes/pathcompare_mode.py,sha256=oRZPB8y7ORDFDPk99joRNQRj_e5qmR-GuJHrGnSynS4,10808
98
+ canns/pipeline/asa_gui/analysis_modes/tda_mode.py,sha256=xnsWv_zfstzYPf_nLbQkCNHxxhYRznz4m-73ClaBQKs,6094
99
99
  canns/pipeline/asa_gui/controllers/__init__.py,sha256=RuQz960T4kEuQsBI_cjS0cQgFyqAdblLXy_dDoLPbTE,198
100
100
  canns/pipeline/asa_gui/controllers/analysis_controller.py,sha256=8cKs-RYHh_NflP7xeS0u0_y9WsZ268H1Wyp-wHZC97I,1769
101
101
  canns/pipeline/asa_gui/controllers/preprocess_controller.py,sha256=uNZifNGadYPxAVyWnfonOs5pwCgxwB1nrBGqvv8Y3hU,2825
@@ -109,8 +109,8 @@ canns/pipeline/asa_gui/models/config.py,sha256=oDAeNlReKJEITc8B5AT9xxQl3Ug1yaGs6
109
109
  canns/pipeline/asa_gui/models/job.py,sha256=_HdhJIzdH9OSKrRpcl3WCS4O04Bvur5uT9yb9oOWBuE,852
110
110
  canns/pipeline/asa_gui/models/presets.py,sha256=zEtR1_35ovSuGa3xMOvZoVDJJUK0rSi8OOlfkQWFvFE,519
111
111
  canns/pipeline/asa_gui/resources/__init__.py,sha256=xaC4dS2THCTTKOF6MDHMOGGPni9Nnjmeqj7fCaoxzwI,575
112
- canns/pipeline/asa_gui/resources/dark.qss,sha256=s9Uhyod7aLwIC5QqYOVIKN7NofcXbV1ESZk-j98YMY4,2990
113
- canns/pipeline/asa_gui/resources/light.qss,sha256=B8rU85VrYMKf8QasvRiR-tCypov7pzIOFr7lcL81mcw,2883
112
+ canns/pipeline/asa_gui/resources/dark.qss,sha256=xqlPIFtnKKfBJbcL-kZ-qwFKTl9ADEU2mN7PDJiT7SE,3132
113
+ canns/pipeline/asa_gui/resources/light.qss,sha256=PuFpiWoU3qfGczTWlSjkHGDpMr3Jh0x6SZ0hdCvg_b4,3025
114
114
  canns/pipeline/asa_gui/resources/logo.ico,sha256=V54_QvIzs6TK1K28zX915taioAM1gPESAKRCRPAjR4U,125806
115
115
  canns/pipeline/asa_gui/resources/logo.svg,sha256=UQqbGRtnROO2BdN3XMkovdBqWew69Z9Sr7SUof2_n3o,48323
116
116
  canns/pipeline/asa_gui/resources/styles.qss,sha256=MaWWGn3f9yvOYpgdgy8VyITWk5vGGPut2H_rnQM4-EU,2188
@@ -121,8 +121,8 @@ canns/pipeline/asa_gui/utils/validators.py,sha256=x5Tw2Pk434vlfQKBYgUOJPL6MLw0Oh
121
121
  canns/pipeline/asa_gui/views/__init__.py,sha256=ThoLlMw7bKxA7lkv_AvIR1mbpaoM0vkIxVP1p7mlzQM,28
122
122
  canns/pipeline/asa_gui/views/help_content.py,sha256=kL7MSwc9v3gHLz86Apiy64xbwymt9r7sPEjz5ka6EB0,8452
123
123
  canns/pipeline/asa_gui/views/pages/__init__.py,sha256=xB7VTY_hKfoCNMGeWZbV3gHG9ErrzmwqW30UlUkbqgE,161
124
- canns/pipeline/asa_gui/views/pages/analysis_page.py,sha256=CNt-ZXaWuHa1gOkTgBpZYYsrklW0L8WS74PL_9NnlZ4,22538
125
- canns/pipeline/asa_gui/views/pages/preprocess_page.py,sha256=N7IwQCNtqd30cV81S2AgejWJKIJ2aD9IVHVWSFa26dA,19472
124
+ canns/pipeline/asa_gui/views/pages/analysis_page.py,sha256=X6PGW_cgvAiNFqUpsS2TuVWl258Q6Q90C9NEQT1TetQ,22807
125
+ canns/pipeline/asa_gui/views/pages/preprocess_page.py,sha256=AHWidQqNKF0TBx8i2dDmPfxjT4Qy45_FjBYh-O1zB8E,31364
126
126
  canns/pipeline/asa_gui/views/panels/__init__.py,sha256=Spqmc0Sjh38cgr42gszmiogZQFFOLN1yL7ekSpVJCrE,36
127
127
  canns/pipeline/asa_gui/views/widgets/__init__.py,sha256=xaTYXw99OL8ye1cpfoKgSwqC7c2B6lrLLsYHRB16m64,481
128
128
  canns/pipeline/asa_gui/views/widgets/artifacts_tab.py,sha256=U_fuOCfSmkDhx3G97aod-8UPSIFVz_MrsU4b_ik_5qE,1431
@@ -158,8 +158,8 @@ canns/trainer/utils.py,sha256=ZdoLiRqFLfKXsWi0KX3wGUp0OqFikwiou8dPf3xvFhE,2847
158
158
  canns/typing/__init__.py,sha256=mXySdfmD8fA56WqZTb1Nj-ZovcejwLzNjuk6PRfTwmA,156
159
159
  canns/utils/__init__.py,sha256=OMyZ5jqZAIUS2Jr0qcnvvrx6YM-BZ1EJy5uZYeA3HC0,366
160
160
  canns/utils/benchmark.py,sha256=oJ7nvbvnQMh4_MZh7z160NPLp-197X0rEnmnLHYlev4,1361
161
- canns-0.14.1.dist-info/METADATA,sha256=CQzDKQODTTCPR2zdw9q6mEzfhGdvuA8xOhIg0maVhTc,9751
162
- canns-0.14.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
163
- canns-0.14.1.dist-info/entry_points.txt,sha256=57YF2HZp_BG3GeGB8L0m3wR1sSfNyMXF1q4CKEjce6U,164
164
- canns-0.14.1.dist-info/licenses/LICENSE,sha256=u6NJ1N-QSnf5yTwSk5UvFAdU2yKD0jxG0Xa91n1cPO4,11306
165
- canns-0.14.1.dist-info/RECORD,,
161
+ canns-0.14.3.dist-info/METADATA,sha256=UMyUaYFYtHoS60udBftAohcR0mIxJygdyUXyVdxfK18,9799
162
+ canns-0.14.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
163
+ canns-0.14.3.dist-info/entry_points.txt,sha256=57YF2HZp_BG3GeGB8L0m3wR1sSfNyMXF1q4CKEjce6U,164
164
+ canns-0.14.3.dist-info/licenses/LICENSE,sha256=u6NJ1N-QSnf5yTwSk5UvFAdU2yKD0jxG0Xa91n1cPO4,11306
165
+ canns-0.14.3.dist-info/RECORD,,
File without changes