ZaksPhysicsLibrary 1.4.0__tar.gz → 1.7.0__tar.gz

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.
Files changed (25) hide show
  1. {zaksphysicslibrary-1.4.0/ZaksPhysicsLibrary.egg-info → zaksphysicslibrary-1.7.0}/PKG-INFO +43 -5
  2. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/__init__.py +30 -0
  3. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/analysis.py +193 -0
  4. zaksphysicslibrary-1.7.0/PhysicsLibrary/field_study_validation.py +433 -0
  5. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/oxysoft_loader.py +7 -0
  6. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/processing_TDT.py +10 -0
  7. zaksphysicslibrary-1.7.0/PhysicsLibrary/splice.py +113 -0
  8. zaksphysicslibrary-1.7.0/PhysicsLibrary/text_field_study.py +405 -0
  9. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/README.md +37 -2
  10. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0/ZaksPhysicsLibrary.egg-info}/PKG-INFO +43 -5
  11. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/ZaksPhysicsLibrary.egg-info/SOURCES.txt +3 -0
  12. zaksphysicslibrary-1.7.0/ZaksPhysicsLibrary.egg-info/requires.txt +6 -0
  13. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/pyproject.toml +6 -3
  14. zaksphysicslibrary-1.4.0/ZaksPhysicsLibrary.egg-info/requires.txt +0 -3
  15. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/LICENSE +0 -0
  16. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/dataset.py +0 -0
  17. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/file_parser.py +0 -0
  18. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/file_parser_generic.py +0 -0
  19. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/__init__.py +0 -0
  20. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/pt2_loader.py +0 -0
  21. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/loaders/tdt_loader.py +0 -0
  22. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/PhysicsLibrary/models.py +0 -0
  23. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/ZaksPhysicsLibrary.egg-info/dependency_links.txt +0 -0
  24. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/ZaksPhysicsLibrary.egg-info/top_level.txt +0 -0
  25. {zaksphysicslibrary-1.4.0 → zaksphysicslibrary-1.7.0}/setup.cfg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ZaksPhysicsLibrary
3
- Version: 1.4.0
4
- Summary: Data processing and analysis library for TDT, Oxysoft NIRS, and Terranova EFNMR lab data
3
+ Version: 1.7.0
4
+ Summary: Data processing and analysis library for TDT, Oxysoft NIRS, Terranova EFNMR lab data, and text-field studies
5
5
  Author: zakgm2
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/zakgm2/PhysicsLibrary
8
8
  Project-URL: Repository, https://github.com/zakgm2/PhysicsLibrary
9
9
  Project-URL: Changelog, https://github.com/zakgm2/PhysicsLibrary/blob/main/CHANGELOG.md
10
- Keywords: physics,neuroscience,fibre-photometry,NIRS,TDT,EFNMR,signal-processing
10
+ Keywords: physics,neuroscience,fibre-photometry,NIRS,TDT,EFNMR,signal-processing,text-analysis,nlp
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Classifier: Programming Language :: Python :: 3.10
13
13
  Classifier: Programming Language :: Python :: 3.11
@@ -21,6 +21,9 @@ License-File: LICENSE
21
21
  Requires-Dist: numpy
22
22
  Requires-Dist: scipy
23
23
  Requires-Dist: tdt
24
+ Requires-Dist: pandas
25
+ Requires-Dist: sentence-transformers
26
+ Requires-Dist: statsmodels
24
27
  Dynamic: license-file
25
28
 
26
29
  # PhysicsLibrary
@@ -38,6 +41,8 @@ Data processing and analysis library backing [PhysicsAnalysis](https://github.co
38
41
  - Generic **Excel / CSV / TSV / plain text**, with automatic sub-table detection for side-by-side data layouts on one sheet
39
42
  - **Processes signals** — bleach correction, denoising, Z-score PETH slicing, FFT with peak annotation, slope/segment analysis
40
43
  - **Fits curves** — linear, single/double exponential, exponential rise, Gaussian, sinusoidal, and a photon-entanglement visibility model, all via `scipy.optimize.curve_fit`
44
+ - **Analyses text field studies** — one JSON file per subject with several free-text fields; pick any pair of fields to compare directly (e.g. does the answer to one question track another for the same subject). Word counts, data-quality flagging, sentence-transformers embeddings, an optional delta-vector magnitude between two fields, and an optional paired-similarity metric per pair with a permutation test and a word-count confound check. Domain-agnostic — field names and which pairs to compare are supplied by the caller, nothing is hardcoded to one study
45
+ - **Validates the similarity metric statistically** — Benjamini-Hochberg FDR-corrected permutation-test p-values, Cohen's d effect size, a word-count-controlled OLS regression (statsmodels), a bootstrap confidence interval on the mean, and a leave-one-out sensitivity check, one row per field pair, with docstrings explaining what each statistic means
41
46
 
42
47
  ---
43
48
 
@@ -52,6 +57,8 @@ PhysicsLibrary/
52
57
  processing_TDT.py TDT tank reading, bleach correction, denoising, event markers
53
58
  analysis.py PETH/Z-score, FFT, slope segments, curve-fit runner
54
59
  models.py Parametric model functions for curve fitting
60
+ text_field_study.py Grouped-text-field study pipeline (embeddings, delta vector, paired similarity)
61
+ field_study_validation.py Statistical validation for the paired-similarity metric (permutation test, Cohen's d, regression, bootstrap CI, leave-one-out)
55
62
  loaders/
56
63
  tdt_loader.py Wraps processing_TDT into a Dataset
57
64
  oxysoft_loader.py Oxysoft .txt parsing (folder + single-file) into a Dataset
@@ -77,7 +84,8 @@ git+https://github.com/zakgm2/PhysicsLibrary.git
77
84
  ### Requirements
78
85
 
79
86
  - Python 3.10+
80
- - `numpy`, `scipy`, `tdt` (installed automatically)
87
+ - `numpy`, `scipy`, `tdt`, `pandas`, `sentence-transformers`, `statsmodels` (installed automatically)
88
+ - `sentence-transformers` pulls in `torch`/`transformers` as transitive dependencies — a genuinely heavy install (hundreds of MB) if you only need the signal-processing side; only actually loaded when you call `embed_text_fields`/`run_field_study_pipeline`
81
89
  - `openpyxl` — only needed for `.xlsx`/`.xls` files; imported lazily with a clear error if missing when you actually try to load Excel
82
90
 
83
91
  ---
@@ -125,6 +133,34 @@ result = pl.fit_model_to_segment(x_seg, y_seg, pl.single_exponential_model, p0_f
125
133
  result["popt"], result["r2"], result["y_fit"]
126
134
  ```
127
135
 
136
+ ```python
137
+ # Text field study — one JSON file per subject, e.g. P-0001.json. Pick
138
+ # pairs of fields to compare directly; no grouping concept needed.
139
+ fields = pl.peek_fields(folder_path) # see what fields exist before picking pairs
140
+ df = pl.run_field_study_pipeline(
141
+ folder_path,
142
+ text_fields=["q1", "q2", "q3", "q4"],
143
+ delta_pair=("q1", "q2"), # optional: how much did q2 change from q1
144
+ paired_fields=[("q1", "q3", "pair1")], # optional: does q1 track q3
145
+ )
146
+ # df has one row per subject: wordcount_<field>, low_quality_<field>, delta_magnitude,
147
+ # sim_<pair>, pvalue_<pair>, effect_size_<pair>, wc_confound_r_<pair>, ...
148
+ ```
149
+
150
+ ```python
151
+ # Statistical validation of the paired-similarity metric — one row per pair
152
+ summary = pl.run_validation_pipeline(
153
+ folder_path,
154
+ text_fields=["q1", "q3"],
155
+ paired_fields=[("q1", "q3", "pair1")],
156
+ )
157
+ # summary: p_value, p_value_fdr, cohens_d, wc_coef_a/b + wc_pvalue_a/b,
158
+ # regression_r_squared, ci_lower/ci_upper, n_flagged_loo, flagged_participant_ids
159
+ ```
160
+
161
+ See [FIELD_STUDY_METHODOLOGY.md](FIELD_STUDY_METHODOLOGY.md) for why each statistic in the
162
+ validation step is a sound, standard technique — useful if anyone asks.
163
+
128
164
  See [CHANGELOG.md](CHANGELOG.md) for the version history.
129
165
 
130
166
  ---
@@ -135,8 +171,10 @@ Everything importable from `PhysicsLibrary` directly:
135
171
 
136
172
  | Category | Names |
137
173
  |----------|-------|
138
- | Format detection | `choose_file`, `detect_format`, `detect_format_file`, `DataFormat`, `Dataset` |
174
+ | Format detection | `detect_format`, `detect_format_file`, `DataFormat`, `Dataset` |
139
175
  | Loading | `load_dataset`, `load_dataset_file`, `load_any_file`, `load_pt2` |
140
176
  | TDT processing | `process_tdt_folder`, `validate_tdt_folder`, `get_tdt_struct`, `get_plot_data`, `correct_bleaching`, `denoise_signal`, `get_event_markers` |
141
177
  | Analysis | `get_zscore_slice`, `smooth_signal`, `bin_for_heatmap`, `compute_fft_slice`, `annotate_fft_peaks`, `compute_slope_segment`, `fit_model_to_segment` |
142
178
  | Curve fit models | `linear_model`, `single_exponential_model`, `exponential_rise_model`, `double_exponential_model`, `gaussian_model`, `sinusoidal_model`, `visibility_model` |
179
+ | Text field study | `run_field_study_pipeline`, `load_field_study_folder`, `peek_fields`, `flag_low_quality`, `embed_text_fields`, `compute_delta_vector`, `compute_paired_similarity`, `permutation_test_similarity`, `wordcount_confound_check` |
180
+ | Field study validation | `run_validation_pipeline`, `build_validation_summary`, `cohens_d`, `benjamini_hochberg`, `wordcount_controlled_regression`, `bootstrap_mean_ci`, `leave_one_out_sensitivity` |
@@ -45,6 +45,14 @@ from .analysis import (
45
45
  annotate_fft_peaks,
46
46
  compute_slope_segment,
47
47
  fit_model_to_segment,
48
+ compute_event_zscore_peth,
49
+ find_significant_peaks,
50
+ find_peak_near_events,
51
+ )
52
+
53
+ from .splice import (
54
+ splice_keep_inside,
55
+ splice_cut_out,
48
56
  )
49
57
 
50
58
  from .models import (
@@ -55,4 +63,26 @@ from .models import (
55
63
  exponential_rise_model,
56
64
  gaussian_model,
57
65
  sinusoidal_model,
66
+ )
67
+
68
+ from .text_field_study import (
69
+ run_field_study_pipeline,
70
+ load_field_study_folder,
71
+ peek_fields,
72
+ flag_low_quality,
73
+ embed_text_fields,
74
+ compute_delta_vector,
75
+ compute_paired_similarity,
76
+ permutation_test_similarity,
77
+ wordcount_confound_check,
78
+ )
79
+
80
+ from .field_study_validation import (
81
+ run_validation_pipeline,
82
+ build_validation_summary,
83
+ cohens_d,
84
+ benjamini_hochberg,
85
+ wordcount_controlled_regression,
86
+ bootstrap_mean_ci,
87
+ leave_one_out_sensitivity,
58
88
  )
@@ -69,6 +69,199 @@ def get_zscore_slice(time_array, signal, center_t, window=None, pre=None, post=N
69
69
  return seg_x, (seg_y - mu) / std
70
70
 
71
71
 
72
+ def compute_event_zscore_peth(time_array, signal, event_times, pre, post, num_bins=300):
73
+ """
74
+ Z-score and align every occurrence of one event type into a
75
+ trial x time matrix, for a stacked-heatmap + trial-average PETH view
76
+ (GuPPy-style) rather than a single click-triggered PETH.
77
+
78
+ Each trial is z-scored independently against its own pre-event
79
+ baseline (see get_zscore_slice) — that's what makes a trial's
80
+ response comparable regardless of the signal's absolute level at
81
+ that point in the recording. Trials are then resampled onto one
82
+ shared relative-time axis (num_bins points spanning -pre..+post) so
83
+ they can be stacked into a single matrix despite each trial's raw
84
+ segment having a slightly different sample count from indexing
85
+ rounding.
86
+
87
+ Parameters
88
+ ----------
89
+ time_array : array
90
+ signal : array
91
+ event_times : list of float
92
+ Timestamps (same units as time_array) for every occurrence of
93
+ the event being analyzed.
94
+ pre, post : float
95
+ Seconds before/after each event to include.
96
+ num_bins : int
97
+ Number of points each trial is resampled to.
98
+
99
+ Returns
100
+ -------
101
+ dict with:
102
+ time_axis : array, shape (num_bins,) — relative time, -pre..+post
103
+ trial_matrix : array, shape (n_valid_trials, num_bins)
104
+ trial_event_times : list of the event_times that produced a
105
+ usable trial (too-short/edge-of-recording events are skipped)
106
+ mean_trace : array, shape (num_bins,)
107
+ sem_trace : array, shape (num_bins,) — standard error of the mean
108
+ across trials, zero if fewer than 2 trials
109
+ """
110
+ time_axis = np.linspace(-pre, post, num_bins)
111
+ rows = []
112
+ valid_times = []
113
+ for t in event_times:
114
+ seg_x, seg_z = get_zscore_slice(time_array, signal, t, pre=pre, post=post)
115
+ if seg_x is None or len(seg_x) < 2:
116
+ continue
117
+ rel_x = seg_x - t
118
+ rows.append(np.interp(time_axis, rel_x, seg_z))
119
+ valid_times.append(t)
120
+
121
+ if not rows:
122
+ empty = np.zeros(num_bins)
123
+ return {
124
+ "time_axis": time_axis, "trial_matrix": np.zeros((0, num_bins)),
125
+ "trial_event_times": [], "mean_trace": empty, "sem_trace": empty,
126
+ }
127
+
128
+ trial_matrix = np.array(rows)
129
+ mean_trace = trial_matrix.mean(axis=0)
130
+ if trial_matrix.shape[0] > 1:
131
+ sem_trace = trial_matrix.std(axis=0, ddof=1) / np.sqrt(trial_matrix.shape[0])
132
+ else:
133
+ sem_trace = np.zeros(num_bins)
134
+
135
+ return {
136
+ "time_axis": time_axis, "trial_matrix": trial_matrix,
137
+ "trial_event_times": valid_times, "mean_trace": mean_trace, "sem_trace": sem_trace,
138
+ }
139
+
140
+
141
+ def find_significant_peaks(time_array, signal, z_threshold=2.5, min_distance_sec=1.0,
142
+ include_troughs=False):
143
+ """
144
+ Auto-detect statistically significant transients directly from the
145
+ signal, rather than relying on externally-supplied event markers
146
+ (TDT epocs, manual markers, ...) that may not actually line up with
147
+ where the neural signal itself is doing something.
148
+
149
+ The whole recording is z-scored against its own global mean/std
150
+ (not a local baseline — this is a single-pass "how unusual is this
151
+ point relative to the entire recording" measure, not per-event), and
152
+ scipy.signal.find_peaks picks local maxima at or above z_threshold,
153
+ at least min_distance_sec apart so a single transient's rising edge
154
+ doesn't get counted as several peaks.
155
+
156
+ Parameters
157
+ ----------
158
+ time_array : array
159
+ signal : array
160
+ Already-processed signal (e.g. bleach-corrected + smoothed) —
161
+ this function does no filtering of its own.
162
+ z_threshold : float
163
+ Minimum z-score (standard deviations above the recording's own
164
+ mean) for a peak to count as "statistically significant".
165
+ min_distance_sec : float
166
+ Minimum spacing between detected peaks, in seconds.
167
+ include_troughs : bool
168
+ Also detect significant negative-going deflections (z <=
169
+ -z_threshold) — off by default since most fibre-photometry
170
+ analyses care about excitatory transients specifically.
171
+
172
+ Returns
173
+ -------
174
+ list of dict, each {"time": float, "z_score": float, "kind": "peak"|"trough"},
175
+ sorted by time.
176
+ """
177
+ fs = 1.0 / np.median(np.diff(time_array))
178
+ distance = max(1, int(min_distance_sec * fs))
179
+
180
+ mu, std = np.mean(signal), np.std(signal)
181
+ if std < 1e-9:
182
+ return []
183
+ z = (signal - mu) / std
184
+
185
+ results = []
186
+ peak_idx, _ = find_peaks(z, height=z_threshold, distance=distance)
187
+ for i in peak_idx:
188
+ results.append({"time": float(time_array[i]), "z_score": float(z[i]), "kind": "peak"})
189
+
190
+ if include_troughs:
191
+ trough_idx, _ = find_peaks(-z, height=z_threshold, distance=distance)
192
+ for i in trough_idx:
193
+ results.append({"time": float(time_array[i]), "z_score": float(z[i]), "kind": "trough"})
194
+
195
+ results.sort(key=lambda r: r["time"])
196
+ return results
197
+
198
+
199
+ def find_peak_near_events(time_array, signal, event_times, pre, post,
200
+ z_threshold=2.5, include_troughs=False):
201
+ """
202
+ Check whether a statistically significant peak actually shows up near
203
+ each given event time, rather than assuming the event marker itself
204
+ marks where the neural signal responds. Works for a single event
205
+ (event_times of length 1) or many occurrences of the same event type
206
+ (checking consistency across all of them).
207
+
208
+ Each event's window is baselined the same way as get_zscore_slice
209
+ (pre-event portion), so "significant" means relative to that event's
210
+ own local baseline, not the whole recording's.
211
+
212
+ Parameters
213
+ ----------
214
+ time_array : array
215
+ signal : array
216
+ event_times : list of float
217
+ pre, post : float
218
+ Seconds before/after each event to search within.
219
+ z_threshold : float
220
+ Minimum |z-score| within the window for a peak to count as found.
221
+ include_troughs : bool
222
+ Also consider negative-going deflections as candidate "peaks",
223
+ keeping whichever (peak or trough) is more extreme.
224
+
225
+ Returns
226
+ -------
227
+ list of dict, one per event_time (same order), each:
228
+ {"event_time": float, "found": bool, "peak_time": float or None,
229
+ "latency": float or None (peak_time - event_time),
230
+ "z_score": float or None, "kind": "peak"|"trough"|None}
231
+ "found" is False when the window was unusable (too close to the
232
+ recording's edges) or nothing in it reached z_threshold.
233
+ """
234
+ results = []
235
+ for t in event_times:
236
+ seg_x, seg_z = get_zscore_slice(time_array, signal, t, pre=pre, post=post)
237
+ if seg_x is None or len(seg_x) == 0:
238
+ results.append({"event_time": t, "found": False, "peak_time": None,
239
+ "latency": None, "z_score": None, "kind": None})
240
+ continue
241
+
242
+ idx_max = int(np.argmax(seg_z))
243
+ if include_troughs:
244
+ idx_min = int(np.argmin(seg_z))
245
+ if abs(seg_z[idx_min]) > seg_z[idx_max]:
246
+ best_idx, kind = idx_min, "trough"
247
+ else:
248
+ best_idx, kind = idx_max, "peak"
249
+ else:
250
+ best_idx, kind = idx_max, "peak"
251
+
252
+ best_z = float(seg_z[best_idx])
253
+ found = abs(best_z) >= z_threshold
254
+ peak_time = float(seg_x[best_idx]) if found else None
255
+ results.append({
256
+ "event_time": t, "found": found,
257
+ "peak_time": peak_time,
258
+ "latency": (peak_time - t) if found else None,
259
+ "z_score": best_z if found else None,
260
+ "kind": kind if found else None,
261
+ })
262
+ return results
263
+
264
+
72
265
  def smooth_signal(data, fs, window_sec=0.5):
73
266
  """
74
267
  Moving average smoothing filter.