From 34bfef6fb5b8bfc7025bff2f38965dc95d695db4 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 15:14:23 +0100 Subject: [PATCH 01/19] Added documentating. Added missing docstrings. Used single and double quotes consistently. Added developer info to README.md --- README.md | 28 ++++ docs/.gitignore | 1 + docs/Makefile | 20 +++ docs/make.bat | 36 ++++++ docs/source/_static/bg.png | Bin 0 -> 11174 bytes docs/source/conf.py | 163 ++++++++++++++++++++++++ docs/source/index.rst | 15 +++ docs/source/trace.rst | 10 ++ docs/source/voltage_protocols.rst | 11 ++ setup.py | 16 ++- syncropatch_export/__init__.py | 5 + syncropatch_export/_version.py | 4 + syncropatch_export/trace.py | 87 +++++++------ syncropatch_export/voltage_protocols.py | 77 ++++++++--- 14 files changed, 407 insertions(+), 66 deletions(-) create mode 100644 docs/.gitignore create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/_static/bg.png create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/trace.rst create mode 100644 docs/source/voltage_protocols.rst create mode 100644 syncropatch_export/_version.py diff --git a/README.md b/README.md index 3d61d4f..ce1208d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![codecov](https://codecov.io/gh/CardiacModelling/syncropatch_export/graph/badge.svg?token=HOL0FrpGqs)](https://codecov.io/gh/CardiacModelling/syncropatch_export) This repository contains a python package and scripts for processing data outputted from Nanion SynroPatch 384. + With this package you can export each sweep of each protocol for each well as individual files (.csv). Meta-data describing the protocol, and variables such as membrance capacitance (Cm), Rseries and Rseal can be exported. @@ -41,3 +42,30 @@ Then you can run the tests. ``` python3 -m unittest ``` + +## Usage example + +...TODO + + +## Development + +Commits should be merged in via pull requests. + +Tests are written using the standard [unittest](https://docs.python.org/3.13/library/unittest.html) framework. + +Online testing, style-checking, and coverage testing is set up using GitHub actions. +Coverage testing is handled via [Codecov](https://about.codecov.io/). + +Documentation is implemented using [Sphinx](https://www.sphinx-doc.org/). +To compile locally, first install the required dependencies +``` +pip install -e .'[docs]' +``` +and then use Make +``` +cd docs +make html +``` + + diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..a007fea --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +build/* diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..ad732be --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = SyncropatchExport +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..16faa53 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=Myokit + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/source/_static/bg.png b/docs/source/_static/bg.png new file mode 100644 index 0000000000000000000000000000000000000000..96009e79a27bc3cd666481b1bbca37334b4bc7ef GIT binary patch literal 11174 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 Ef*T``w*UYD literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..c5c072f --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import syncropatch_export + + +# -- General configuration ---------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.mathjax', + 'sphinx.ext.napoleon', +] + +# Autodoc defaults +autodoc_default_options = { + 'members': None, + #'inherited-members': None, +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Syncropatch export' +#copyright = datkit.COPYRIGHT + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = syncropatch_export.__version__ +# The full version, including alpha/beta/rc tags. +release = syncropatch_export.__version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'default' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['_templates'] + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = False + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'SyncropatchExpertDoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass[howto/manual]). +latex_documents = [( + 'index', 'syncropatch_export.tex', u'Syncropatch Export Documentation', + u'Mixed', 'manual' +)] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [( + 'index', 'syncropatch_export', u'Syncropatch Export Documentation', + [u'Syncropatch Export Team'], 1 +)] + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'syncropatch_export', u'Syncropatch Export Documentation', + 'Syncropatch Export Team', 'Syncropatch Export', + 'Exports Nanion Syncropatch data to CSV.', + 'Miscellaneous'), +] + diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..f673e5c --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,15 @@ +***************** +Table of contents +***************** + +.. module:: syncropatch_export + +This module contains methods to export data from the Nanion SynroPatch 384. + +.. toctree:: + :maxdepth: 2 + + self + trace + voltage_protocols + diff --git a/docs/source/trace.rst b/docs/source/trace.rst new file mode 100644 index 0000000..083b087 --- /dev/null +++ b/docs/source/trace.rst @@ -0,0 +1,10 @@ +.. currentmodule:: syncropatch_export.trace + +************** +Accessing data +************** + +Data is accessed and exported via the :class:`Trace` class. + +.. autoclass:: Trace + diff --git a/docs/source/voltage_protocols.rst b/docs/source/voltage_protocols.rst new file mode 100644 index 0000000..0bab766 --- /dev/null +++ b/docs/source/voltage_protocols.rst @@ -0,0 +1,11 @@ +.. currentmodule:: syncropatch_export.voltage_protocols + +*************************** +Accessing voltage protocols +*************************** + +Voltage protocols can be parsed from the JSON data using the +:class:`VoltageProtocol` class. + +.. autoclass:: VoltageProtocol + diff --git a/setup.py b/setup.py index 1d1727b..bdb0b74 100644 --- a/setup.py +++ b/setup.py @@ -4,14 +4,17 @@ with open('README.md') as f: readme = f.read() -# Load version number -# with open('version.txt', 'r') as f: -# version = f.read() -# Go! +# Load version number +import os +import sys +sys.path.append(os.path.abspath('syncropatch_export')) +from _version import __version__ as version # noqa +sys.path.pop() +del os, sys -version = '0.0.1' +# Go! setup( # Module name (lowercase) name='syncropatch_export', @@ -57,5 +60,8 @@ 'mock>=3.0.5', # For mocking command line args etc. 'codecov>=2.1.3', ], + 'docs': [ + 'sphinx>=1.7.4', + ], }, ) diff --git a/syncropatch_export/__init__.py b/syncropatch_export/__init__.py index e69de29..5519020 100644 --- a/syncropatch_export/__init__.py +++ b/syncropatch_export/__init__.py @@ -0,0 +1,5 @@ +# +# Syncropatch, main module +# +from ._version import __version__ + diff --git a/syncropatch_export/_version.py b/syncropatch_export/_version.py new file mode 100644 index 0000000..106c18b --- /dev/null +++ b/syncropatch_export/_version.py @@ -0,0 +1,4 @@ +# +# Syncropatch_export version number +# +__version__ = '0.0.1' diff --git a/syncropatch_export/trace.py b/syncropatch_export/trace.py index 380c8e9..936ee0a 100644 --- a/syncropatch_export/trace.py +++ b/syncropatch_export/trace.py @@ -9,11 +9,14 @@ class Trace: - """ Defines a Trace object from the output of a Nanion experiment. + """ + Defines a Trace object from the output of a Nanion experiment. - @params - filepath: path pointing to folder containing .json and .dat files (str) - json_file: specific filename of json file (str) + Args: + filepath (str): A path pointing to folder containing both ``.json`` and + ``.dat`` files. + json_file (str): The name of a JSON file within ``path``, from which + meta data will be read. """ def __init__(self, filepath, json_file: str): @@ -22,7 +25,7 @@ def __init__(self, filepath, json_file: str): if json_file[-5:] == '.json': self.json_file = json_file else: - self.json_file = json_file + ".json" + self.json_file = json_file + '.json' # load json file with open(os.path.join(self.filepath, self.json_file)) as f: @@ -59,64 +62,65 @@ def __init__(self, filepath, json_file: str): self.voltage_protocol = self.get_voltage_protocol() - def get_voltage_protocol(self, holding_potential=-80.0): - """Extract information about the voltage protocol from the json file - - returns: a VoltageProtocol object - + def get_voltage_protocol(self): """ + Extract information about the voltage protocol from the JSON file. - voltage_protocol = VoltageProtocol.from_json( + Returns: + VoltageProtocol: A voltage protocol object. + """ + return VoltageProtocol.from_json( self.meta['ExperimentConditions']['VoltageProtocol'], self.meta['ExperimentConditions']['VMembrane_mV'] ) - return voltage_protocol - def get_voltage_protocol_json(self): """ - Returns the voltage protocol as a JSON object + Returns unparsed JSON object representing the voltage protocol. """ + #TODO Why only the first row? return self.meta['ExperimentConditions']['VoltageProtocol'][0] def get_protocol_description(self, holding_potential=-80.0): - """Get the protocol as a numpy array describing the voltages and - durations for each section + """ + Returns the protocol as an ``np.numpy`` with an entry for each segment. - returns: np.array where each row contains the start time, end time, - initial voltage, and final voltage + Returns: + np.array: An array where each row contains the start time, + end time, initial voltage, and final voltage of a ramp or step + segment. """ return self.get_voltage_protocol().get_all_sections() def get_voltage(self): - ''' + """ Returns the voltage stimulus from Nanion .json file - ''' - return np.array(self.TimeScaling['Stimulus']).astype(np.float64)\ - * 1e3 + """ + return np.array(self.TimeScaling['Stimulus']).astype(np.float64) * 1e3 def get_times(self): - ''' + """ Returns the time steps from Nanion .json file - ''' + """ return np.array(self.TimeScaling['TR_Time']) * 1e3 def get_all_traces(self, leakcorrect=False): - ''' + """ - Params: - leakcorrect: Bool. Set to true if using onboard leak correction + Args: + leakcorrect (bool): Set to true if using onboard leak correction - Returns: all raw current traces from .dat files + Returns: + All raw current traces from .dat files - ''' + """ return self.get_trace_sweeps(leakcorrect=leakcorrect) def get_trace_file(self, sweeps): - ''' + """ Returns the trace file index of the file for a given set of sweeps - ''' + """ OUT_file_idx = [] OUT_idx_i = [] for actSweep in sweeps: @@ -133,9 +137,9 @@ def get_trace_file(self, sweeps): return OUT_file_idx, OUT_idx_i def get_trace_sweeps(self, sweeps=None, leakcorrect=False): - ''' - Returns a subset of sweeps defined by the input 'sweeps' - ''' + """ + Returns a subset of sweeps defined by the input ``sweeps``. + """ # initialise output out_dict = {} @@ -221,13 +225,13 @@ def get_trace_sweeps(self, sweeps=None, leakcorrect=False): return out_dict def get_onboard_QC_values(self, sweeps=None): - '''Read quality control values Rseal, Cslow (Cm), and Rseries from a Nanion .json file + """ + Return the quality control values Rseal, Cslow (Cm), and Rseries. returns: A dictionary where the keys are the well e.g. 'A01' and the values are the values used for onboard QC i.e., the seal resistance, cell capacitance and the series resistance. - - ''' + """ # load QC values RSeal = np.array(self.meta['QCData']['RSeal']) @@ -262,11 +266,13 @@ def get_onboard_QC_values(self, sweeps=None): return out_dict def get_onboard_QC_df(self, sweeps=None): - """Create a Pandas DataFrame which lists the Rseries, memebrane + """ + Create a Pandas DataFrame which lists the Rseries, memebrane capacitance and Rseries for each well and sweep. - @Returns A pandas.DataFrame describing the onboard QC estimates for - each well, sweep + Returns: + pandas.DataFrame: A data frame describing the onboard QC estimates + for each well, sweep """ @@ -288,3 +294,4 @@ def get_onboard_QC_df(self, sweeps=None): df_rows.append(df_row) return pd.DataFrame.from_records(df_rows) + diff --git a/syncropatch_export/voltage_protocols.py b/syncropatch_export/voltage_protocols.py index bdb0265..b2b9e52 100644 --- a/syncropatch_export/voltage_protocols.py +++ b/syncropatch_export/voltage_protocols.py @@ -1,26 +1,46 @@ import numpy as np -class VoltageProtocol(): - def from_json(json_protocol, holding_potential): - """ Converts a protocol (from the json file) into a np.array +class VoltageProtocol: + """ + Represent a voltage step (and ramp) protocol. + Each protocol is represented as + + 1. A list of segment starts, ends, initial voltages, and final voltages + 2. A holding potential + + To create a :class:`VoltageProtocol`, use either + :meth:`VoltageProtocol.from_json` or + `meth:`VoltageProtocol.from_voltage_trace`. + """ + + @classmethod + def from_json(cls, json_protocol, holding_potential): """ + Reads a protocol from a JSON file. + Args: + json_protocol (list): A list or other sequence containing the + ``VoltageProtocol`` section from the JSON file. + holding_potential (float): The holding potential + + """ output_sections = [] for section in json_protocol: tstart = float(section['SegmentStart_ms']) tdur = float(section['Duration ms']) vstart = float(section['VoltageStart']) vend = float(section['VoltageEnd']) + output_sections.append((tstart, tstart + tdur, vstart, vend)) + return cls(np.array(output_sections), holding_potential) - output_sections.append(np.array((tstart, tstart + tdur, - vstart, vend))) - - return VoltageProtocol(np.array(output_sections), - holding_potential=holding_potential) - - def from_voltage_trace(voltage_trace, times, holding_potential=-80.0): + @classmethod + def from_voltage_trace(cls, voltage_trace, times, holding_potential=-80.0): + """ + Creates an approximate voltage protocol from a time series ``(times, + voltage_trace)``. + """ threshold = 1e-3 # Find gradient changes @@ -28,11 +48,11 @@ def from_voltage_trace(voltage_trace, times, holding_potential=-80.0): windows = np.argwhere(diff2 > threshold).flatten() window_locs = np.unique(windows) - window_locs = np.array([val for val in window_locs if val + 1 - not in window_locs]) + 1 + window_locs = 1 + np.array([ + val for val in window_locs if val + 1 not in window_locs]) - windows = zip([0] + list(window_locs), list(window_locs) - + [len(voltage_trace) - 1]) + windows = zip([0] + list(window_locs), + list(window_locs) + [len(voltage_trace) - 1]) lst = [] for start, end in windows: @@ -53,31 +73,46 @@ def from_voltage_trace(voltage_trace, times, holding_potential=-80.0): lst.append(np.array([start_t, end_t, v_start, v_end])) desc = np.vstack(lst) - return VoltageProtocol(desc, holding_potential) + return cls(desc, holding_potential) def __init__(self, desc, holding_potential): self._desc = desc self.holding_potential = holding_potential def get_holding_potential(self): + """ Returns this protocol's holding potential. """ return self.holding_potential def get_step_start_times(self): + """ Returns a list of all segment start times. """ return [line[0] for line in self._desc] def get_ramps(self): + """ + Returns all segments that are ramps. + + Each segment is represented as ``(start time, end time, start voltage, + end voltage)``. + """ return [line for line in self._desc if line[2] != line[3]] def get_all_sections(self): - """ Return a np.array describing the protocol. - - returns: an np.array where the ith row is the start-time, - end-time, start-voltage and end-voltage for the ith section of the protocol - + """ + Return an ``np.array`` describing the protocol, where each row in the + array contains the the start time, end time, initial voltage and final + voltage for a segment of the protocol. """ return np.array(self._desc) def export_txt(self, fname): + """ + Writes a partial textual representation of this protocol to a file. + + The created file will have a header line, followed by one line per + segment. Segments are represented as "Type" (Set or Ramp), "Voltage" + (the final voltage of a segment), and "Duration". + """ + output_lines = ['Type \t Voltage \t Duration'] desc = self.get_all_sections() @@ -93,7 +128,7 @@ def export_txt(self, fname): if round: vend = np.round(vend) - output_lines.append(f"{_type}\t{vend}\t{dur}") + output_lines.append(f'{_type}\t{vend}\t{dur}') with open(fname, 'w') as fout: for line in output_lines: From 2a1326dacccfa4f24450313576487e4a875d7022 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 15:22:01 +0100 Subject: [PATCH 02/19] Update flake8 ignore rules --- .flake8 | 5 ++--- docs/source/conf.py | 6 +++--- syncropatch_export/__init__.py | 2 +- syncropatch_export/trace.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/.flake8 b/.flake8 index ab31112..4585697 100644 --- a/.flake8 +++ b/.flake8 @@ -9,10 +9,9 @@ ignore = W504, # missing whitespace around arithmetic operator E226, - # Import sorting - I201 - I100 exclude= .git, + .github, venv, + tests/test_data, diff --git a/docs/source/conf.py b/docs/source/conf.py index c5c072f..d1a2f70 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,7 @@ # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. @@ -33,7 +33,7 @@ # Autodoc defaults autodoc_default_options = { 'members': None, - #'inherited-members': None, + # 'inherited-members': None, } # Add any paths that contain templates here, relative to this directory. @@ -50,7 +50,7 @@ # General information about the project. project = u'Syncropatch export' -#copyright = datkit.COPYRIGHT +# copyright = datkit.COPYRIGHT # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/syncropatch_export/__init__.py b/syncropatch_export/__init__.py index 5519020..53ae12e 100644 --- a/syncropatch_export/__init__.py +++ b/syncropatch_export/__init__.py @@ -1,5 +1,5 @@ # # Syncropatch, main module # -from ._version import __version__ +from ._version import __version__ # noqa diff --git a/syncropatch_export/trace.py b/syncropatch_export/trace.py index 936ee0a..0e26536 100644 --- a/syncropatch_export/trace.py +++ b/syncropatch_export/trace.py @@ -78,7 +78,7 @@ def get_voltage_protocol_json(self): """ Returns unparsed JSON object representing the voltage protocol. """ - #TODO Why only the first row? + # TODO Why only the first row? return self.meta['ExperimentConditions']['VoltageProtocol'][0] def get_protocol_description(self, holding_potential=-80.0): From 882e39beff2373955b3616ad661d388e7ec1697c Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 15:26:39 +0100 Subject: [PATCH 03/19] Added #noqa to unusual import --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index bdb0b74..14aac91 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,8 @@ # Load version number -import os -import sys +import os # noqa +import sys # noqa sys.path.append(os.path.abspath('syncropatch_export')) from _version import __version__ as version # noqa sys.path.pop() From 0e0c1de2b375983013e1785329e6042220b53ceb Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 15:28:56 +0100 Subject: [PATCH 04/19] Added # isort:skip for unusual import --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 14aac91..2a53965 100644 --- a/setup.py +++ b/setup.py @@ -6,8 +6,8 @@ # Load version number -import os # noqa -import sys # noqa +import os # isort:skip +import sys # isort:skip sys.path.append(os.path.abspath('syncropatch_export')) from _version import __version__ as version # noqa sys.path.pop() From ba2c0e8f344127bd71f08f3be0b776caef968635 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 15:30:00 +0100 Subject: [PATCH 05/19] Added # isort:skip for unusual import --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2a53965..af05e42 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ import os # isort:skip import sys # isort:skip sys.path.append(os.path.abspath('syncropatch_export')) -from _version import __version__ as version # noqa +from _version import __version__ as version # noqa isort:skip sys.path.pop() del os, sys From 9fcd993841f927e255283a675823167cd34c569a Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 15:33:06 +0100 Subject: [PATCH 06/19] Added style and isort instructions to readme --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index ce1208d..3d66263 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,15 @@ Tests are written using the standard [unittest](https://docs.python.org/3.13/lib Online testing, style-checking, and coverage testing is set up using GitHub actions. Coverage testing is handled via [Codecov](https://about.codecov.io/). +Style testing is done with `flake8`. For example, to test with 4 subprocesses use +``` +flake8 -j4 +``` +Import sorting can be checked with `isort`: +``` +isort --verbose --check-only --diff syncropatch_export tests setup.py +``` + Documentation is implemented using [Sphinx](https://www.sphinx-doc.org/). To compile locally, first install the required dependencies ``` From 8d386fcc65b43932552ed43416cbbd63b9f94bd9 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 16:41:35 +0100 Subject: [PATCH 07/19] Removed unused holding_potential argument --- syncropatch_export/trace.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/syncropatch_export/trace.py b/syncropatch_export/trace.py index 0e26536..18cb570 100644 --- a/syncropatch_export/trace.py +++ b/syncropatch_export/trace.py @@ -81,7 +81,7 @@ def get_voltage_protocol_json(self): # TODO Why only the first row? return self.meta['ExperimentConditions']['VoltageProtocol'][0] - def get_protocol_description(self, holding_potential=-80.0): + def get_protocol_description(self): """ Returns the protocol as an ``np.numpy`` with an entry for each segment. @@ -119,7 +119,8 @@ def get_all_traces(self, leakcorrect=False): def get_trace_file(self, sweeps): """ - Returns the trace file index of the file for a given set of sweeps + Returns the trace file index + of the file for a given set of sweeps """ OUT_file_idx = [] OUT_idx_i = [] From 92707e95f78bc837ff5473bec2f0b88d4bb4fe90 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 16:45:39 +0100 Subject: [PATCH 08/19] Tidied up docs config --- docs/Makefile | 2 +- docs/make.bat | 2 +- docs/source/_static/bg.png | Bin 11174 -> 0 bytes docs/source/_static/placeholder | 1 + docs/source/conf.py | 2 +- 5 files changed, 4 insertions(+), 3 deletions(-) delete mode 100644 docs/source/_static/bg.png create mode 100644 docs/source/_static/placeholder diff --git a/docs/Makefile b/docs/Makefile index ad732be..0a6e431 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -SPHINXPROJ = SyncropatchExport +SPHINXPROJ = syncropath_export SOURCEDIR = source BUILDDIR = build diff --git a/docs/make.bat b/docs/make.bat index 16faa53..a56a7d5 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -9,7 +9,7 @@ if "%SPHINXBUILD%" == "" ( ) set SOURCEDIR=source set BUILDDIR=build -set SPHINXPROJ=Myokit +set SPHINXPROJ=syncropath_export if "%1" == "" goto help diff --git a/docs/source/_static/bg.png b/docs/source/_static/bg.png deleted file mode 100644 index 96009e79a27bc3cd666481b1bbca37334b4bc7ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11174 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 Ef*T``w*UYD diff --git a/docs/source/_static/placeholder b/docs/source/_static/placeholder new file mode 100644 index 0000000..8566aa9 --- /dev/null +++ b/docs/source/_static/placeholder @@ -0,0 +1 @@ +Images etc. can be placed here diff --git a/docs/source/conf.py b/docs/source/conf.py index d1a2f70..456da49 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,7 +50,7 @@ # General information about the project. project = u'Syncropatch export' -# copyright = datkit.COPYRIGHT +# copyright = syncropath_export.COPYRIGHT # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From a35da7ab4a87eff7d787b8e5a922ee16f0dae292 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 16:46:46 +0100 Subject: [PATCH 09/19] Fixed typo --- docs/Makefile | 2 +- docs/make.bat | 2 +- docs/source/conf.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 0a6e431..e9bd66d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -4,7 +4,7 @@ # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -SPHINXPROJ = syncropath_export +SPHINXPROJ = syncropatch_export SOURCEDIR = source BUILDDIR = build diff --git a/docs/make.bat b/docs/make.bat index a56a7d5..481a7ec 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -9,7 +9,7 @@ if "%SPHINXBUILD%" == "" ( ) set SOURCEDIR=source set BUILDDIR=build -set SPHINXPROJ=syncropath_export +set SPHINXPROJ=syncropatch_export if "%1" == "" goto help diff --git a/docs/source/conf.py b/docs/source/conf.py index 456da49..d708f16 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -50,7 +50,7 @@ # General information about the project. project = u'Syncropatch export' -# copyright = syncropath_export.COPYRIGHT +# copyright = syncropatch_export.COPYRIGHT # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From 893b79917b64fa4a08af259742eefbe082f2df45 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 17:56:46 +0100 Subject: [PATCH 10/19] Improved docstrings in Trace class --- syncropatch_export/trace.py | 91 +++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 24 deletions(-) diff --git a/syncropatch_export/trace.py b/syncropatch_export/trace.py index 18cb570..3c1a26e 100644 --- a/syncropatch_export/trace.py +++ b/syncropatch_export/trace.py @@ -10,7 +10,18 @@ class Trace: """ - Defines a Trace object from the output of a Nanion experiment. + Reads a Nanion experiment and provides access to the data and meta data it + contains. + + To create a :class:`Trace`, a directory name should be passed in, along + with the name of a JSON file within that directory containing the meta + data. Data can then be accessed using :meth:`get_all_traces` (to obtain a + ``dict`` mapping well names onto a 2-d numpy array containing the sampled + currents (in pA) for all sweeps. + + Well are + + Args: filepath (str): A path pointing to folder containing both ``.json`` and @@ -44,6 +55,13 @@ def __init__(self, filepath, json_file: str): self.MeasurementLayout = TraceHeader['MeasurementLayout'] self.FileInformation = TraceHeader['FileInformation'] + # Create (hardcoded) list-of-list of well names: + # [['A01', 'B01', ..., 'P01'], + # ['A02', 'B02', ..., 'P02'], + # ... + # ['A24', 'B24', ..., 'P24']] + # So a list of 24 lists with 16 entries each + # self.WELL_ID = np.array([ [lab + str(i).zfill(2) for lab in string.ascii_uppercase[:16]] for i in range(1, 25)]) @@ -67,7 +85,7 @@ def get_voltage_protocol(self): Extract information about the voltage protocol from the JSON file. Returns: - VoltageProtocol: A voltage protocol object. + The :class:`VoltageProtocol` used in this experiment. """ return VoltageProtocol.from_json( self.meta['ExperimentConditions']['VoltageProtocol'], @@ -76,7 +94,8 @@ def get_voltage_protocol(self): def get_voltage_protocol_json(self): """ - Returns unparsed JSON object representing the voltage protocol. + Returns an unparsed JSON object representing the first segment of the + voltage protocol. """ # TODO Why only the first row? return self.meta['ExperimentConditions']['VoltageProtocol'][0] @@ -86,33 +105,43 @@ def get_protocol_description(self): Returns the protocol as an ``np.numpy`` with an entry for each segment. Returns: - np.array: An array where each row contains the start time, - end time, initial voltage, and final voltage of a ramp or step - segment. + A numpy ``array`` where each row contains the start time, end time, + initial voltage, and final voltage of a ramp or step segment. """ return self.get_voltage_protocol().get_all_sections() def get_voltage(self): """ - Returns the voltage stimulus from Nanion .json file + Returns an array containing voltages (in mV) for each sampled point in + the traces. """ return np.array(self.TimeScaling['Stimulus']).astype(np.float64) * 1e3 def get_times(self): """ - Returns the time steps from Nanion .json file + Returns the sampled times (in ms). """ return np.array(self.TimeScaling['TR_Time']) * 1e3 def get_all_traces(self, leakcorrect=False): """ + Returns data for all wells and all sweeps (equivalent to calling + :meth:`get_trace_sweeps()` without any arguments). + + Current is returned in pA. + + By default, the data is returned without leak correction, but the leak + corrected data can be obtained by setting ``leakcorrect=True``. Args: - leakcorrect (bool): Set to true if using onboard leak correction + sweeps (int): The number of sweeps to return. + leakcorrect (bool): Used to choose corrected or uncorrected data. Returns: - All raw current traces from .dat files + A dictionary mapping well names (e.g. "A01") onto 2-d numpy arrays + of shape ``n_sweeps, n_times`` where ``n_sweeps`` is the number of + sweeps and ``n_times`` is the number of sampled points. """ return self.get_trace_sweeps(leakcorrect=leakcorrect) @@ -139,7 +168,22 @@ def get_trace_file(self, sweeps): def get_trace_sweeps(self, sweeps=None, leakcorrect=False): """ - Returns a subset of sweeps defined by the input ``sweeps``. + Returns the first ``sweeps`` sweeps, for all wells. + + Current is returned in pA. + + By default, the data is returned without leak correction, but the leak + corrected data can be obtained by setting ``leakcorrect=True``. + + Args: + sweeps (int): The number of sweeps to return. + leakcorrect (bool): Used to choose corrected or uncorrected data. + + Returns: + A dictionary mapping well names (e.g. "A01") onto 2-d numpy arrays + of shape ``n_sweeps, n_times`` where ``n_sweeps`` is the number of + sweeps and ``n_times`` is the number of sampled points. + """ # initialise output @@ -149,11 +193,11 @@ def get_trace_sweeps(self, sweeps=None, leakcorrect=False): out_dict[ijWell] = [] if sweeps is None: - #  Sometimes NofSweeps seems to be incorrect + # Sometimes NofSweeps seems to be incorrect sweeps = list(range(self.NofSweeps)) - # check `getsweep` input is something sensible - if len(sweeps) > self.NofSweeps: + # Check `sweeps` is something sensible + elif len(sweeps) > self.NofSweeps: raise ValueError('Required #sweeps > total #sweeps.') # convert negative values to positive @@ -193,9 +237,7 @@ def get_trace_sweeps(self, sweeps=None, leakcorrect=False): # convert to double in pA iColTraces = trace[idx_i:idx_f] * self.I2DScale[i] * 1e12 - iColWells = self.WELL_ID[i] - - for j, ijWell in enumerate(iColWells): + for j, ijWell in enumerate(self.WELL_ID[i]): if leakcorrect: leakoffset = 1 else: @@ -229,9 +271,11 @@ def get_onboard_QC_values(self, sweeps=None): """ Return the quality control values Rseal, Cslow (Cm), and Rseries. - returns: A dictionary where the keys are the well e.g. 'A01' and the - values are the values used for onboard QC i.e., the seal resistance, - cell capacitance and the series resistance. + Returns: + A dict mapping well names ('A01' up to 'P24') to tuples + ``(R_seal, Cm, R_series)`` containing the seal resistance, membrane + capacitance, and series resistance. + """ # load QC values @@ -268,12 +312,11 @@ def get_onboard_QC_values(self, sweeps=None): def get_onboard_QC_df(self, sweeps=None): """ - Create a Pandas DataFrame which lists the Rseries, memebrane - capacitance and Rseries for each well and sweep. + Create a Pandas DataFrame containing the seal resistance, membrane + capacitance, and series resistance for each well and sweep. Returns: - pandas.DataFrame: A data frame describing the onboard QC estimates - for each well, sweep + A ``pandas.DataFrame`` with the onboard QC estimates. """ From 635bc3d7853f5f5d82ca7e23efbf9c97c02324f7 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 21:48:01 +0100 Subject: [PATCH 11/19] Made default VoltageTrace constructor copy mutable list/array data --- syncropatch_export/voltage_protocols.py | 28 ++++++++++--------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/syncropatch_export/voltage_protocols.py b/syncropatch_export/voltage_protocols.py index b2b9e52..fe6c59f 100644 --- a/syncropatch_export/voltage_protocols.py +++ b/syncropatch_export/voltage_protocols.py @@ -3,7 +3,7 @@ class VoltageProtocol: """ - Represent a voltage step (and ramp) protocol. + Represent a voltage step and ramp protocol. Each protocol is represented as @@ -15,6 +15,10 @@ class VoltageProtocol: `meth:`VoltageProtocol.from_voltage_trace`. """ + def __init__(self, desc, holding_potential, copy_data=True): + self._desc = np.copy(desc) if copy_data else desc + self.holding_potential = holding_potential + @classmethod def from_json(cls, json_protocol, holding_potential): """ @@ -33,7 +37,7 @@ def from_json(cls, json_protocol, holding_potential): vstart = float(section['VoltageStart']) vend = float(section['VoltageEnd']) output_sections.append((tstart, tstart + tdur, vstart, vend)) - return cls(np.array(output_sections), holding_potential) + return cls(np.array(output_sections), holding_potential, False) @classmethod def from_voltage_trace(cls, voltage_trace, times, holding_potential=-80.0): @@ -45,12 +49,9 @@ def from_voltage_trace(cls, voltage_trace, times, holding_potential=-80.0): # Find gradient changes diff2 = np.abs(np.diff(voltage_trace, n=2)) - - windows = np.argwhere(diff2 > threshold).flatten() - window_locs = np.unique(windows) + window_locs = np.unique(np.argwhere(diff2 > threshold).flatten()) window_locs = 1 + np.array([ val for val in window_locs if val + 1 not in window_locs]) - windows = zip([0] + list(window_locs), list(window_locs) + [len(voltage_trace) - 1]) @@ -58,26 +59,19 @@ def from_voltage_trace(cls, voltage_trace, times, holding_potential=-80.0): for start, end in windows: start_t = times[start] end_t = times[end] - - ramp = voltage_trace[end - 1] != voltage_trace[start] - v_start = voltage_trace[start] - - if ramp: + if voltage_trace[end - 1] != voltage_trace[start]: + # Ramp grad = (voltage_trace[end - 1] - voltage_trace[start]) / \ (times[end - 1] - times[start]) v_end = v_start + grad * (end_t - start_t) else: + # Step v_end = voltage_trace[end - 1] lst.append(np.array([start_t, end_t, v_start, v_end])) - desc = np.vstack(lst) - return cls(desc, holding_potential) - - def __init__(self, desc, holding_potential): - self._desc = desc - self.holding_potential = holding_potential + return cls(np.vstack(lst), holding_potential, False) def get_holding_potential(self): """ Returns this protocol's holding potential. """ From ea24c57791b88d5c138c4d31472c55001fbc99e6 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 21:52:02 +0100 Subject: [PATCH 12/19] Added cover pragma to tests themselves --- tests/test_trace_class.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py index 0b4901b..7d8ab8a 100644 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -19,7 +19,7 @@ def setUp(self): self.output_dir = os.path.join('test_output', 'test_trace_class') if not os.path.exists(self.output_dir): - os.makedirs(self.output_dir) + os.makedirs(self.output_dir) # pragma: no-cover self.test_trace = tr(filepath, json_file) def test_protocol_descriptions(self): @@ -60,11 +60,10 @@ def voltage_func(t): for tstart, tend, vstart, vend in voltage_protocol.get_all_sections(): if t >= tstart and t < tend: if vstart != vend: - return vstart + (vend - vstart) * (t - tstart)/(tend - tstart) + return vstart + (vend - vstart) * (t - tstart) / (tend - tstart) else: return vstart - - return voltage_protocol.get_holding_potential() + return voltage_protocol.get_holding_potential() # pragma: no-cover for t, v in zip(times, voltages): self.assertLess(voltage_func(t) - v, 1e-3) From 7061902838aa77c622d2f15c89f78acdc3ecdeec Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 22:22:02 +0100 Subject: [PATCH 13/19] Made tests use temporary directory. Added missing unit tests. --- tests/test_trace_class.py | 86 +++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 35 deletions(-) mode change 100644 => 100755 tests/test_trace_class.py diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py old mode 100644 new mode 100755 index 7d8ab8a..7038ba2 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -1,32 +1,33 @@ +#!/usr/bin/env python import json import os +import tempfile import unittest import matplotlib.pyplot as plt import numpy as np import pandas as pd -from syncropatch_export.trace import Trace as tr +from syncropatch_export.trace import Trace from syncropatch_export.voltage_protocols import VoltageProtocol class TestTraceClass(unittest.TestCase): - def setUp(self): - filepath = os.path.join('tests', 'test_data', '13112023_MW2_FF', - 'staircaseramp (2)_2kHz_15.01.07') - json_file = "staircaseramp (2)_2kHz_15.01.07" + """ + Tests both the Trace and VoltageProtocol classes. + """ - self.output_dir = os.path.join('test_output', 'test_trace_class') - if not os.path.exists(self.output_dir): - os.makedirs(self.output_dir) # pragma: no-cover - self.test_trace = tr(filepath, json_file) + def setUp(self): + f = 'staircaseramp (2)_2kHz_15.01.07' + self.trace = Trace( + os.path.join('tests', 'test_data', '13112023_MW2_FF', f), f) def test_protocol_descriptions(self): - voltages = self.test_trace.get_voltage() - times = self.test_trace.get_times() + voltages = self.trace.get_voltage() + times = self.trace.get_times() - protocol_from_json = self.test_trace.get_voltage_protocol() + protocol_from_json = self.trace.get_voltage_protocol() holding_potential = protocol_from_json.get_holding_potential() protocol_desc = VoltageProtocol.from_voltage_trace(voltages, times, holding_potential) @@ -43,18 +44,17 @@ def test_protocol_descriptions(self): self.assertLess(v_error, 1e-4) def test_protocol_export(self): - protocol = self.test_trace.get_voltage_protocol() - protocol.export_txt(os.path.join(self.output_dir, 'protocol.txt')) - json_protocol = self.test_trace.get_voltage_protocol_json() - - with open(os.path.join(self.output_dir, 'protocol.json'), 'w') as fin: - json.dump(json_protocol, fin) + with tempfile.TemporaryDirectory() as d: + protocol = self.trace.get_voltage_protocol() + protocol.export_txt(os.path.join(d, 'protocol.txt')) + json_protocol = self.trace.get_voltage_protocol_json() + with open(os.path.join(d, 'protocol.json'), 'w') as fin: + json.dump(json_protocol, fin) def test_protocol_timeseries(self): - voltages = self.test_trace.get_voltage() - times = self.test_trace.get_times() - - voltage_protocol = self.test_trace.get_voltage_protocol() + voltages = self.trace.get_voltage() + times = self.trace.get_times() + voltage_protocol = self.trace.get_voltage_protocol() def voltage_func(t): for tstart, tend, vstart, vend in voltage_protocol.get_all_sections(): @@ -68,21 +68,33 @@ def voltage_func(t): for t, v in zip(times, voltages): self.assertLess(voltage_func(t) - v, 1e-3) + def test_protocol_get_step_start_times(self): + a = list(self.trace.get_voltage_protocol().get_step_start_times()) + b = [0, 250, 300, 696, 896, 1896, 2396, 3396, 3896, 4396, 4896, 5396, + 5896, 6396, 6896, 7396, 7896, 8396, 8896, 9396, 9896, 10396, + 10896, 11396, 11896, 12396, 12896, 13896, 14396, 14406, 14502, + 14892] + self.assertEqual(a, b) + + def test_protocol_get_ramps(self): + a = np.array(self.trace.get_voltage_protocol().get_ramps()) + b = np.array([[300, 696, -120, -80], [14406, 14502, -70, -110]]) + self.assertEqual(a.shape, b.shape) + self.assertTrue(np.all(a == b)) + def test_get_QC(self): - tr = self.test_trace - QC_values = tr.get_onboard_QC_values() + QC_values = self.trace.get_onboard_QC_values() self.assertGreater(len(QC_values), 0) - df = tr.get_onboard_QC_df() + df = self.trace.get_onboard_QC_df() self.assertGreater(df.shape[0], 0) self.assertGreater(df.shape[1], 0) def test_get_traces(self): - tr = self.test_trace - v = tr.get_voltage() - ts = tr.get_times() - all_traces = tr.get_all_traces(leakcorrect=True) - all_traces = tr.get_all_traces() + v = self.trace.get_voltage() + ts = self.trace.get_times() + all_traces = self.trace.get_all_traces(leakcorrect=True) + all_traces = self.trace.get_all_traces() self.assertTrue(np.all(np.isfinite(v))) self.assertTrue(np.all(np.isfinite(ts))) @@ -90,11 +102,12 @@ def test_get_traces(self): for well, trace in all_traces.items(): self.assertTrue(np.all(np.isfinite(trace))) + ''' if self.output_dir: # plot test output fig, (ax1, ax2) = plt.subplots(2, 1) ax1.set_title("Example Sweeps") - some_sweeps = tr.get_trace_sweeps([0])['A01'] + some_sweeps = self.trace.get_trace_sweeps([0])['A01'] ax1.plot(ts, np.transpose(some_sweeps), color='grey', alpha=0.5) ax1.set_ylabel('Current') @@ -104,13 +117,13 @@ def test_get_traces(self): ax2.set_ylabel('Voltage') ax2.set_xlabel('Time') plt.tight_layout() - plt.savefig(os.path.join(self.output_dir, - 'example_trace')) + plt.savefig(os.path.join(self.output_dir, 'example_trace')) plt.close(fig) + ''' def test_qc_df(self): - dfs = [self.test_trace.get_onboard_QC_df(sweeps=[0]), - self.test_trace.get_onboard_QC_df(sweeps=None)] + dfs = [self.trace.get_onboard_QC_df(sweeps=[0]), + self.trace.get_onboard_QC_df(sweeps=None)] for res in dfs: # Check res is a pd.DataFrame self.assertIsInstance(res, pd.DataFrame) @@ -125,3 +138,6 @@ def test_qc_df(self): # Check restricting number of sweeps returns less data self.assertLess(dfs[0].shape[0], dfs[1].shape[0]) + +if __name__ == '__main__': + unittest.main() From c3263f7de3618300703feee6ba5c3b40c5109b2f Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 22:25:17 +0100 Subject: [PATCH 14/19] Style fix --- tests/test_trace_class.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py index 7038ba2..10a435a 100755 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -4,7 +4,6 @@ import tempfile import unittest -import matplotlib.pyplot as plt import numpy as np import pandas as pd @@ -17,7 +16,6 @@ class TestTraceClass(unittest.TestCase): Tests both the Trace and VoltageProtocol classes. """ - def setUp(self): f = 'staircaseramp (2)_2kHz_15.01.07' self.trace = Trace( @@ -103,21 +101,26 @@ def test_get_traces(self): self.assertTrue(np.all(np.isfinite(trace))) ''' - if self.output_dir: - # plot test output + # plot test output + if False: + d = 'test_output' + if not os.path.exists(d): + os.makedirs(d) + + import matplotlib.pyplot as plt fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.set_title("Example Sweeps") + ax1.set_title('Example Sweeps') some_sweeps = self.trace.get_trace_sweeps([0])['A01'] ax1.plot(ts, np.transpose(some_sweeps), color='grey', alpha=0.5) ax1.set_ylabel('Current') ax1.set_xlabel('Time') - ax2.set_title("Voltage Protocol") + ax2.set_title('Voltage Protocol') ax2.plot(ts, v) ax2.set_ylabel('Voltage') ax2.set_xlabel('Time') plt.tight_layout() - plt.savefig(os.path.join(self.output_dir, 'example_trace')) + plt.savefig(os.path.join(d, 'example_trace')) plt.close(fig) ''' From fc7d1d2ee51f5b1da90dda3ae0547172dc83aa51 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 22:33:41 +0100 Subject: [PATCH 15/19] Added missing unit tests --- syncropatch_export/trace.py | 7 +++---- tests/test_trace_class.py | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/syncropatch_export/trace.py b/syncropatch_export/trace.py index 3c1a26e..d3c5725 100644 --- a/syncropatch_export/trace.py +++ b/syncropatch_export/trace.py @@ -33,10 +33,9 @@ class Trace: def __init__(self, filepath, json_file: str): # store file paths self.filepath = filepath - if json_file[-5:] == '.json': - self.json_file = json_file - else: - self.json_file = json_file + '.json' + self.json_file = json_file + if not json_file.endswith('.json'): + self.json_file += '.json' # load json file with open(os.path.join(self.filepath, self.json_file)) as f: diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py index 10a435a..7cb9c6f 100755 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -41,6 +41,12 @@ def test_protocol_descriptions(self): self.assertLess(t_error, 1e-2) self.assertLess(v_error, 1e-4) + def test_get_protocol_description(self): + a = np.array(self.trace.get_protocol_description()) + b = np.array(self.trace.get_voltage_protocol().get_all_sections()) + self.assertEqual(a.shape, b.shape) + self.assertTrue(np.all(a == b)) + def test_protocol_export(self): with tempfile.TemporaryDirectory() as d: protocol = self.trace.get_voltage_protocol() From ab5772409da1208a3d2fb95c228c4afa046c6975 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 22:46:54 +0100 Subject: [PATCH 16/19] Added missing unit tests --- syncropatch_export/trace.py | 26 +++++++++++++++----------- tests/test_trace_class.py | 13 +++++++++++++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/syncropatch_export/trace.py b/syncropatch_export/trace.py index d3c5725..cb144a6 100644 --- a/syncropatch_export/trace.py +++ b/syncropatch_export/trace.py @@ -175,7 +175,7 @@ def get_trace_sweeps(self, sweeps=None, leakcorrect=False): corrected data can be obtained by setting ``leakcorrect=True``. Args: - sweeps (int): The number of sweeps to return. + sweeps (list): A list of sweep indexes to return, e.g. ``[0, 1, 2]``. leakcorrect (bool): Used to choose corrected or uncorrected data. Returns: @@ -191,18 +191,22 @@ def get_trace_sweeps(self, sweeps=None, leakcorrect=False): for ijWell in iCol: out_dict[ijWell] = [] + # No sweeps selected? Then return full set if sweeps is None: - # Sometimes NofSweeps seems to be incorrect sweeps = list(range(self.NofSweeps)) - - # Check `sweeps` is something sensible - elif len(sweeps) > self.NofSweeps: - raise ValueError('Required #sweeps > total #sweeps.') - - # convert negative values to positive - for i, sweep in enumerate(sweeps): - if sweep < 0: - sweeps[i] = self.NofSweeps + sweep + else: + # Allow negative values to index later sweeps + sweeps = [self.NofSweeps + x if x < 0 else x for x in sweeps] + # Check all sweeps exist + if max(sweeps) >= self.NofSweeps: + raise ValueError( + f'Invalid sweep selection: sweep {max(sweeps)} requested,' + f' but only {self.NofSweeps} available.') + if min(sweeps) < 0: + raise ValueError( + f'Invalid sweep selection: sweep' + f' {min(sweeps) - self.NofSweeps} requested, but only' + f' {self.NofSweeps} available.') trace_file_idxs, idx_is = self.get_trace_file(sweeps) diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py index 7cb9c6f..5bb0676 100755 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -99,6 +99,7 @@ def test_get_traces(self): ts = self.trace.get_times() all_traces = self.trace.get_all_traces(leakcorrect=True) all_traces = self.trace.get_all_traces() + # TODO: Check the output, numerically, by comparing a few points self.assertTrue(np.all(np.isfinite(v))) self.assertTrue(np.all(np.isfinite(ts))) @@ -106,6 +107,18 @@ def test_get_traces(self): for well, trace in all_traces.items(): self.assertTrue(np.all(np.isfinite(trace))) + # Test complex sweep selection + a = self.trace.get_trace_sweeps([-1, -2]) + b = self.trace.get_trace_sweeps([1, 0]) + self.assertEqual(len(a), len(b)) + self.assertTrue(np.all(a['A01'] == b['A01'])) + + # Test asking for non-existent sweeps + self.assertRaisesRegex(ValueError, 'Invalid sweep selection', + self.trace.get_trace_sweeps, [2]) + self.assertRaisesRegex(ValueError, 'Invalid sweep selection', + self.trace.get_trace_sweeps, [-3]) + ''' # plot test output if False: From 14a279be31949fe99889defb129047ebf9868afd Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 22:49:40 +0100 Subject: [PATCH 17/19] Fixed cover pragma --- tests/test_trace_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py index 5bb0676..a51295a 100755 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -67,7 +67,7 @@ def voltage_func(t): return vstart + (vend - vstart) * (t - tstart) / (tend - tstart) else: return vstart - return voltage_protocol.get_holding_potential() # pragma: no-cover + return voltage_protocol.get_holding_potential() # pragma: no cover for t, v in zip(times, voltages): self.assertLess(voltage_func(t) - v, 1e-3) From 1ae5d98f0c245e766814441f2fe2779b33a21d53 Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Tue, 15 Jul 2025 22:50:36 +0100 Subject: [PATCH 18/19] Added cover pragma --- tests/test_trace_class.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_trace_class.py b/tests/test_trace_class.py index a51295a..bee8289 100755 --- a/tests/test_trace_class.py +++ b/tests/test_trace_class.py @@ -162,4 +162,4 @@ def test_qc_df(self): if __name__ == '__main__': - unittest.main() + unittest.main() # pragma: no cover From 41b8d357bcab3b98c32fbb7722c27d74028d53dc Mon Sep 17 00:00:00 2001 From: Michael Clerx Date: Wed, 16 Jul 2025 11:51:08 +0100 Subject: [PATCH 19/19] Update .flake8 Co-authored-by: Kwabena Amponsah --- .flake8 | 1 + 1 file changed, 1 insertion(+) diff --git a/.flake8 b/.flake8 index 4585697..8f9ea55 100644 --- a/.flake8 +++ b/.flake8 @@ -13,5 +13,6 @@ ignore = exclude= .git, .github, + .venv, venv, tests/test_data,