Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions nstat/confidence_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
import numpy as np


MATLAB_COLOR_ORDER = np.asarray(
[
[0.0660, 0.4430, 0.7450],
[0.8660, 0.3290, 0.0000],
[0.9290, 0.6940, 0.1250],
[0.4940, 0.1840, 0.5560],
[0.4660, 0.6740, 0.1880],
[0.3010, 0.7450, 0.9330],
[0.6350, 0.0780, 0.1840],
],
dtype=float,
)


class ConfidenceInterval:
def __init__(self, time, bounds, *args, color: str | None = None, value: float = 0.95) -> None:
t = np.asarray(time, dtype=float).reshape(-1)
Expand Down Expand Up @@ -176,12 +190,14 @@ def plot(self, color: str | None = None, alphaVal: float = 0.2, drawPatches: int
import matplotlib.pyplot as plt

axis = plt.gca() if ax is None else ax
plot_color = color or self.color
plot_color = self.color if color is None else color
if drawPatches:
return axis.fill_between(self.time, self.lower, self.upper, color=plot_color, alpha=alphaVal)
return axis.fill_between(self.time, self.lower, self.upper, color=plot_color, edgecolor="none", alpha=alphaVal)
lines = axis.plot(self.time, self.bounds)
for line in lines:
line.set_alpha(alphaVal)
if plot_color is not None and not isinstance(plot_color, (str, bytes)):
line.set_color(plot_color)
if plot_color is None or isinstance(plot_color, (str, bytes)):
for index, line in enumerate(lines):
line.set_color(MATLAB_COLOR_ORDER[index % MATLAB_COLOR_ORDER.shape[0]])
return lines
8 changes: 7 additions & 1 deletion nstat/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -738,11 +738,17 @@ def resampleMe(self, newSampleRate: float) -> None:
newTime = np.arange(self.time[0], self.time[-1] + 0.5 * dt, dt, dtype=float)
if self.data.shape[0] > 1:
columns = []
if self.time.size >= 4:
interp_kind = "cubic"
elif self.time.size == 3:
interp_kind = "quadratic"
else:
interp_kind = "linear"
for index in range(self.dimension):
interpolator = interp1d(
self.time,
self.data[:, index],
kind="cubic",
kind=interp_kind,
bounds_error=False,
fill_value=0.0,
)
Expand Down
44 changes: 32 additions & 12 deletions nstat/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,38 @@ def fromStructure(structure: dict[str, Any] | None) -> "Events" | None:
return Events(event_times, event_labels, event_color)

def plot(self, *_, handle=None, **__):
ax = handle if handle is not None else plt.subplots(1, 1, figsize=(6.0, 2.2))[1]
ax.clear()
if self.eventTimes.size:
ax.vlines(self.eventTimes, 0.0, 1.0, color=self.eventColor, linewidth=1.5)
for x, label in zip(self.eventTimes, self.eventLabels, strict=False):
if label:
ax.text(float(x), 1.02, label, rotation=45, ha="left", va="bottom", fontsize=8)
ax.set_ylim(0.0, 1.1)
ax.set_xlabel("time [s]")
ax.set_yticks([])
ax.set_title("Events")
return ax
if handle is None:
handles = [plt.gca()]
elif isinstance(handle, Sequence) and not hasattr(handle, "plot"):
handles = list(handle)
else:
handles = [handle]

last_ax = None
for ax in handles:
last_ax = ax
v = ax.axis()
if self.eventTimes.size:
times = np.vstack([self.eventTimes, self.eventTimes])
y = np.vstack(
[
np.full(self.eventTimes.shape, float(v[2]), dtype=float),
np.full(self.eventTimes.shape, float(v[3]), dtype=float),
]
)
ax.plot(times, y, "r", linewidth=4)
for event_time, label in zip(self.eventTimes, self.eventLabels, strict=False):
if label and ((float(event_time) - float(v[0])) / max(float(v[1] - v[0]), 1e-12) >= 0) and float(event_time) <= float(v[1]):
ax.text(
(float(event_time) - float(v[0])) / max(float(v[1] - v[0]), 1e-12) - 0.02,
1.03,
label,
rotation=0,
fontsize=10,
color=[0, 0, 0],
transform=ax.transAxes,
)
return last_ax


__all__ = ["Events"]
1 change: 0 additions & 1 deletion nstat/fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,6 @@ def mapCovLabelsToUniqueLabels(self):
self.indicesToUniqueLabels.append(indices)
if indices:
self.flatMask[np.asarray(indices, dtype=int) - 1, fit_idx] = 1
self.computePlotParams()
return self

def getSubsetFitResult(self, subfits) -> "FitResult":
Expand Down
22 changes: 13 additions & 9 deletions nstat/trial.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ def _selector_cell_from_names(self, dataSelector: Sequence[Any]) -> list[list[in
covIndex = self.getCovIndFromName(covName)
currCov = self.getCov(covIndex)
if len(dataSelector) == 1:
selectorCell[covIndex - 1] = list(range(1, currCov.dimension + 1))
selectorCell[covIndex - 1] = currCov.getIndicesFromLabels([])
else:
selectorCell[covIndex - 1] = currCov.getIndicesFromLabels([str(v) for v in dataSelector[1:]])
return selectorCell
Expand All @@ -280,7 +280,7 @@ def _selector_cell_from_names(self, dataSelector: Sequence[Any]) -> list[list[in
covIndex = self.getCovIndFromName(covName)
currCov = self.getCov(covIndex)
if len(parsed) == 1:
selectorCell[covIndex - 1] = list(range(1, currCov.dimension + 1))
selectorCell[covIndex - 1] = currCov.getIndicesFromLabels([])
else:
selectorCell[covIndex - 1] = currCov.getIndicesFromLabels([str(v) for v in parsed[1:]])
return selectorCell
Expand Down Expand Up @@ -938,8 +938,9 @@ def __init__(self, configs: Sequence[TrialConfig] | TrialConfig | str | None = N
self.numConfigs = 0
self.configNames: list[str] = []
self.configArray: list[TrialConfig | str | list[str]] = []
if configs is not None:
self.addConfig(configs)
# MATLAB ConfigColl() routes through addConfig([]), which creates
# a single "Empty Config" entry by default.
self.addConfig([] if configs is None else configs)

@property
def configs(self) -> list[TrialConfig]:
Expand All @@ -950,6 +951,11 @@ def add_config(self, cfg: TrialConfig) -> None:

def addConfig(self, cfg: Sequence[TrialConfig] | TrialConfig | str | None) -> None:
if isinstance(cfg, Sequence) and not isinstance(cfg, (str, bytes, TrialConfig, np.ndarray)):
if len(cfg) == 0:
self.numConfigs += 1
self.configNames.append("Empty Config")
self.configArray.append(["Empty Config"])
return
for item in cfg:
self.addConfig(item)
return
Expand All @@ -964,10 +970,8 @@ def addConfig(self, cfg: Sequence[TrialConfig] | TrialConfig | str | None) -> No
self.setConfigNames(cfg.name, [self.numConfigs])
return
if isinstance(cfg, str):
self.numConfigs += 1
self.configArray.append(cfg)
self.setConfigNames(cfg, [self.numConfigs])
return
# MATLAB's string branch dereferences tcObj.name and errors.
getattr(cfg, "name")
raise TypeError("ConfigColl can only add TrialConfig objects, strings, or sequences of them.")

def get_config(self, idx: int) -> TrialConfig | str | list[str]:
Expand Down Expand Up @@ -1007,7 +1011,7 @@ def setConfigNames(self, names, index: Sequence[int] | None = None) -> None:
target = int(index[0]) - 1
while len(self.configNames) < self.numConfigs:
self.configNames.append("")
self.configNames[target] = names if names else f"Fit {target + 1}"
self.configNames[target] = names if names else f"Fit {self.numConfigs}"
return
if isinstance(names, Sequence) and not isinstance(names, (str, bytes)):
if len(index) != len(names):
Expand Down
84 changes: 39 additions & 45 deletions parity/class_fidelity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,57 +186,51 @@ items:
matlab_path: TrialConfig.m
python_public_name: nstat.TrialConfig
python_impl_path: nstat/trial.py
status: high_fidelity
status: exact
constructor_parity: The constructor now matches MATLAB intent much more closely,
including covMask, sampleRate, history, ensCovHist, ensCovMask, covLag, and name
handling.
property_parity: Core configuration fields and normalized metadata are now exposed
in the canonical implementation rather than a dataclass shim.
method_parity: MATLAB-facing methods now include naming, fixture-backed structure
round-trip, and setConfig application against Trial state, including the legacy
MATLAB fromStructure argument-shift quirk.
defaults_parity: Defaults for empty masks/configs and name handling are close to
MATLAB.
round-trip, and fixture-backed setConfig application against Trial state, including
the legacy MATLAB fromStructure argument-shift quirk and the empty-label selector
semantics MATLAB actually applies through CovColl.
defaults_parity: Defaults for empty masks/configs and name handling are fixture-backed
against MATLAB behavior.
indexing_parity: N/A for this class.
error_warning_parity: Validation is still lighter than MATLAB in some malformed-configuration
paths.
error_warning_parity: MATLAB's minimal constructor/fromStructure validation behavior
is matched for the implemented public surface.
output_type_parity: Returns and mutates canonical TrialConfig/Trial objects as expected.
symbol_presence_verified: yes
known_remaining_differences:
- Some malformed-configuration normalization and validation branches remain looser
in Python.
required_remediation:
- Add malformed-config fixtures from MATLAB before promoting TrialConfig from fixture-backed
high_fidelity to exact.
known_remaining_differences: []
required_remediation: []
plotting_report_parity: N/A
- matlab_name: ConfigColl
kind: class
matlab_path: ConfigColl.m
python_public_name: nstat.ConfigColl
python_impl_path: nstat/trial.py
status: high_fidelity
status: exact
constructor_parity: Fixture-backed canonical behavior now matches MATLAB for collections
of TrialConfig objects, while Python still preserves extra string/empty convenience
paths beyond the MATLAB round-trip surface.
of TrialConfig objects, the default empty-config constructor, and the legacy
string-constructor failure branch.
property_parity: numConfigs, configNames, and configArray are exposed with MATLAB-style
semantics.
method_parity: addConfig, getConfig, setConfig, getConfigNames, setConfigNames,
getSubsetConfigs, and the TrialConfig-only structure round-trip now follow the
MATLAB collection behavior, including rebuilt Fit N names after the legacy TrialConfig
round-trip bug.
defaults_parity: Empty-config and naming defaults now align closely with MATLAB
round-trip bug and MATLAB's Fit numConfigs empty-name quirk.
defaults_parity: Empty-config and naming defaults are fixture-backed against MATLAB
behavior.
indexing_parity: One-based getConfig behavior is preserved.
error_warning_parity: Basic validation exists, though some MATLAB collection-coercion
edge cases are still looser.
error_warning_parity: Constructor and setConfig error behavior are fixture-backed
against the implemented MATLAB surface, including the legacy string-constructor
failure.
output_type_parity: Returns TrialConfig instances.
symbol_presence_verified: yes
known_remaining_differences:
- Python still preserves extra string/empty convenience branches that are not part
of the canonical MATLAB TrialConfig collection round-trip.
required_remediation:
- Decide whether to retain or remove the extra Python convenience branches before
labeling ConfigColl exact.
known_remaining_differences: []
required_remediation: []
plotting_report_parity: N/A
- matlab_name: Analysis
kind: class
Expand Down Expand Up @@ -463,26 +457,24 @@ items:
matlab_path: Events.m
python_public_name: nstat.Events
python_impl_path: nstat/events.py
status: high_fidelity
status: exact
constructor_parity: Constructor now tracks MATLAB eventTimes, eventLabels, and eventColor
semantics, including label-count validation.
property_parity: eventTimes, eventLabels, and eventColor are canonical public fields,
with legacy Python aliases preserved.
method_parity: Structure round-trip and notebook/workflow-facing access patterns
are implemented.
defaults_parity: Empty-label and default-color behavior are close to MATLAB for
the implemented workflow subset.
method_parity: Structure round-trip, MATLAB-style plotting, and notebook/workflow-facing
access patterns are fixture-backed against MATLAB.
defaults_parity: Empty-label and default-color behavior are fixture-backed against
MATLAB for the implemented public surface.
indexing_parity: Event vectors are stored in MATLAB-style flat time/label arrays.
error_warning_parity: Core validation now matches MATLAB intent, though plotting-related
behaviors remain absent.
error_warning_parity: Core validation now matches MATLAB intent for the implemented
public surface.
output_type_parity: Returns canonical Events objects.
symbol_presence_verified: yes
known_remaining_differences:
- Plotting and some MATLAB-specific display behaviors are still unported.
required_remediation:
- Add notebook-backed fixtures for event serialization and display workflows.
plotting_report_parity: Event plotting/display behavior is still limited compared
with MATLAB.
known_remaining_differences: []
required_remediation: []
plotting_report_parity: Event plotting is fixture-backed for MATLAB's red-line
display semantics, line width, normalized label placement, and structure round-trip.
- matlab_name: ConfidenceInterval
kind: class
matlab_path: ConfidenceInterval.m
Expand All @@ -506,13 +498,15 @@ items:
the expected workflow positions.
symbol_presence_verified: yes
known_remaining_differences:
- Full MATLAB display/plot styling semantics are still lighter than the original
toolbox.
- The subclass-specific constructor/plot/round-trip surface is now fixture-backed,
but ConfidenceInterval still inherits the remaining non-exact SignalObj display/report
helpers.
required_remediation:
- Add MATLAB-derived fixtures for exact plot styling before promoting ConfidenceInterval
from fixture-backed high_fidelity to exact.
plotting_report_parity: Core CI plotting works, including MATLAB's string-color
quirk in line mode; full display/styling parity remains lighter.
- Promote the remaining SignalObj helper/report surface from high_fidelity to
exact before re-evaluating ConfidenceInterval as exact.
plotting_report_parity: Core CI plotting now matches MATLAB's string-color line
behavior and patch face/edge/alpha semantics for the implemented surface; inherited
SignalObj display/report differences remain.
- matlab_name: CovColl
kind: class
matlab_path: CovColl.m
Expand Down
3 changes: 2 additions & 1 deletion parity/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,8 @@ repo_structure:
or repo-root package stub.
fidelity_summary:
class_fidelity:
high_fidelity: 18
exact: 3
high_fidelity: 15
not_applicable: 1
notebook_fidelity:
high_fidelity: 13
Expand Down
4 changes: 2 additions & 2 deletions parity/report.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Generated from `parity/manifest.yml`, `parity/class_fidelity.yml`, `tools/notebo

| Status | Count |
|---|---:|
| `exact` | 0 |
| `high_fidelity` | 18 |
| `exact` | 3 |
| `high_fidelity` | 15 |
| `partial` | 0 |
| `wrapper_only` | 0 |
| `missing` | 0 |
Expand Down
Binary file modified tests/parity/fixtures/matlab_gold/analysis_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/cif_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/confidence_interval_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/config_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/covariate_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/decoding_predict_exactness.mat
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/fit_summary_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/hybrid_filter_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/ksdiscrete_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/nonlinear_decode_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/nspiketrain_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/nstcoll_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/point_process_exactness.mat
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/signalobj_exactness.mat
Binary file not shown.
Binary file not shown.
Binary file modified tests/parity/fixtures/matlab_gold/thinning_exactness.mat
Binary file not shown.
2 changes: 1 addition & 1 deletion tests/test_analysis_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_trial_analysis_pipeline() -> None:
spikes = model.simulate(num_realizations=3, seed=2)

trial = Trial(spike_collection=spikes, covariate_collection=CovariateCollection([cov]))
cfgs = ConfigCollection([TrialConfig(covMask=["stim"], sampleRate=1000.0, name="stim_model")])
cfgs = ConfigCollection([TrialConfig(covMask=[["stim", "stim"]], sampleRate=1000.0, name="stim_model")])

fits = Analysis.run_analysis_for_all_neurons(trial, cfgs)
assert len(fits) == 3
Expand Down
2 changes: 1 addition & 1 deletion tests/test_fitresult_diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def _build_fit_result():
spikes = model.simulate(num_realizations=2, seed=7)

trial = Trial(spike_collection=spikes, covariate_collection=CovariateCollection([cov]))
cfgs = ConfigCollection([TrialConfig(covMask=["stim"], sampleRate=1000.0, name="stim_model")])
cfgs = ConfigCollection([TrialConfig(covMask=[["stim", "stim"]], sampleRate=1000.0, name="stim_model")])
return Analysis.run_analysis_for_all_neurons(trial, cfgs)[0]


Expand Down
Loading