Skip to content

Commit b8ac498

Browse files
authored
Merge branch 'main' into MichaelClerx-patch-1
2 parents 8686b53 + 3ef4b0b commit b8ac498

16 files changed

Lines changed: 470 additions & 356 deletions

.flake8

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ ignore =
99
W504,
1010
# missing whitespace around arithmetic operator
1111
E226,
12-
# Import sorting
13-
I201
14-
I100
12+
# Allow I, l and O as variable names
13+
E741,
14+
1515

1616
exclude=
1717
.git,

.github/workflows/pytest.yml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ on: [push]
44
jobs:
55
build:
66
runs-on: ubuntu-latest
7+
if: github.event.pull_request.draft == false
78
strategy:
89
matrix:
9-
python-version: [3.8, 3.9, '3.10', 3.11, 3.12, 3.13]
10+
python-version: ['3.10', 3.11, 3.12, 3.13, 3.14]
11+
1012
steps:
1113
- name: Checkout repository
1214
uses: actions/checkout@v4
@@ -52,5 +54,5 @@ jobs:
5254
- uses: codecov/codecov-action@v4
5355
with:
5456
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
55-
if: success() && matrix.python-version == 3.13
57+
if: success() && matrix.python-version == 3.14
5658

.github/workflows/todo.yml

Lines changed: 0 additions & 9 deletions
This file was deleted.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This repository contains a python package and scripts for handling time-series d
55
The package has been tested with data from a SyncroPatch 384, but may be adapted to work with data in other formats.
66
It can also be used to perform quality control (QC) as described in [Lei et al. (2019)](https://doi.org/10.1016%2Fj.bpj.2019.07.029).
77

8-
This package is tested on Ubuntu with Python 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13.
8+
This package is tested on Ubuntu with Python 3.10, 3.11, 3.12, 3.13 and 3.14.
99

1010
## Getting Started
1111

pcpostprocess/directory_builder.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import datetime
2+
import os
3+
import sys
4+
5+
from ._version import __commit_id__, __version__
6+
7+
8+
def get_git_revision_hash():
9+
"""
10+
Get the hash for the git commit currently being used.
11+
12+
@return The most recent commit hash or a suitable message
13+
14+
"""
15+
16+
return __commit_id__
17+
18+
19+
def get_build_type():
20+
if "dev" in __version__:
21+
return "Develop"
22+
else:
23+
return "Release"
24+
25+
26+
def setup_output_directory(dirname: str = None, subdir_name: str = None):
27+
"""
28+
Create an output directory if one doesn't already exist. Place an info
29+
file in this directory which lists the date/time created, the version of
30+
pcpostprocess, the command-line arguments provided, and the most recent git
31+
commit. The two parameters allow for a user specified top-level directory and
32+
a script-defined name for a subdirectory.
33+
34+
@param Optional directory name
35+
@param Optional subdirectory name
36+
37+
@return The path to the created file directory (String)
38+
"""
39+
40+
if dirname is None:
41+
if subdir_name:
42+
dirname = os.path.join("output", f"{subdir_name}")
43+
else:
44+
dirname = os.path.join("output", "output")
45+
46+
if subdir_name is not None:
47+
dirname = os.path.join(dirname, subdir_name)
48+
if not os.path.exists(dirname):
49+
os.makedirs(dirname)
50+
51+
with open(os.path.join(dirname, "pcpostprocess_info.txt"), "w") as description_fout:
52+
git_hash = get_git_revision_hash()
53+
datetimestr = str(datetime.datetime.now())
54+
description_fout.write("pcpostprocess output "
55+
"https://github.com/CardiacModelling/pcpostprocess\n")
56+
description_fout.write(f"Date: {datetimestr}\n")
57+
description_fout.write(f"Version: {__version__}\n")
58+
description_fout.write(f"Build type: {get_build_type()}\n")
59+
description_fout.write(f"Commit: {git_hash}\n")
60+
command = " ".join(sys.argv)
61+
description_fout.write(f"Command: {command}\n")
62+
return dirname
63+

pcpostprocess/infer_reversal.py

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,55 @@
55
import numpy as np
66
import numpy.polynomial.polynomial as poly
77

8+
from .detect_ramp_bounds import detect_ramp_bounds
89

9-
def infer_reversal_potential(current, times, voltage_segments, voltages,
10-
ax=None, output_path=None, plot=None,
11-
known_Erev=None, figsize=(5, 3)):
12-
13-
if output_path:
14-
dirname = os.path.dirname(output_path)
15-
if not os.path.exists(dirname):
16-
os.makedirs(dirname)
17-
18-
if (ax or output_path) and plot is not False:
19-
plot = True
20-
21-
# Find indices of observations during the reversal ramp
22-
ramps = [line for line in voltage_segments if line[2] != line[3]]
2310

24-
# Assume the last ramp is the reversal ramp (convert to ms)
25-
tstart, tend = np.array(ramps)[-1, :2]
11+
def infer_reversal_potential(current, times, voltage_segments, voltages,
12+
output_path=None, known_Erev=None,
13+
figsize=(5, 3)):
14+
"""
15+
Infers a reversal potential in a time series, based on a reversal ramp.
16+
17+
The data is denoised by fitting a 4-th order polynomial through the ramp
18+
data, from which a reversal potential is then detected. If no polynomial
19+
can be fit or the resulting zero-crossing is outside of
20+
``min(voltages), max(voltages)``, then ``np.nan`` is returned.
21+
22+
@param current: The currents that make up a time series with ``times``
23+
@param times: The sampled times
24+
@param voltage_segments: A list of tuples (tstart, tend, vstart, vend)
25+
describing voltage steps or ramps. It is assumed the final ramp is the
26+
reversal ramp.
27+
@param voltages: The sampled voltages
28+
@param output_path: An optional path to store a plot at
29+
@param known_Erev: A known reversal potential to include in the plot
30+
@param figsize: A size for the plot.
31+
32+
@return: The inferred reversal potential
33+
"""
34+
35+
# Get ramp bounds, assuming final ramp is the reversal ramp
36+
tstart, tend = detect_ramp_bounds(times, voltage_segments, -1)
2637

2738
istart = np.argmax(times > tstart)
2839
iend = np.argmax(times > tend)
2940

30-
times = times[istart:iend]
3141
current = current[istart:iend]
3242
voltages = voltages[istart:iend]
3343

44+
# Fit a 4-th order polynomial
3445
try:
3546
fitted_poly = poly.Polynomial.fit(voltages, current, 4)
3647
except ValueError as exc:
3748
logging.warning(str(exc))
3849
return np.nan
3950

51+
# Try extracting the polynomial's roots, accepting only ones that are
52+
# within the range of sampled voltages (so not using ramp info here!)
4053
try:
54+
vmin, vmax = np.min(voltages), np.max(voltages)
4155
roots = np.unique([np.real(root) for root in fitted_poly.roots()
42-
if root > np.min(voltages) and root < np.max(voltages)])
56+
if root > vmin and root < vmax])
4357
except np.linalg.LinAlgError as exc:
4458
logging.warning(str(exc))
4559
return np.nan
@@ -50,33 +64,30 @@ def infer_reversal_potential(current, times, voltage_segments, voltages,
5064

5165
if len(roots) == 0:
5266
return np.nan
67+
erev = roots[-1]
5368

54-
if plot:
55-
created_fig = False
56-
if ax is None and output_path is not None:
57-
58-
created_fig = True
59-
fig = plt.figure(figsize=figsize)
60-
ax = fig.subplots()
69+
# Optional plot
70+
if output_path is not None:
71+
dirname = os.path.dirname(output_path)
72+
if not os.path.exists(dirname):
73+
os.makedirs(dirname)
6174

62-
ax.set_xlabel('$V$ (mV)')
75+
fig = plt.figure(figsize=figsize)
76+
ax = fig.subplots()
77+
ax.set_xlabel('$V$ (mV)') # Assuming mV here
6378
ax.set_ylabel('$I$ (nA)')
6479

6580
# Now plot current vs voltage
6681
ax.plot(voltages, current, 'x', markersize=2, color='grey', alpha=.5)
67-
ax.axvline(roots[-1], linestyle='--', color='grey', label=r'$E_\mathrm{obs}$')
82+
ax.axvline(erev, linestyle='--', color='grey', label=r'$E_\mathrm{obs}$')
6883
if known_Erev:
6984
ax.axvline(known_Erev, linestyle='--', color='orange',
7085
label="Calculated $E_{Kr}$")
7186
ax.axhline(0, linestyle='--', color='grey')
7287
ax.plot(*fitted_poly.linspace())
7388
ax.legend()
7489

75-
if output_path is not None:
76-
fig = ax.figure
77-
fig.savefig(output_path)
78-
79-
if created_fig:
80-
plt.close(fig)
90+
fig.savefig(output_path)
91+
plt.close(fig)
8192

82-
return roots[-1]
93+
return erev

0 commit comments

Comments
 (0)