From 359c7047ff86eaae80be9ff9d7a165b33cfd06e5 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 22 Jun 2020 14:51:52 +0200 Subject: [PATCH 01/46] Added dedicated directory for benchmarking and started implementing an ECG benchmark that streams data from the GUDB. --- .../benchmark_ECG_local.py} | 2 +- biopeaks/benchmarks/benchmark_ECG_stream.py | 8 + .../{tests => benchmarks}/benchmark_PPG.py | 0 biopeaks/benchmarks/benchmark_utils.py | 166 ++++++++++++++++++ 4 files changed, 175 insertions(+), 1 deletion(-) rename biopeaks/{tests/benchmark_ECG.py => benchmarks/benchmark_ECG_local.py} (97%) create mode 100644 biopeaks/benchmarks/benchmark_ECG_stream.py rename biopeaks/{tests => benchmarks}/benchmark_PPG.py (100%) create mode 100644 biopeaks/benchmarks/benchmark_utils.py diff --git a/biopeaks/tests/benchmark_ECG.py b/biopeaks/benchmarks/benchmark_ECG_local.py similarity index 97% rename from biopeaks/tests/benchmark_ECG.py rename to biopeaks/benchmarks/benchmark_ECG_local.py index 002d29d..6eaa31a 100644 --- a/biopeaks/tests/benchmark_ECG.py +++ b/biopeaks/benchmarks/benchmark_ECG_local.py @@ -9,7 +9,7 @@ from wfdb.processing import compare_annotations -GUDB_dir = r"directory\containing\GUDB\subjects" +GUDB_dir = r"...\experiment_data" os.chdir(GUDB_dir) diff --git a/biopeaks/benchmarks/benchmark_ECG_stream.py b/biopeaks/benchmarks/benchmark_ECG_stream.py new file mode 100644 index 0000000..2fe1798 --- /dev/null +++ b/biopeaks/benchmarks/benchmark_ECG_stream.py @@ -0,0 +1,8 @@ +from biopeaks.heart import ecg_peaks +from benchmark_utils import BenchmarkDetectorGUDB + + +urls = [f"https://berndporr.github.io/ECG-GUDB/experiment_data/subject_0{i}/hand_bike/" for i in range(25)] + +pipeline = BenchmarkDetectorGUDB(ecg_peaks, 1) +pipeline.benchmark_records(urls) diff --git a/biopeaks/tests/benchmark_PPG.py b/biopeaks/benchmarks/benchmark_PPG.py similarity index 100% rename from biopeaks/tests/benchmark_PPG.py rename to biopeaks/benchmarks/benchmark_PPG.py diff --git a/biopeaks/benchmarks/benchmark_utils.py b/biopeaks/benchmarks/benchmark_utils.py new file mode 100644 index 0000000..981b7aa --- /dev/null +++ b/biopeaks/benchmarks/benchmark_utils.py @@ -0,0 +1,166 @@ +import asyncio +from io import StringIO +from timeit import default_timer as timer +import aiohttp +import numpy as np +from wfdb.processing import compare_annotations + + +class BenchmarkDetectorGUDB: + """Evaluate an ECG R-peak detector on datasets from the GUDB database. + """ + + def __init__(self, detector, tolerance, sfreq=250, n_runs=100): + """ + Parameters + ---------- + detector : function + A function that takes a 1d array containing a physiological record + as first positional argument and an integer sampling rate as second + positional argument. Must return a 1d array containing the detected + extrema. + tolerance : int + Maximum difference in samples that is permitted between the manual + annotation and the annotation generated by the detector. + sfreq : int, optional + The sampling rate of the GUDB records in Hertz, by default 250. + n_runs : int, optional + The number of runs used for obtaining the average run time of the + detector, by default 100. + """ + self.detector = detector + self.tolerance = tolerance + self.sfreq = sfreq + self.n_runs = n_runs + self.session = None + self.queue = None + + + async def score_record(self, record, annotation): + """Obtain detector performance for an annotated record. + + Parameters + ---------- + record : 1d array + The raw physiological record. + annotation : 1d array + The manual extrema annotations. + + Returns + ------- + precision : float + The detectors precision on the record given the tolerance. + sensitivity : float + The detectors sensitivity on the record given the tolerance. + + """ + detector_annotation = self.detector(record, self.sfreq) + + comparitor = compare_annotations(detector_annotation, annotation, + self.tolerance) + tp = comparitor.tp + fp = comparitor.fp + fn = comparitor.fn + sensitivity = tp / (tp + fn) + precision = tp / (tp + fp) + + return precision, sensitivity + + + async def time_record(self, record): + """Obtain the average run time of a detector on a record over N runs. + + Parameters + ---------- + record : 1d array + The raw physiological record. + + Returns + ------- + avg_time : int + The run time of the detector on the record averaged over n_runs. In + milliseconds. + + """ + start = timer() + + for _ in range(self.n_runs): + self.detector(record, self.sfreq) + + end = timer() + avg_time = (end - start) / self.n_runs * 1000 + + return avg_time + + + async def fetch_record(self, url): + """Get a record from the GUDB server. + + Fetch the raw physiological data and the corresponding annotation, + format them, and put them on a queue for further processing. + + Parameters + ---------- + url : str + An experiment directory of the GUDB. The URL must end with the + experiment ID. E.g., "URL/maths". The experiment ID can be one of + {"maths", "hand_bike", "jogging", "walking", "sitting"}. + """ + print(f"fetching {url}") + async with self.session.get(url + "/ECG.tsv") as response: + physio = await response.read() + physio = np.loadtxt(StringIO(physio.decode("utf-8"))) + physio = np.ravel(physio[:, 1]) # select the first lead + async with self.session.get(url + "/annotation_cables.tsv") as response: + annotation = await response.read() + annotation = np.loadtxt(StringIO(annotation.decode("utf-8"))) + annotation = np.ravel(annotation) + + await self.queue.put((physio, annotation, url)) + + + async def benchmark_record(self, n_records): + """Evaluate the performance of the detector on a single record. + + Parameters + ---------- + n_records : int + Overall number of records to be evaluated. Necessary to know when + to stop waiting for incoming records. + """ + n = 0 + while n < n_records: + # Wait for a record to be added to the queue. + physio, annotation, url = await self.queue.get() + + # Process the record. + print(f"processing {url}") + precision, sensitivity = await self.score_record(physio, annotation) + avg_time = await self.time_record(physio) + print(precision, sensitivity, avg_time) + + n += 1 + + + async def _benchmark_records(self, experiment_urls): + """Evaluate the performance of the detector on a set of records. + + Parameters + ---------- + experiment_urls : list + List of strings containing the URLs to GUDB records. The URL must + end with the experiment ID. E.g., "URL/maths". The experiment ID can + be one of {"maths", "hand_bike", "jogging", "walking", "sitting"}. + """ + self.session = aiohttp.ClientSession() + self.queue = asyncio.Queue() + fetch_coro = [self.fetch_record(url) for url in experiment_urls] + benchmark_coro = self.benchmark_record(len(experiment_urls)) + + await asyncio.gather(*fetch_coro, benchmark_coro) + await self.session.close() + + + def benchmark_records(self, experiment_urls): + """Wrapper starting the event loop.""" + asyncio.run(self._benchmark_records(experiment_urls)) From 3f2fde3e7b2410984115fb1007df50cc204265bb Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 22 Jun 2020 20:58:53 +0200 Subject: [PATCH 02/46] Handling bad requests. --- biopeaks/benchmarks/benchmark_ECG_stream.py | 2 +- biopeaks/benchmarks/benchmark_utils.py | 29 ++++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/biopeaks/benchmarks/benchmark_ECG_stream.py b/biopeaks/benchmarks/benchmark_ECG_stream.py index 2fe1798..35e4218 100644 --- a/biopeaks/benchmarks/benchmark_ECG_stream.py +++ b/biopeaks/benchmarks/benchmark_ECG_stream.py @@ -2,7 +2,7 @@ from benchmark_utils import BenchmarkDetectorGUDB -urls = [f"https://berndporr.github.io/ECG-GUDB/experiment_data/subject_0{i}/hand_bike/" for i in range(25)] +urls = [f"https://berndporr.github.io/ECG-GUDB/experiment_data/subject_{str(i).zfill(2)}/jogging/" for i in range(25)] pipeline = BenchmarkDetectorGUDB(ecg_peaks, 1) pipeline.benchmark_records(urls) diff --git a/biopeaks/benchmarks/benchmark_utils.py b/biopeaks/benchmarks/benchmark_utils.py index 981b7aa..6c3559e 100644 --- a/biopeaks/benchmarks/benchmark_utils.py +++ b/biopeaks/benchmarks/benchmark_utils.py @@ -108,14 +108,21 @@ async def fetch_record(self, url): """ print(f"fetching {url}") async with self.session.get(url + "/ECG.tsv") as response: - physio = await response.read() - physio = np.loadtxt(StringIO(physio.decode("utf-8"))) - physio = np.ravel(physio[:, 1]) # select the first lead + if response.status == 200: + physio = await response.text() + physio = np.loadtxt(StringIO(physio)) + physio = np.ravel(physio[:, 1]) # select the first lead + else: + physio = None + print(f"Couldn't find physio file at {url}") async with self.session.get(url + "/annotation_cables.tsv") as response: - annotation = await response.read() - annotation = np.loadtxt(StringIO(annotation.decode("utf-8"))) - annotation = np.ravel(annotation) - + if response.status == 200: + annotation = await response.text() + annotation = np.loadtxt(StringIO(annotation)) + annotation = np.ravel(annotation) + else: + annotation = None + print(f"Couldn't find annotation file at {url}") await self.queue.put((physio, annotation, url)) @@ -133,6 +140,14 @@ async def benchmark_record(self, n_records): # Wait for a record to be added to the queue. physio, annotation, url = await self.queue.get() + skip_record = physio is None + skip_record = annotation is None + + if skip_record: + print(f"Skipping benchmarking of {url}.") + n += 1 + continue + # Process the record. print(f"processing {url}") precision, sensitivity = await self.score_record(physio, annotation) From 0fbf912c8684beba5e01d89c46af84ff1964e69a Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Wed, 24 Jun 2020 15:19:38 +0200 Subject: [PATCH 03/46] Print results. --- biopeaks/benchmarks/benchmark_utils.py | 37 ++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/biopeaks/benchmarks/benchmark_utils.py b/biopeaks/benchmarks/benchmark_utils.py index 6c3559e..6333cae 100644 --- a/biopeaks/benchmarks/benchmark_utils.py +++ b/biopeaks/benchmarks/benchmark_utils.py @@ -114,7 +114,7 @@ async def fetch_record(self, url): physio = np.ravel(physio[:, 1]) # select the first lead else: physio = None - print(f"Couldn't find physio file at {url}") + # print(f"Couldn't find physio file at {url}") async with self.session.get(url + "/annotation_cables.tsv") as response: if response.status == 200: annotation = await response.text() @@ -122,7 +122,7 @@ async def fetch_record(self, url): annotation = np.ravel(annotation) else: annotation = None - print(f"Couldn't find annotation file at {url}") + # print(f"Couldn't find annotation file at {url}") await self.queue.put((physio, annotation, url)) @@ -136,6 +136,10 @@ async def benchmark_record(self, n_records): to stop waiting for incoming records. """ n = 0 + sensitivities = [] + precisions = [] + avg_times = [] + while n < n_records: # Wait for a record to be added to the queue. physio, annotation, url = await self.queue.get() @@ -144,18 +148,41 @@ async def benchmark_record(self, n_records): skip_record = annotation is None if skip_record: - print(f"Skipping benchmarking of {url}.") + print(f"\nSkipping benchmarking of {url}: missing files.") n += 1 continue # Process the record. - print(f"processing {url}") precision, sensitivity = await self.score_record(physio, annotation) avg_time = await self.time_record(physio) - print(precision, sensitivity, avg_time) + + precisions.append(precision) + sensitivities.append(sensitivity) + avg_times.append(avg_time) n += 1 + print(f"\nResults {url}") + print("-" * len(url)) + print(f"sensitivity = {sensitivity}") + print(f"precision = {precision}") + print(f"average run time over {self.n_runs} runs = {avg_time}") + + print(f"\nAverage results over {len(precisions)} records") + print("-" * 31) + + mean_avg_time = np.mean(avg_times) + std_avg_time = np.std(avg_times) + print(f"average run time over {self.n_runs} runs: mean = {mean_avg_time}, std = {std_avg_time}") + + mean_sensitivity = np.mean(sensitivities) + std_sensitivity = np.std(sensitivities) + print(f"sensitivity: mean = {mean_sensitivity}, std = {std_sensitivity}") + + mean_precision = np.mean(precisions) + std_precision = np.std(precisions) + print(f"precision: mean = {mean_precision}, std = {std_precision}") + async def _benchmark_records(self, experiment_urls): """Evaluate the performance of the detector on a set of records. From 4560a5c23ba71d639e31f82e3e4076f503332417 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Wed, 24 Jun 2020 16:19:45 +0200 Subject: [PATCH 04/46] Made experiment, channel, and annotation configurable. --- biopeaks/benchmarks/benchmark_ECG_stream.py | 4 +- biopeaks/benchmarks/benchmark_utils.py | 70 +++++++++++++-------- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/biopeaks/benchmarks/benchmark_ECG_stream.py b/biopeaks/benchmarks/benchmark_ECG_stream.py index 35e4218..2ab8efa 100644 --- a/biopeaks/benchmarks/benchmark_ECG_stream.py +++ b/biopeaks/benchmarks/benchmark_ECG_stream.py @@ -2,7 +2,5 @@ from benchmark_utils import BenchmarkDetectorGUDB -urls = [f"https://berndporr.github.io/ECG-GUDB/experiment_data/subject_{str(i).zfill(2)}/jogging/" for i in range(25)] - pipeline = BenchmarkDetectorGUDB(ecg_peaks, 1) -pipeline.benchmark_records(urls) +pipeline.benchmark_records("jogging", channel="cs_V2_V1", annotation="annotation_cs") diff --git a/biopeaks/benchmarks/benchmark_utils.py b/biopeaks/benchmarks/benchmark_utils.py index 6333cae..88a3f42 100644 --- a/biopeaks/benchmarks/benchmark_utils.py +++ b/biopeaks/benchmarks/benchmark_utils.py @@ -9,6 +9,8 @@ class BenchmarkDetectorGUDB: """Evaluate an ECG R-peak detector on datasets from the GUDB database. """ + channels = {"cs_V2_V1": 0, "einthoven_II": 1, "einthoven_III": 2} + base_url = "https://berndporr.github.io/ECG-GUDB/experiment_data/subject_" def __init__(self, detector, tolerance, sfreq=250, n_runs=100): """ @@ -34,6 +36,9 @@ def __init__(self, detector, tolerance, sfreq=250, n_runs=100): self.n_runs = n_runs self.session = None self.queue = None + self.channel = None + self.annotation = None + self.urls = None async def score_record(self, record, annotation): @@ -111,11 +116,11 @@ async def fetch_record(self, url): if response.status == 200: physio = await response.text() physio = np.loadtxt(StringIO(physio)) - physio = np.ravel(physio[:, 1]) # select the first lead + physio = np.ravel(physio[:, self.channel]) else: physio = None # print(f"Couldn't find physio file at {url}") - async with self.session.get(url + "/annotation_cables.tsv") as response: + async with self.session.get(url + f"/{self.annotation}.tsv") as response: if response.status == 200: annotation = await response.text() annotation = np.loadtxt(StringIO(annotation)) @@ -126,16 +131,10 @@ async def fetch_record(self, url): await self.queue.put((physio, annotation, url)) - async def benchmark_record(self, n_records): - """Evaluate the performance of the detector on a single record. - - Parameters - ---------- - n_records : int - Overall number of records to be evaluated. Necessary to know when - to stop waiting for incoming records. - """ + async def benchmark_record(self): + """Evaluate the performance of the detector on a single record.""" n = 0 + n_records = len(self.urls) sensitivities = [] precisions = [] avg_times = [] @@ -184,25 +183,44 @@ async def benchmark_record(self, n_records): print(f"precision: mean = {mean_precision}, std = {std_precision}") - async def _benchmark_records(self, experiment_urls): - """Evaluate the performance of the detector on a set of records. - - Parameters - ---------- - experiment_urls : list - List of strings containing the URLs to GUDB records. The URL must - end with the experiment ID. E.g., "URL/maths". The experiment ID can - be one of {"maths", "hand_bike", "jogging", "walking", "sitting"}. - """ + async def _benchmark_records(self): + """Evaluate the performance of the detector on a set of records.""" self.session = aiohttp.ClientSession() self.queue = asyncio.Queue() - fetch_coro = [self.fetch_record(url) for url in experiment_urls] - benchmark_coro = self.benchmark_record(len(experiment_urls)) + fetch_coro = [self.fetch_record(url) for url in self.urls] + benchmark_coro = self.benchmark_record() await asyncio.gather(*fetch_coro, benchmark_coro) await self.session.close() - def benchmark_records(self, experiment_urls): - """Wrapper starting the event loop.""" - asyncio.run(self._benchmark_records(experiment_urls)) + def benchmark_records(self, experiment, channel="einthoven_II", + annotation="annotation_cables"): + """Wrapper starting the event loop. + + Benchmark a detector on all available records from all 25 subjects for a + given combination of experiment, channel, and annotation. + + Parameters + ---------- + experiment : str + The name of the experiment to be benchmarked. Can be one of + {"sitting", "maths", "walking", "hand_bike", "jogging"}. + channel : str, optional + The ECG channel to be benchmarked. Can be one of {"cs_V2_V1", + "einthoven_II", "einthoven_III"}, by default "einthoven_II". + annotation : str, optional + The annotation file used for benchmarking. Can be one of + {"annotation_cs", "annotation_cables"}, by default + "annotation_cables". + """ + if experiment not in ["sitting", "maths", "walking", "hand_bike", "jogging"]: + raise ValueError(f"{experiment} is not a valid experiment.") + if channel not in self.channels.keys(): + raise ValueError(f"{channel} is not a valid channel") + if annotation not in ["annotation_cs", "annotation_cables"]: + raise ValueError(f"{annotation} is not a valid annotation") + self.channel = self.channels[channel] + self.annotation = annotation + self.urls = [f"{self.base_url}{str(i).zfill(2)}/{experiment}/" for i in range(25)] + asyncio.run(self._benchmark_records()) From 4500673cc80852265abcd08b4eba80888fe48735 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Wed, 24 Jun 2020 16:32:10 +0200 Subject: [PATCH 05/46] Update ECG benchmark section. --- docs/tests.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/docs/tests.md b/docs/tests.md index 83a39a2..3759f69 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -17,19 +17,15 @@ pytest -v ## Extrema detection benchmarks -In order to validate the performance of the ECG peak detector `heart.ecg_peaks()`, -please download the [Glasgow University Database (GUDB)](http://researchdata.gla.ac.uk/716/). -In addition you need to install the [wfdb](https://github.com/MIT-LCP/wfdb-python) package either with conda +To validate the performance of the ECG peak detector `heart.ecg_peaks()`, please install the [wfdb](https://github.com/MIT-LCP/wfdb-python) and [aiohttp](https://github.com/aio-libs/aiohttp): ``` conda install -c conda-forge wfdb +conda install -c conda-forge aiohttp ``` -or pip. -``` -pip install wfdb -``` -You can then run the `benchmark_ECG` script in the test folder. +You can then run the `benchmark_ECG_stream` script in the `benchmarks` folder. The script streams ECG and annotation files from the [Glasgow University Database (GUDB)](http://researchdata.gla.ac.uk/716/). +You can select an experiment, ECG channel, and annotation file. -In order to validate the performance of the PPG peak detector `heart.ppg_peaks()` +To validate the performance of the PPG peak detector `heart.ppg_peaks()` please download the [Capnobase IEEE TBME benchmark dataset](http://www.capnobase.org/index.php?id=857). -After extracting the PPG signals and peak annotations you can run the `benchmark_PPG` script in the test folder. \ No newline at end of file +After extracting the PPG signals and peak annotations you can run the `benchmark_PPG` script in the `benchmarks` folder. \ No newline at end of file From 912a8102197cdcabeb713476bb585b288a68131f Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 6 Jul 2020 17:33:35 +0200 Subject: [PATCH 06/46] List comprehensions. --- biopeaks/controller.py | 5 +---- biopeaks/io_utils.py | 9 +++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index 37224d5..b448b3b 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -534,10 +534,7 @@ def calculate_stats(self): @threaded def save_stats(self): - savekeys = [] - for key, value in self._model.savestats.items(): - if value: - savekeys.append(key) + savekeys = [key for key, value in self._model.savestats.items() if value] savearray = np.zeros((self._model.signal.size, len(savekeys))) for i, key in enumerate(savekeys): if key == 'period': diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 74ca58a..63b6d4a 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -75,10 +75,8 @@ def write_opensignals(rpath, wpath, segment): Start and end of segments in samples. """ # Get the header. - header = [] with open(rpath, "rt") as oldfile: - for line in islice(oldfile, 3): - header.append(line) + header = [line for line in islice(oldfile, 3)] # Get the data. data = pd.read_csv(rpath, delimiter='\t', header=None, comment='#') # Apply segmentation to all channels. @@ -280,10 +278,9 @@ def _read_edfchannel(signal, n_samples, chanidx): channel_offset = np.cumsum(n_samples)[chanidx - 1] - n_chansamples # Get the number of samples to skip from epoch to epoch. channel_stride = sum(n_samples) - chansignal = [] # Skip from epoch to epoch and read the signal belonging to the channel. - for i in np.arange(channel_offset, signal.size, channel_stride): - chansignal.append(signal[i:i + n_chansamples]) + epochstarts = np.arange(channel_offset, signal.size, channel_stride) + chansignal = [signal[i:i + n_chansamples] for i in epochstarts] return np.ravel(chansignal) From cf12001aad7dcf93eb0476c57f954fb711017fcd Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 11 Jul 2020 19:46:21 +0200 Subject: [PATCH 07/46] Started implementing support for custom file input. --- biopeaks/controller.py | 55 +++++++++++++-------------- biopeaks/io_utils.py | 78 ++++++++++++++++++++++++++++---------- biopeaks/model.py | 32 +++++++++++----- biopeaks/tests/test_gui.py | 22 ++++++++--- biopeaks/view.py | 31 ++++++++++++--- 5 files changed, 149 insertions(+), 69 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index b448b3b..e9075d1 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -2,7 +2,7 @@ from .heart import ecg_peaks, ppg_peaks, correct_peaks, heart_period from .resp import resp_extrema, resp_stats -from .io_utils import read_opensignals, write_opensignals, read_edf, write_edf +from .io_utils import read_custom, read_opensignals, read_edf, write_opensignals, write_edf from pathlib import Path import pandas as pd import numpy as np @@ -59,9 +59,13 @@ def __init__(self, model): def get_fpaths(self): + self._model.fpaths = getOpenFileNames(None, 'Choose your data', "\\home")[0] if not self._model.fpaths: + if self._model.filetype == "Custom": + self._model.customheader = dict.fromkeys(self._model.customheader, None) # for custom files also reset header + self._model.set_filetype(None) # reset file type return if (self._model.batchmode == 'multiple files' and len(self._model.fpaths) >= 1): @@ -254,40 +258,29 @@ def dispatcher(self, progress): @threaded def read_signal(self, path): self._model.status = "Loading file." - file_extension = Path(path).suffix - - if file_extension not in [".txt", ".edf"]: - self._model.status = "Error: Please select an OpenSignals or EDF file." - return - - if file_extension == ".txt": + file_type = self._model.filetype - # Read signal and associated metadata. + if file_type == "OpenSignals": output = read_opensignals(path, self._model.signalchan, channeltype="signal") - # If the io utility returns an error, print the error and return. - if output["status"]: - self._model.status = output["status"] - return - - self._model.filetype = "OpenSignals" - - elif file_extension == ".edf": - - # Read signal and associated metadata. + elif file_type == "EDF": output = read_edf(path, self._model.signalchan, channeltype="signal") - # If the io utility returns an error, print the error and return. - if output["status"]: - self._model.status = output["status"] - return - - self._model.filetype = "EDF" + elif file_type == "Custom": + output = read_custom(path, self._model.customheader, + channeltype="signal") + + if output["error"]: + self._model.status = output["error"] + if self._model.filetype == "Custom": + self._model.customheader = dict.fromkeys(self._model.customheader, None) # for custom files also reset header + self._model.set_filetype(None) # reset file type + return # Important to set seconds PRIOR TO signal, otherwise plotting # behaves unexpectadly (since plotting is triggered as soon as # signal changes). - self._model.sfreq = output["sfreq"] + self._model.sfreq = output["sfreq"] # in case of custom file, sfreq is now taken over from customheader self._model.sec = output["sec"] self._model.signal = output["signal"] self._model.loaded = True @@ -305,10 +298,14 @@ def read_marker(self, path): elif self._model.filetype == "EDF": output = read_edf(path, self._model.markerchan, channeltype="marker") - # If the io utility returns an error, print the error and return. - if output["status"]: - self._model.status = output["status"] + elif self._model.filetype == "Custom": + output = read_custom(path, self._model.customheader, + channeltype="marker") + + if output["error"]: + self._model.status = output["error"] return + self._model.sfreqmarker = output["sfreq"] self._model.marker = output["signal"] diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 63b6d4a..2ae7b3c 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -5,12 +5,53 @@ import numpy as np from itertools import islice from struct import pack +from pathlib import Path + + +def read_custom(rpath, customheader, channeltype): + + # Prepare output. + output = {"error": False, + "sec": None, + "signal": None, + "sfreq": None} + + if channeltype == "signal": + chanidx = customheader["signalidx"] + elif channeltype == "narker": + chanidx = customheader["markeridx"] + + sfreq = customheader["sfreq"] + + try: + # If sep is None, the Python parsing engine can automatically detect the + # separator usinf the builtin sniffer tool, csv.Sniffer. + signal = pd.read_csv(rpath, sep=None, usecols=[chanidx], header=None, + skiprows=customheader["skiprows"], engine="python") + except Exception as error: + output["error"] = error + return output + + signallen = signal.size + sec = np.linspace(0, signallen / sfreq, signallen) + + if channeltype == "signal": + output["sec"] = sec + output["sfreq"] = sfreq + + output["signal"] = np.ravel(signal) + + return output + + +def write_custom(rpath, wpath, segment, customheader): + pass def read_opensignals(rpath, channel, channeltype): # Prepare output. - output = {"status": False, + output = {"error": False, "sec": None, "signal": None, "sfreq": None} @@ -21,7 +62,7 @@ def read_opensignals(rpath, channel, channeltype): # file. Note that the file is closed automatically once the "with" # block is exited. if "OpenSignals" not in f.readline(): - output["status"] = "Error: Text file is not in OpenSignals format." + output["error"] = "Error: Text file is not in OpenSignals format." return output # Read second line and convert json header to dict (only select first @@ -38,7 +79,7 @@ def read_opensignals(rpath, channel, channeltype): chanidx = [i for i, s in enumerate(channels) if int(channel[1]) == s] if not chanidx: - output["status"] = f"Error: {channeltype.capitalize()} channel not found." + output["error"] = f"Error: {channeltype.capitalize()} channel not found." return output # Select only first sensor of the selected modality (it is possible @@ -59,12 +100,9 @@ def read_opensignals(rpath, channel, channeltype): if channeltype == "signal": output["sec"] = sec - output["signal"] = np.ravel(signal) output["sfreq"] = sfreq - elif channeltype == "marker": - output["signal"] = np.ravel(signal) - output["sfreq"] = sfreq + output["signal"] = np.ravel(signal) return output @@ -97,11 +135,16 @@ def read_edf(rpath, channel, channeltype): https://www.teuniz.net/edf_bdf_testfiles/ """ # Prepare output. - output = {"status": False, + output = {"error": False, "sec": None, "signal": None, "sfreq": None} + file_extension = Path(rpath).suffix + if file_extension != ".edf": + output["error"] = "Error: File is not in EDF format." + return output + chanidx = int(channel[1]) with open(rpath, "rb") as f: @@ -109,7 +152,7 @@ def read_edf(rpath, channel, channeltype): signal = _read_edfsignal(f, info["end_header"]) if info["n_channels"] < chanidx: # both indices are one-based - output["status"] = f"Error: {channeltype.capitalize()} channel not found." + output["error"] = f"Error: {channeltype.capitalize()} channel not found." return output # Take care of improperly specified number of epochs. @@ -124,12 +167,9 @@ def read_edf(rpath, channel, channeltype): if channeltype == "signal": output["sec"] = sec - output["signal"] = chansignal - output["sfreq"] = chansfreq - elif channeltype == "marker": - output["signal"] = chansignal - output["sfreq"] = chansfreq + output["signal"] = chansignal + output["sfreq"] = chansfreq # important to send for both marker and signal, since they can differ in sfreq return output @@ -139,7 +179,7 @@ def write_edf(rpath, wpath, segment): segment : list Start and end of segments in seconds. """ - status = False + error = False with open(rpath, "rb") as f: info, header = _read_edfheader(f) @@ -149,11 +189,11 @@ def write_edf(rpath, wpath, segment): # writing process. duration_segment = segment[1] - segment[0] if duration_segment < info["duration_epoch"]: - status = f"Error: The segment is shorter than the epoch duration of " \ - f"{info['duration_epoch']} seconds in the original EDF file. " \ - f"Choose a segment longer than {info['duration_epoch']}." + error = f"Error: The segment is shorter than the epoch duration of " \ + f"{info['duration_epoch']} seconds in the original EDF file. " \ + f"Choose a segment longer than {info['duration_epoch']}." print(duration_segment, info["duration_epoch"]) - return status + return error # Store the signal belonging to each channel as entry in a list. chansignals = [_read_edfchannel(signal, info["n_samples"], i) diff --git a/biopeaks/model.py b/biopeaks/model.py index 539a057..90868a8 100644 --- a/biopeaks/model.py +++ b/biopeaks/model.py @@ -199,16 +199,24 @@ def status(self, value): self.status_changed.emit(value) @property - def filetype(self): - return self._filetype + def customheader(self): + return self._customheader - @filetype.setter - def filetype(self, value): - self._filetype = value + @customheader.setter + def customheader(self, value): + self._customheader = value # the following model attributes are slots that are connected to signals # from the view or controller + @Property(object) + def filetype(self): + return self._filetype + + @Slot(object) + def set_filetype(self, value): + self._filetype = value + @Property(object) def segment(self): return self._segment @@ -354,8 +362,16 @@ def __init__(self): self._correctbatchpeaks = False self._savestats = {"period":False, "rate":False, "tidalamp":False} self._filetype = None + self._customheader = {"signalidx": None, "markeridx": None, + "skiprows": None, "sfreq": None} def reset(self): + """ + Don't reset attributes that aren't ideosyncratic to the dataset (e.g., + channels, batchmode, savestats etc.). Also don't reset attributes that + must be permanently accessible during batch processing (e.g., fpaths, + wdirpeaks, wdirstats, filetype, customheader). + """ self._signal = None self._peaks = None self._periodintp = None @@ -374,9 +390,5 @@ def reset(self): self._wpathsignal = None self._rpathsignal = None self._wpathstats = None - self._filetype = None - # don't reset attributes that aren't ideosyncratic to the dataset - # (e.g., channels, batchmode, savestats etc.); also don't reset - # attributes that must be permanently accessible during batch - # processing (e.g., fpaths, wdirpeaks, wdirstats) + self.model_reset.emit() diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 528097a..370c564 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -57,7 +57,8 @@ def __init__(self, key, xdata): "peaksum": 461362, "avgperiod": 0.6652, "avgrate": 90.7158, - "segment": [20, 90]} + "segment": [20, 90], + "filetype": "OpenSignals"} ppg_edf = {"modality": "PPG", "sigchan": "A5", @@ -75,7 +76,8 @@ def __init__(self, key, xdata): "peaksum": 123270, "avgperiod": 1.0000, "avgrate": 60.0000, - "segment": [11.51, 81.7]} + "segment": [11.51, 81.7], + "filetype": "EDF"} ecg_os = {"modality": "ECG", "sigchan": "A3", @@ -93,7 +95,8 @@ def __init__(self, key, xdata): "peaksum": 4572190, "avgperiod": 1.0921, "avgrate": 55.1027, - "segment": [760, 860]} + "segment": [760, 860], + "filetype": "OpenSignals"} ecg_edf = {"modality": "ECG", "sigchan": "A3", @@ -111,7 +114,8 @@ def __init__(self, key, xdata): "peaksum": 1228440, "avgperiod": 0.4000, "avgrate": 150.0000, - "segment": [11.51, 81.7]} + "segment": [11.51, 81.7], + "filetype": "EDF"} rsp_os = {"modality": "RESP", "sigchan": "A2", @@ -130,7 +134,8 @@ def __init__(self, key, xdata): "avgperiod": 3.2676, "avgrate": 19.7336, "avgtidalamp": 129.722, - "segment": [3200, 3400]} + "segment": [3200, 3400], + "filetype": "OpenSignals"} rsp_edf = {"modality": "RESP", "sigchan": "A5", @@ -149,7 +154,8 @@ def __init__(self, key, xdata): "avgperiod": 1.0000, "avgrate": 60.0003, "avgtidalamp": 16350.0000, - "segment": [602.6, 679.26]} + "segment": [602.6, 679.26], + "filetype": "EDF"} def idcfg_single(cfg): @@ -182,6 +188,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): qtbot.keyClicks(view.sigchanmenu, cfg_single["sigchan"]) qtbot.keyClicks(view.markerchanmenu, cfg_single["markerchan"]) qtbot.keyClicks(view.batchmenu, cfg_single["mode"]) + model.set_filetype(cfg_single["filetype"]) # 1. load signal ######################################################### with qtbot.waitSignals([model.signal_changed, model.marker_changed], @@ -280,6 +287,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): ecg_batch = {"modality": "ECG", "sigchan": "A3", "mode": "multiple files", + "filetype": "OpenSignals", "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", "OSmontage2A.txt", "OSmontage2J.txt", "OSmontage3A.txt", "OSmontage3J.txt"], @@ -293,6 +301,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): ecg_batch_autocorrect = {"modality": "ECG", "sigchan": 'A3', "mode": "multiple files", + "filetype": "OpenSignals", "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", "OSmontage2A.txt", "OSmontage2J.txt", "OSmontage3A.txt", "OSmontage3J.txt"], @@ -338,6 +347,7 @@ def test_batchfile(qtbot, tmpdir, cfg_batch): view.periodcheckbox.setCheckState(Qt.Checked) view.ratecheckbox.setCheckState(Qt.Checked) model.fpaths = [datadir.joinpath(p) for p in cfg_batch["sigfnames"]] + model.set_filetype(cfg_batch["filetype"]) # Mock the controller's batch_processor in order to avoid # calls to the controller's get_wpathpeaks and get_wpathstats methods. diff --git a/biopeaks/view.py b/biopeaks/view.py index 15593aa..22d9550 100644 --- a/biopeaks/view.py +++ b/biopeaks/view.py @@ -4,7 +4,7 @@ QVBoxLayout, QHBoxLayout, QCheckBox, QLabel, QStatusBar, QGroupBox, QDockWidget, QLineEdit, QFormLayout, QPushButton, - QProgressBar, QSplitter) + QProgressBar, QSplitter, QMenu) from PySide2.QtCore import Qt, QSignalMapper, QRegExp from PySide2.QtGui import QIcon, QRegExpValidator from matplotlib.figure import Figure @@ -222,9 +222,20 @@ def __init__(self, model, controller): # signal menu signalmenu = menubar.addMenu("biosignal") - openSignal = QAction("load", self) - openSignal.triggered.connect(self._controller.get_fpaths) - signalmenu.addAction(openSignal) + openSignal = signalmenu.addMenu("load") + openEDF = QAction("EDF", self) + openEDF.triggered.connect(lambda: self._model.set_filetype("EDF")) + openEDF.triggered.connect(self._controller.get_fpaths) + openSignal.addAction(openEDF) + openOpenSignals = QAction("OpenSignals", self) + openOpenSignals.triggered.connect(lambda: self._model.set_filetype("OpenSignals")) + openOpenSignals.triggered.connect(self._controller.get_fpaths) + openSignal.addAction(openOpenSignals) + openCustom = QAction("Custom", self) + openCustom.triggered.connect(lambda: self._model.set_filetype("Custom")) + openCustom.triggered.connect(self.set_customheader) + # openCustom.triggered.connect(self._controller.get_fpaths) + openSignal.addAction(openCustom) segmentSignal = QAction("select segment", self) segmentSignal.triggered.connect(self.segmentermap.map) @@ -345,7 +356,7 @@ def __init__(self, model, controller): self.optionsgroup.setWidget(self.optionsgroupwidget) self.addDockWidget(Qt.LeftDockWidgetArea, self.optionsgroup) - + self.vlayout0.addWidget(self.splitter) self.hlayout0.addWidget(self.navitools) @@ -516,6 +527,16 @@ def enable_segmentedit(self): self.segmentcursor = "end" + def set_customheader(self): + # For now, mock the customheader. Eventually, the customheader will be + # collected in a pop-up dialog. + self._model.customheader["signalidx"] = 0 + self._model.customheader["markeridx"] = 0 + self._model.customheader["skiprows"] = 3 + self._model.customheader["sfreq"] = 250 + self._controller.get_fpaths() + + def get_xcursor(self, event): # event.button 1 corresponds to left mouse button if event.button != 1: From 948df0974100b41ba31dd77937dfbed9178d18c6 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sun, 12 Jul 2020 18:20:09 +0200 Subject: [PATCH 08/46] Implemented dialog asking the user for header infos in case of a custom file. --- biopeaks/io_utils.py | 9 +++--- biopeaks/model.py | 2 +- biopeaks/view.py | 75 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 2ae7b3c..ddd772c 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -18,7 +18,7 @@ def read_custom(rpath, customheader, channeltype): if channeltype == "signal": chanidx = customheader["signalidx"] - elif channeltype == "narker": + elif channeltype == "marker": chanidx = customheader["markeridx"] sfreq = customheader["sfreq"] @@ -26,10 +26,11 @@ def read_custom(rpath, customheader, channeltype): try: # If sep is None, the Python parsing engine can automatically detect the # separator usinf the builtin sniffer tool, csv.Sniffer. - signal = pd.read_csv(rpath, sep=None, usecols=[chanidx], header=None, - skiprows=customheader["skiprows"], engine="python") + signal = pd.read_csv(rpath, sep=customheader["separator"], + usecols=[chanidx], header=None, + skiprows=customheader["skiprows"]) except Exception as error: - output["error"] = error + output["error"] = str(error) return output signallen = signal.size diff --git a/biopeaks/model.py b/biopeaks/model.py index 90868a8..b9c7026 100644 --- a/biopeaks/model.py +++ b/biopeaks/model.py @@ -363,7 +363,7 @@ def __init__(self): self._savestats = {"period":False, "rate":False, "tidalamp":False} self._filetype = None self._customheader = {"signalidx": None, "markeridx": None, - "skiprows": None, "sfreq": None} + "skiprows": None, "sfreq": None, "separator": None} def reset(self): """ diff --git a/biopeaks/view.py b/biopeaks/view.py index 22d9550..ad86152 100644 --- a/biopeaks/view.py +++ b/biopeaks/view.py @@ -4,7 +4,7 @@ QVBoxLayout, QHBoxLayout, QCheckBox, QLabel, QStatusBar, QGroupBox, QDockWidget, QLineEdit, QFormLayout, QPushButton, - QProgressBar, QSplitter, QMenu) + QProgressBar, QSplitter, QMenu, QDialog) from PySide2.QtCore import Qt, QSignalMapper, QRegExp from PySide2.QtGui import QIcon, QRegExpValidator from matplotlib.figure import Figure @@ -216,6 +216,54 @@ def __init__(self, model, controller): self.segmenter.setAllowedAreas(Qt.RightDockWidgetArea) self.addDockWidget(Qt.RightDockWidgetArea, self.segmenter) + + # Set up dialog to gather user input for custom files. + + regex = QRegExp("[0-9]{2}") + validator = QRegExpValidator(regex) + + self.signallabel = QLabel("biosignal column") + self.signaledit = QLineEdit() + self.signaledit.setValidator(validator) + + self.markerlabel = QLabel("marker column") + self.markeredit = QLineEdit() + self.markeredit.setValidator(validator) + + regex = QRegExp("[0-9]{2}") + validator = QRegExpValidator(regex) + + self.headerrowslabel = QLabel("number of header rows") + self.headerrowsedit = QLineEdit() + self.headerrowsedit.setValidator(validator) + + regex = QRegExp("[0-9]{5}") + validator = QRegExpValidator(regex) + + self.sfreqlabel = QLabel("sampling rate") + self.sfreqedit = QLineEdit() + self.sfreqedit.setValidator(validator) + + self.separatorlabel = QLabel("column separator") + self.separatormenu = QComboBox(self) + self.separatormenu.addItem("comma") + self.separatormenu.addItem("tab") + self.separatormenu.addItem("colon") + self.separatormenu.addItem("space") + + self.continuecustomfile = QPushButton("continue loading file") + self.continuecustomfile.clicked.connect(self.set_customheader) + + self.customfiledialog = QDialog() + self.customfilelayout = QFormLayout() + self.customfilelayout.addRow(self.signallabel, self.signaledit) + self.customfilelayout.addRow(self.markerlabel, self.markeredit) + self.customfilelayout.addRow(self.separatorlabel, self.separatormenu) + self.customfilelayout.addRow(self.headerrowslabel, self.headerrowsedit) + self.customfilelayout.addRow(self.sfreqlabel, self.sfreqedit) + self.customfilelayout.addRow(self.continuecustomfile) + self.customfiledialog.setLayout(self.customfilelayout) + # set up menubar menubar = self.menuBar() @@ -233,8 +281,7 @@ def __init__(self, model, controller): openSignal.addAction(openOpenSignals) openCustom = QAction("Custom", self) openCustom.triggered.connect(lambda: self._model.set_filetype("Custom")) - openCustom.triggered.connect(self.set_customheader) - # openCustom.triggered.connect(self._controller.get_fpaths) + openCustom.triggered.connect(lambda: self.customfiledialog.exec_()) openSignal.addAction(openCustom) segmentSignal = QAction("select segment", self) @@ -528,12 +575,22 @@ def enable_segmentedit(self): def set_customheader(self): - # For now, mock the customheader. Eventually, the customheader will be - # collected in a pop-up dialog. - self._model.customheader["signalidx"] = 0 - self._model.customheader["markeridx"] = 0 - self._model.customheader["skiprows"] = 3 - self._model.customheader["sfreq"] = 250 + """Populate the customheader with inputs from the customfiledialog""" + seps = {"comma": ",", "tab": "\t", "colon": ":", "space": " "} + + self._model.customheader = dict.fromkeys(self._model.customheader, None) # reset header here since it cannot be reset in controller.get_fpaths() + + if self.signaledit.text(): + self._model.customheader["signalidx"] = int(self.signaledit.text()) + if self.markeredit.text(): + self._model.customheader["markeridx"] = int(self.markeredit.text()) + if self.headerrowsedit.text(): + self._model.customheader["skiprows"] = int(self.headerrowsedit.text()) + if self.sfreqedit.text(): + self._model.customheader["sfreq"] = int(self.sfreqedit.text()) + self._model.customheader["separator"] = seps[self.separatormenu.currentText()] + + self.customfiledialog.done(QDialog.Accepted) # close the dialog window self._controller.get_fpaths() From c77a2b930d7e847a8ce7829571da9ecce34cd1c8 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 13 Jul 2020 19:31:39 +0200 Subject: [PATCH 09/46] Load marker channel in case of custom file. --- biopeaks/controller.py | 4 ++-- biopeaks/io_utils.py | 17 +++++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index e9075d1..0d3be3b 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -287,7 +287,7 @@ def read_signal(self, path): self._model.rpathsignal = path # If requested, read marker channel. - if self._model.markerchan != "none": + if self._model.markerchan != "none" or self._model.customheader["markeridx"]: self.read_marker(path) @@ -306,7 +306,7 @@ def read_marker(self, path): self._model.status = output["error"] return - self._model.sfreqmarker = output["sfreq"] + self._model.sfreqmarker = output["sfreq"] # only not set to None in case of EDF self._model.marker = output["signal"] diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index ddd772c..807d443 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -21,8 +21,6 @@ def read_custom(rpath, customheader, channeltype): elif channeltype == "marker": chanidx = customheader["markeridx"] - sfreq = customheader["sfreq"] - try: # If sep is None, the Python parsing engine can automatically detect the # separator usinf the builtin sniffer tool, csv.Sniffer. @@ -33,10 +31,10 @@ def read_custom(rpath, customheader, channeltype): output["error"] = str(error) return output - signallen = signal.size - sec = np.linspace(0, signallen / sfreq, signallen) - if channeltype == "signal": + sfreq = customheader["sfreq"] + signallen = signal.size + sec = np.linspace(0, signallen / sfreq, signallen) output["sec"] = sec output["sfreq"] = sfreq @@ -96,10 +94,10 @@ def read_opensignals(rpath, channel, channeltype): # Load data with pandas for performance. signal = pd.read_csv(rpath, delimiter='\t', usecols=[chanidx], header=None, comment='#') - signallen = signal.size - sec = np.linspace(0, signallen / sfreq, signallen) if channeltype == "signal": + signallen = signal.size + sec = np.linspace(0, signallen / sfreq, signallen) output["sec"] = sec output["sfreq"] = sfreq @@ -160,13 +158,12 @@ def read_edf(rpath, channel, channeltype): if info["n_epochs"] == -1: info["n_epochs"] = int(np.rint(signal.size / sum(info["n_samples"]))) - chansignallen = info["n_epochs"] * info["n_samples"][chanidx - 1] chansfreq = info["sfreqs"][chanidx - 1] - sec = np.linspace(0, chansignallen / chansfreq, chansignallen) - chansignal = _read_edfchannel(signal, info["n_samples"], chanidx) if channeltype == "signal": + chansignallen = info["n_epochs"] * info["n_samples"][chanidx - 1] + sec = np.linspace(0, chansignallen / chansfreq, chansignallen) output["sec"] = sec output["signal"] = chansignal From 60272353c14ac75c68b0bae91d7600bec4d96c0c Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 20 Jul 2020 18:22:31 +0200 Subject: [PATCH 10/46] Started writing tests for custom files and account for one-based channel indices in custom files. --- biopeaks/controller.py | 4 +- biopeaks/io_utils.py | 2 +- biopeaks/model.py | 2 +- biopeaks/tests/test_gui.py | 77 +++++++++++++++++++++++++++++++++----- 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index 0d3be3b..dd0c73b 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -287,7 +287,7 @@ def read_signal(self, path): self._model.rpathsignal = path # If requested, read marker channel. - if self._model.markerchan != "none" or self._model.customheader["markeridx"]: + if self._model.customheader["markeridx"] != None or self._model.markerchan != "none": self.read_marker(path) @@ -341,7 +341,7 @@ def segment_signal(self): return if self._model.filetype == "EDF": sfreq = self._model.sfreqmarker - elif self._model.filetype == "OpenSignals": + else: sfreq = self._model.sfreq begsamp = int(np.rint(self._model.segment[0] * sfreq)) endsamp = int(np.rint(self._model.segment[1] * sfreq)) diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 807d443..57623db 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -25,7 +25,7 @@ def read_custom(rpath, customheader, channeltype): # If sep is None, the Python parsing engine can automatically detect the # separator usinf the builtin sniffer tool, csv.Sniffer. signal = pd.read_csv(rpath, sep=customheader["separator"], - usecols=[chanidx], header=None, + usecols=[chanidx - 1], header=None, # convert chanidx from one-based to zero-based skiprows=customheader["skiprows"]) except Exception as error: output["error"] = str(error) diff --git a/biopeaks/model.py b/biopeaks/model.py index b9c7026..3f45d66 100644 --- a/biopeaks/model.py +++ b/biopeaks/model.py @@ -363,7 +363,7 @@ def __init__(self): self._savestats = {"period":False, "rate":False, "tidalamp":False} self._filetype = None self._customheader = {"signalidx": None, "markeridx": None, - "skiprows": None, "sfreq": None, "separator": None} + "skiprows": None, "sfreq": None, "separator": None} def reset(self): """ diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 370c564..16d4dbd 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -60,6 +60,24 @@ def __init__(self, key, xdata): "segment": [20, 90], "filetype": "OpenSignals"} +ppg_custom = {"modality": "PPG", + "header": {"signalidx": 6, "markeridx": 1, "skiprows": 3, + "sfreq": 125, "separator": "\t"}, + "mode": "single file", + "sigpathorig": datadir.joinpath("OSmontagePPG.txt"), + "sigfnameseg": "testdata_segmented.txt", + "peakfname": "testdata_segmented_peaks.csv", + "statsfname": "testdata_segmented_stats.csv", + "siglen": 60001, + "siglenseg": 8750, + "markerlen": 60001, + "markerlenseg": 8750, + "peaksum": 461362, + "avgperiod": 0.6652, + "avgrate": 90.7158, + "segment": [20, 90], + "filetype": "Custom"} + ppg_edf = {"modality": "PPG", "sigchan": "A5", "markerchan": "A1", @@ -77,7 +95,7 @@ def __init__(self, key, xdata): "avgperiod": 1.0000, "avgrate": 60.0000, "segment": [11.51, 81.7], - "filetype": "EDF"} + "filetype": "EDF"} ecg_os = {"modality": "ECG", "sigchan": "A3", @@ -98,6 +116,24 @@ def __init__(self, key, xdata): "segment": [760, 860], "filetype": "OpenSignals"} +ecg_custom = {"modality": "ECG", + "header": {"signalidx": 7, "markeridx": 1, "skiprows": 3, + "sfreq": 1000, "separator": "\t"}, + "mode": "single file", + "sigpathorig": Path(datadir).joinpath("OSmontage0J.txt"), + "sigfnameseg": "testdata_segmented.txt", + "peakfname": "testdata_segmented_peaks.csv", + "statsfname": "testdata_segmented_stats.csv", + "siglen": 5100000, + "siglenseg": 100000, + "markerlen": 5100000, + "markerlenseg": 100000, + "peaksum": 4572190, + "avgperiod": 1.0921, + "avgrate": 55.1027, + "segment": [760, 860], + "filetype": "Custom"} + ecg_edf = {"modality": "ECG", "sigchan": "A3", "markerchan": "A1", @@ -137,6 +173,25 @@ def __init__(self, key, xdata): "segment": [3200, 3400], "filetype": "OpenSignals"} +rsp_custom = {"modality": "RESP", + "header" : {"signalidx": 6, "markeridx": 1, "skiprows": 3, + "sfreq": 1000, "separator": "\t"}, + "mode": "single file", + "sigpathorig": datadir.joinpath("OSmontage0J.txt"), + "sigfnameseg": "testdata_segmented.txt", + "peakfname": "testdata_segmented_peaks.csv", + "statsfname": "testdata_segmented_stats.csv", + "siglen": 5100000, + "siglenseg": 200000, + "markerlen": 5100000, + "markerlenseg": 200000, + "peaksum": 13355662, + "avgperiod": 3.2676, + "avgrate": 19.7336, + "avgtidalamp": 129.722, + "segment": [3200, 3400], + "filetype": "Custom"} + rsp_edf = {"modality": "RESP", "sigchan": "A5", "markerchan": "A1", @@ -161,14 +216,14 @@ def __init__(self, key, xdata): def idcfg_single(cfg): """Generate a test ID.""" modality = cfg["modality"] - fileformat = cfg["sigpathorig"].suffix - if fileformat == ".txt": - fileformat = ".os" + filetype = cfg["filetype"] - return f"{modality}{fileformat}" + return f"{modality}:{filetype}" -@pytest.fixture(params=[ppg_os, ppg_edf, ecg_os, ecg_edf, rsp_os, rsp_edf], +@pytest.fixture(params=[ppg_os, ppg_custom, ppg_edf, + ecg_os, ecg_custom, ecg_edf, + rsp_os, rsp_custom, rsp_edf], ids=idcfg_single) # automatically runs the test(s) using this fixture with all values of params def cfg_single(request): return request.param @@ -184,9 +239,12 @@ def test_singlefile(qtbot, tmpdir, cfg_single): view.show() # Configure options. + if cfg_single["filetype"] == "Custom": + model.customheader = cfg_single["header"] + else: + qtbot.keyClicks(view.sigchanmenu, cfg_single["sigchan"]) + qtbot.keyClicks(view.markerchanmenu, cfg_single["markerchan"]) qtbot.keyClicks(view.modmenu, cfg_single["modality"]) - qtbot.keyClicks(view.sigchanmenu, cfg_single["sigchan"]) - qtbot.keyClicks(view.markerchanmenu, cfg_single["markerchan"]) qtbot.keyClicks(view.batchmenu, cfg_single["mode"]) model.set_filetype(cfg_single["filetype"]) @@ -197,7 +255,8 @@ def test_singlefile(qtbot, tmpdir, cfg_single): assert np.size(model.signal) == cfg_single["siglen"] assert np.size(model.sec) == cfg_single["siglen"] assert np.size(model.marker) == cfg_single["markerlen"] - assert model.sfreq == cfg_single["sfreq"] + sfreq = cfg_single["header"]["sfreq"] if cfg_single["filetype"] == "Custom" else cfg_single["sfreq"] + assert model.sfreq == sfreq assert model.loaded # 2. segment signal ###################################################### From 92ff6d62a83881a3a98db8af9c8587e07940aebb Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 21 Jul 2020 17:45:21 +0200 Subject: [PATCH 11/46] Enforce one-based channel indexing for custom files. --- biopeaks/view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biopeaks/view.py b/biopeaks/view.py index ad86152..46bc423 100644 --- a/biopeaks/view.py +++ b/biopeaks/view.py @@ -219,7 +219,7 @@ def __init__(self, model, controller): # Set up dialog to gather user input for custom files. - regex = QRegExp("[0-9]{2}") + regex = QRegExp("[1-9][0-9]") validator = QRegExpValidator(regex) self.signallabel = QLabel("biosignal column") From fdeb621867508d12fd539add32b7de91afb18262 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 21 Jul 2020 19:04:26 +0200 Subject: [PATCH 12/46] Check for empty columns when loading custom files. --- biopeaks/io_utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 57623db..578fca8 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -31,6 +31,11 @@ def read_custom(rpath, customheader, channeltype): output["error"] = str(error) return output + if signal.empty: + output["error"] = (f"{channeltype.capitalize()}-column {chanidx} didn't" + " contain any data.") + return output + if channeltype == "signal": sfreq = customheader["sfreq"] signallen = signal.size From 3c98b3fafe030ba3602d7526f01ed971ee8a2098 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Thu, 23 Jul 2020 19:29:21 +0200 Subject: [PATCH 13/46] Simplify reading of channels (biosignal, markers). --- biopeaks/controller.py | 90 +++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 46 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index dd0c73b..14bba34 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -2,7 +2,8 @@ from .heart import ecg_peaks, ppg_peaks, correct_peaks, heart_period from .resp import resp_extrema, resp_stats -from .io_utils import read_custom, read_opensignals, read_edf, write_opensignals, write_edf +from .io_utils import (read_custom, read_opensignals, read_edf, + write_custom, write_opensignals, write_edf) from pathlib import Path import pandas as pd import numpy as np @@ -13,10 +14,15 @@ getOpenFileNames = QFileDialog.getOpenFileNames getSaveFileName = QFileDialog.getSaveFileName getExistingDirectory = QFileDialog.getExistingDirectory + peakfuncs = {"ECG": ecg_peaks, "PPG": ppg_peaks, "RESP": resp_extrema} +readfuncs = {"Custom": read_custom, + "OpenSignals": read_opensignals, + "EDF": read_edf} + # threading is implemented according to https://pythonguis.com/courses/ # multithreading-pyqt-applications-qthreadpool/complete-example/ class WorkerSignals(QObject): @@ -63,8 +69,7 @@ def get_fpaths(self): self._model.fpaths = getOpenFileNames(None, 'Choose your data', "\\home")[0] if not self._model.fpaths: - if self._model.filetype == "Custom": - self._model.customheader = dict.fromkeys(self._model.customheader, None) # for custom files also reset header + self._model.customheader = dict.fromkeys(self._model.customheader, None) # for custom files also reset header self._model.set_filetype(None) # reset file type return if (self._model.batchmode == 'multiple files' and @@ -73,7 +78,7 @@ def get_fpaths(self): elif (self._model.batchmode == 'single file' and len(self._model.fpaths) == 1): self._model.reset() - self.read_signal(path=self._model.fpaths[0]) + self.read_channels(path=self._model.fpaths[0]) def get_wpathsignal(self): @@ -224,7 +229,7 @@ def dispatcher(self, progress): if self.methodnb == 0: self.methodnb += 1 self._model.reset() - self.read_signal(path=fpath) + self.read_channels(path=fpath) elif self.methodnb == 1: self.methodnb += 1 self.find_peaks() @@ -256,58 +261,45 @@ def dispatcher(self, progress): @threaded - def read_signal(self, path): + def read_channels(self, path): self._model.status = "Loading file." - file_type = self._model.filetype - - if file_type == "OpenSignals": - output = read_opensignals(path, self._model.signalchan, - channeltype="signal") - elif file_type == "EDF": - output = read_edf(path, self._model.signalchan, - channeltype="signal") - elif file_type == "Custom": - output = read_custom(path, self._model.customheader, - channeltype="signal") - - if output["error"]: - self._model.status = output["error"] - if self._model.filetype == "Custom": - self._model.customheader = dict.fromkeys(self._model.customheader, None) # for custom files also reset header + + filetype = self._model.filetype + readfunc = readfuncs[filetype] + + biosignalinfo = (self._model.customheader if filetype == "Custom" + else self._model.signalchan) + biosignal = readfunc(path, biosignalinfo, channeltype="signal") + + if biosignal["error"]: + self._model.status = biosignal["error"] + self._model.customheader = dict.fromkeys(self._model.customheader, None) # for custom files also reset header self._model.set_filetype(None) # reset file type return # Important to set seconds PRIOR TO signal, otherwise plotting # behaves unexpectadly (since plotting is triggered as soon as # signal changes). - self._model.sfreq = output["sfreq"] # in case of custom file, sfreq is now taken over from customheader - self._model.sec = output["sec"] - self._model.signal = output["signal"] + self._model.sfreq = biosignal["sfreq"] # in case of custom file, sfreq is now taken over from customheader + self._model.sec = biosignal["sec"] + self._model.signal = biosignal["signal"] self._model.loaded = True self._model.rpathsignal = path - # If requested, read marker channel. - if self._model.customheader["markeridx"] != None or self._model.markerchan != "none": - self.read_marker(path) - - - def read_marker(self, path): - if self._model.filetype == "OpenSignals": - output = read_opensignals(path, self._model.markerchan, - channeltype="marker") - elif self._model.filetype == "EDF": - output = read_edf(path, self._model.markerchan, - channeltype="marker") - elif self._model.filetype == "Custom": - output = read_custom(path, self._model.customheader, - channeltype="marker") + markerinfo = (self._model.customheader if filetype == "Custom" + else self._model.markerchan) + if filetype == "Custom" and markerinfo["markeridx"] is None: + return + if markerinfo == "none": + return + marker = readfunc(path, markerinfo, channeltype="marker") - if output["error"]: - self._model.status = output["error"] + if marker["error"]: + self._model.status = marker["error"] return - self._model.sfreqmarker = output["sfreq"] # only not set to None in case of EDF - self._model.marker = output["signal"] + self._model.sfreqmarker = marker["sfreq"] # only not set to None in case of EDF + self._model.marker = marker["signal"] @threaded @@ -361,10 +353,16 @@ def save_signal(self): if self._model.filetype == "OpenSignals": begsamp = int(np.rint(self._model.segment[0] * self._model.sfreq)) endsamp = int(np.rint(self._model.segment[1] * self._model.sfreq)) - write_opensignals(self._model.rpathsignal, - self._model.wpathsignal, + write_opensignals(self._model.rpathsignal, self._model.wpathsignal, segment=[begsamp, endsamp]) + elif self._model.filetype == "Custom": + begsamp = int(np.rint(self._model.segment[0] * self._model.sfreq)) + endsamp = int(np.rint(self._model.segment[1] * self._model.sfreq)) + write_custom(self._model.rpathsignal, self._model.wpathsignal, + segment=[begsamp, endsamp], + customheader=self._model.customheader) + elif self._model.filetype == "EDF": status = write_edf(self._model.rpathsignal, self._model.wpathsignal, From cfea98de3cbe416e4c0ad3f0f6d85413fe8b2d93 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Thu, 23 Jul 2020 19:30:09 +0200 Subject: [PATCH 14/46] Implemented write_custom(). --- biopeaks/io_utils.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 578fca8..33311f2 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -22,8 +22,6 @@ def read_custom(rpath, customheader, channeltype): chanidx = customheader["markeridx"] try: - # If sep is None, the Python parsing engine can automatically detect the - # separator usinf the builtin sniffer tool, csv.Sniffer. signal = pd.read_csv(rpath, sep=customheader["separator"], usecols=[chanidx - 1], header=None, # convert chanidx from one-based to zero-based skiprows=customheader["skiprows"]) @@ -49,7 +47,25 @@ def read_custom(rpath, customheader, channeltype): def write_custom(rpath, wpath, segment, customheader): - pass + """ + segment : list + Start and end of segments in samples. + """ + # Get the header. + with open(rpath, "rt") as oldfile: + header = [line for line in islice(oldfile, customheader["skiprows"])] + # Get the data. + data = pd.read_csv(rpath, sep=customheader["separator"], header=None, + skiprows=customheader["skiprows"]) + # Apply segmentation to all channels. + data = data.iloc[segment[0]:segment[1], :] + + # Write header and segmented data to the new file. + with open(wpath, "w", newline='') as newfile: + for line in header: + newfile.write(line) + data.to_csv(newfile, sep=customheader["separator"], header=False, + index=False) def read_opensignals(rpath, channel, channeltype): @@ -97,7 +113,7 @@ def read_opensignals(rpath, channel, channeltype): chanidx = int(channel[1]) # Load data with pandas for performance. - signal = pd.read_csv(rpath, delimiter='\t', usecols=[chanidx], header=None, + signal = pd.read_csv(rpath, sep='\t', usecols=[chanidx], header=None, comment='#') if channeltype == "signal": @@ -120,7 +136,7 @@ def write_opensignals(rpath, wpath, segment): with open(rpath, "rt") as oldfile: header = [line for line in islice(oldfile, 3)] # Get the data. - data = pd.read_csv(rpath, delimiter='\t', header=None, comment='#') + data = pd.read_csv(rpath, sep='\t', header=None, comment='#') # Apply segmentation to all channels. data = data.iloc[segment[0]:segment[1], :] From b95a6d9342842d62c9dbc0273c3d4fd7087d04f5 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Thu, 23 Jul 2020 19:30:56 +0200 Subject: [PATCH 15/46] Enforce mandatory fields of custom header. --- biopeaks/view.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/biopeaks/view.py b/biopeaks/view.py index 46bc423..9c1ceac 100644 --- a/biopeaks/view.py +++ b/biopeaks/view.py @@ -576,22 +576,28 @@ def enable_segmentedit(self): def set_customheader(self): """Populate the customheader with inputs from the customfiledialog""" - seps = {"comma": ",", "tab": "\t", "colon": ":", "space": " "} + # Check if one of the mandatory fields is missing. + mandatoryfields = self.signaledit.text() and self.headerrowsedit.text() and self.sfreqedit.text() + + if not mandatoryfields: + self._model.status = ("Please provide values for 'biosignal column'" + ", 'number of header rows' and 'sampling" + " rate'.") + return + + seps = {"comma": ",", "tab": "\t", "colon": ":", "space": " "} self._model.customheader = dict.fromkeys(self._model.customheader, None) # reset header here since it cannot be reset in controller.get_fpaths() - if self.signaledit.text(): - self._model.customheader["signalidx"] = int(self.signaledit.text()) - if self.markeredit.text(): - self._model.customheader["markeridx"] = int(self.markeredit.text()) - if self.headerrowsedit.text(): - self._model.customheader["skiprows"] = int(self.headerrowsedit.text()) - if self.sfreqedit.text(): - self._model.customheader["sfreq"] = int(self.sfreqedit.text()) + self._model.customheader["signalidx"] = int(self.signaledit.text()) + self._model.customheader["skiprows"] = int(self.headerrowsedit.text()) + self._model.customheader["sfreq"] = int(self.sfreqedit.text()) self._model.customheader["separator"] = seps[self.separatormenu.currentText()] + if self.markeredit.text(): # not mandatory + self._model.customheader["markeridx"] = int(self.markeredit.text()) self.customfiledialog.done(QDialog.Accepted) # close the dialog window - self._controller.get_fpaths() + self._controller.get_fpaths() # move on to file selection def get_xcursor(self, event): From 14a83d4bcf350336ceecd75622957a8f988bd609 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Thu, 23 Jul 2020 19:31:42 +0200 Subject: [PATCH 16/46] Reload segmented signal in test_singlefile(). --- biopeaks/tests/test_gui.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 16d4dbd..272e63a 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -251,7 +251,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): # 1. load signal ######################################################### with qtbot.waitSignals([model.signal_changed, model.marker_changed], timeout=10000): - controller.read_signal(path=cfg_single["sigpathorig"]) + controller.read_channels(path=cfg_single["sigpathorig"]) assert np.size(model.signal) == cfg_single["siglen"] assert np.size(model.sec) == cfg_single["siglen"] assert np.size(model.marker) == cfg_single["markerlen"] @@ -305,7 +305,21 @@ def test_singlefile(qtbot, tmpdir, cfg_single): with qtbot.waitSignals([model.progress_changed] * 2, timeout=10000): controller.save_peaks() - # 7. load peaks ########################################################### + # 7. re-load signal ####################################################### + with qtbot.waitSignals([model.signal_changed, model.marker_changed], + timeout=10000): + controller.read_channels(path=tmpdir.join(cfg_single["sigfnameseg"])) + sfreq = cfg_single["header"]["sfreq"] if cfg_single["filetype"] == "Custom" else cfg_single["sfreq"] + assert model.sfreq == sfreq + assert model.loaded + # Increase tolerance to 38, since for EDF files data needs to be saved as + # epochs of fixed size which can lead to deviations from original segment + # length. + assert np.allclose(np.size(model.signal), seg, atol=38) + assert np.allclose(np.size(model.sec), seg, atol=38) + assert np.size(model.signal) == np.size(model.sec) + + # 8. load peaks ########################################################### model.rpathpeaks = tmpdir.join(cfg_single["peakfname"]) with qtbot.waitSignal(model.peaks_changed, timeout=5000): controller.read_peaks() @@ -314,7 +328,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): # way extrema are added and deleted in controller.edit_peaks(). assert np.allclose(sum(model.peaks), cfg_single["peaksum"], atol=10) - # 8. calculate stats ###################################################### + # 9. calculate stats ###################################################### signals = ([model.period_changed, model.rate_changed, model.tidalamp_changed] if model.modality == "RESP" else [model.period_changed, model.rate_changed]) @@ -326,7 +340,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): assert np.around(np.mean(model.tidalampintp), 4) == cfg_single["avgtidalamp"] - # 9. save stats ########################################################### + # 10. save stats ########################################################## view.periodcheckbox.setCheckState(Qt.Checked) view.ratecheckbox.setCheckState(Qt.Checked) if model.modality == "RESP": @@ -434,7 +448,7 @@ def test_batchfile(qtbot, tmpdir, cfg_batch): for sigfname, peaksum in zip(cfg_batch["sigfnames"], cfg_batch["peaksums"]): with qtbot.waitSignal(model.signal_changed, timeout=5000): - controller.read_signal(path=datadir.joinpath(sigfname)) + controller.read_channels(path=datadir.joinpath(sigfname)) fname = Path(sigfname).stem model.rpathpeaks = tmpdir.join(f"{fname}_peaks.csv") with qtbot.waitSignal(model.peaks_changed, timeout=5000): From e927fec6f45432f10348210422446801e6266682 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Fri, 24 Jul 2020 11:57:49 +0200 Subject: [PATCH 17/46] Simplify saving of segmented channels. --- biopeaks/controller.py | 36 ++++++++++++++++-------------------- biopeaks/io_utils.py | 18 ++++++++++-------- biopeaks/tests/test_gui.py | 2 +- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index 14bba34..dc8dc1f 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -23,6 +23,10 @@ "OpenSignals": read_opensignals, "EDF": read_edf} +writefuncs = {"Custom": write_custom, + "OpenSignals": write_opensignals, + "EDF": write_edf} + # threading is implemented according to https://pythonguis.com/courses/ # multithreading-pyqt-applications-qthreadpool/complete-example/ class WorkerSignals(QObject): @@ -96,7 +100,7 @@ def get_wpathsignal(self): "untitled", filefilter)[0] if self._model.wpathsignal: - self.save_signal() + self.save_channels() def get_rpathpeaks(self): @@ -343,32 +347,24 @@ def segment_signal(self): " marker channel to resolve this segment." @threaded - def save_signal(self): + def save_channels(self): self._model.status = "Saving signal." if self._model.segment is None: self._model.status = "Error: Cannot save non-segmented file." return - if self._model.filetype == "OpenSignals": - begsamp = int(np.rint(self._model.segment[0] * self._model.sfreq)) - endsamp = int(np.rint(self._model.segment[1] * self._model.sfreq)) - write_opensignals(self._model.rpathsignal, self._model.wpathsignal, - segment=[begsamp, endsamp]) - - elif self._model.filetype == "Custom": - begsamp = int(np.rint(self._model.segment[0] * self._model.sfreq)) - endsamp = int(np.rint(self._model.segment[1] * self._model.sfreq)) - write_custom(self._model.rpathsignal, self._model.wpathsignal, - segment=[begsamp, endsamp], - customheader=self._model.customheader) + filetype = self._model.filetype + writefunc = writefuncs[filetype] - elif self._model.filetype == "EDF": - status = write_edf(self._model.rpathsignal, - self._model.wpathsignal, - segment=self._model.segment) - if status: - self._model.status = status + headerinfo = (self._model.customheader if filetype == "Custom" + else self._model.sfreq) + + status = writefunc(self._model.rpathsignal, self._model.wpathsignal, + self._model.segment, headerinfo) # only write_edf() returns status, other write functions return None (no return) + + if status: + self._model.status = status @threaded def read_peaks(self): diff --git a/biopeaks/io_utils.py b/biopeaks/io_utils.py index 33311f2..2e49888 100644 --- a/biopeaks/io_utils.py +++ b/biopeaks/io_utils.py @@ -49,7 +49,7 @@ def read_custom(rpath, customheader, channeltype): def write_custom(rpath, wpath, segment, customheader): """ segment : list - Start and end of segments in samples. + Start and end of segments in seconds. """ # Get the header. with open(rpath, "rt") as oldfile: @@ -58,7 +58,9 @@ def write_custom(rpath, wpath, segment, customheader): data = pd.read_csv(rpath, sep=customheader["separator"], header=None, skiprows=customheader["skiprows"]) # Apply segmentation to all channels. - data = data.iloc[segment[0]:segment[1], :] + begsamp = int(np.rint(segment[0] * customheader["sfreq"])) + endsamp = int(np.rint(segment[1] * customheader["sfreq"])) + data = data.iloc[begsamp:endsamp, :] # Write header and segmented data to the new file. with open(wpath, "w", newline='') as newfile: @@ -127,10 +129,10 @@ def read_opensignals(rpath, channel, channeltype): return output -def write_opensignals(rpath, wpath, segment): +def write_opensignals(rpath, wpath, segment, sfreq): """ segment : list - Start and end of segments in samples. + Start and end of segments in seconds. """ # Get the header. with open(rpath, "rt") as oldfile: @@ -138,7 +140,9 @@ def write_opensignals(rpath, wpath, segment): # Get the data. data = pd.read_csv(rpath, sep='\t', header=None, comment='#') # Apply segmentation to all channels. - data = data.iloc[segment[0]:segment[1], :] + begsamp = int(np.rint(segment[0] * sfreq)) + endsamp = int(np.rint(segment[1] * sfreq)) + data = data.iloc[begsamp:endsamp, :] # Write header and segmented data to the new file. with open(wpath, "w", newline='') as newfile: @@ -193,13 +197,11 @@ def read_edf(rpath, channel, channeltype): return output -def write_edf(rpath, wpath, segment): +def write_edf(rpath, wpath, segment, *args): """ segment : list Start and end of segments in seconds. """ - error = False - with open(rpath, "rb") as f: info, header = _read_edfheader(f) signal = _read_edfsignal(f, info["end_header"]) diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 272e63a..4033d42 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -273,7 +273,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): # 3. save segment ######################################################## model.wpathsignal = tmpdir.join(cfg_single["sigfnameseg"]) with qtbot.waitSignals([model.progress_changed] * 2, timeout=10000): - controller.save_signal() + controller.save_channels() # 4. find extrema ######################################################### with qtbot.waitSignal(model.peaks_changed, timeout=5000): From c89befb6b94e020585476b114d6f201133334e3d Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Fri, 24 Jul 2020 16:35:46 +0200 Subject: [PATCH 18/46] Cleaned up batch processing a bit. --- biopeaks/controller.py | 40 ++++++++++++++++---------------------- biopeaks/tests/test_gui.py | 2 +- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index dc8dc1f..a165edd 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -195,15 +195,17 @@ def batch_processor(self): is displayed. TODO: make method calls and their order more explicit! ''' + self.get_wpathpeaks() + self.get_wpathstats() - self.methodnb = 0 + self.methodnb = -1 self.nmethods = 5 + if self._model.wdirstats is None: + self._model.status = "No statistics selected for saving." + return self.filenb = 0 self.nfiles = len(self._model.fpaths) - self.get_wpathpeaks() - self.get_wpathstats() - self._model.status = "Processing files." self._model.plotting = False self._model.progress_changed.connect(self.dispatcher) @@ -219,10 +221,9 @@ def dispatcher(self, progress): method. Once all methods are executed, go to the next file and start cycling through methods again. ''' - if progress == 0: + if not progress: return - if self.filenb == self.nfiles: - # this condition works because filenb starts at 0 + if self.filenb == self.nfiles: # works because filenb starts at 0 self._model.plotting = True self._model.progress_changed.disconnect(self.dispatcher) self._model.wdirpeaks = None @@ -230,36 +231,29 @@ def dispatcher(self, progress): return fpath = self._model.fpaths[self.filenb] fname = Path(fpath).stem + self.methodnb += 1 if self.methodnb == 0: - self.methodnb += 1 self._model.reset() self.read_channels(path=fpath) elif self.methodnb == 1: - self.methodnb += 1 self.find_peaks() elif self.methodnb == 2: - self.methodnb += 1 self.autocorrect_peaks() elif self.methodnb == 3: - self.methodnb += 1 self.calculate_stats() elif self.methodnb == 4: - self.methodnb += 1 - if self._model.wdirpeaks: - p = Path(self._model.wdirpeaks).joinpath(f"{fname}_peaks.csv") - self._model.wpathpeaks = p - self.save_peaks() - else: - self.dispatcher(1) + p = Path(self._model.wdirstats).joinpath(f"{fname}_stats.csv") + self._model.wpathstats = p + self.save_stats() elif self.methodnb == self.nmethods: # once all methods are executed, move to next file and start with # first method again - self.methodnb = 0 + self.methodnb = -1 self.filenb += 1 - if self._model.wdirstats: - p = Path(self._model.wdirstats).joinpath(f"{fname}_stats.csv") - self._model.wpathstats = p - self.save_stats() + if self._model.wdirpeaks: # optional + p = Path(self._model.wdirpeaks).joinpath(f"{fname}_peaks.csv") + self._model.wpathpeaks = p + self.save_peaks() else: self.dispatcher(1) diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 4033d42..8fb5ef4 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -424,7 +424,7 @@ def test_batchfile(qtbot, tmpdir, cfg_batch): # Mock the controller's batch_processor in order to avoid # calls to the controller's get_wpathpeaks and get_wpathstats methods. - controller.methodnb = 0 + controller.methodnb = -1 controller.nmethods = 5 controller.filenb = 0 controller.nfiles = len(model.fpaths) From 04a355945f54ed8fd6a04d38c1310dce7df5aa22 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 11:31:46 +0200 Subject: [PATCH 19/46] Changed title and icon of customfiledialog. --- biopeaks.qrc | 2 +- biopeaks/images/file_icon.png | Bin 0 -> 329 bytes biopeaks/resources.py | 1366 +++------------------------------ biopeaks/view.py | 3 + 4 files changed, 99 insertions(+), 1272 deletions(-) create mode 100644 biopeaks/images/file_icon.png diff --git a/biopeaks.qrc b/biopeaks.qrc index 470b026..7013222 100644 --- a/biopeaks.qrc +++ b/biopeaks.qrc @@ -2,6 +2,6 @@ biopeaks/images/python_icon.png biopeaks/images/mouse_icon.png - biopeaks/images/logo.png + biopeaks/images/file_icon.png \ No newline at end of file diff --git a/biopeaks/images/file_icon.png b/biopeaks/images/file_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3acf268433ec60cf0f7e2cf118f2a162c347c376 GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEY)RhkE)4%caKYZ?lYt`tJY5_^ zB3hFZBv_{~y2S{+lRf@F__R=2*s{&DyUzNqNR?w|WyCh?&z?YJsaBrf{mja(-JbPRfl$n{E1@$~<;S?o!|jM-@pljJ7JdaPod(lk-_=o1%B zD~aADuY(zS%S3wKXvm#C+RWJV3@EUm|4|@&5EsZ4iBg574EdHF>z!@6+JHf2wPZ$H UbsIw?FeDf}UHx3vIVCg!0Kd$D6aWAK literal 0 HcmV?d00001 diff --git a/biopeaks/resources.py b/biopeaks/resources.py index 9bd6b90..633139a 100644 --- a/biopeaks/resources.py +++ b/biopeaks/resources.py @@ -2,7 +2,7 @@ # Resource object code # -# Created: Mo Apr 27 19:37:56 2020 +# Created: Fr Jul 24 20:49:47 2020 # by: The Resource Compiler for PySide2 (Qt v5.12.5) # # WARNING! All changes made in this file will be lost! @@ -10,72 +10,29 @@ from PySide2 import QtCore qt_resource_data = b"\ -\x00\x00\x03\xf8\ +\x00\x00\x01I\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ \x00\x00(\x00\x00\x00(\x08\x06\x00\x00\x00\x8c\xfe\xb8m\ -\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ -\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00\ -\x09pHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\ -\xa8d\x00\x00\x03\x8dIDATXG\xed\x98\xcdO\ -\x13A\x18\xc6kTb\xd4\xc4\x8b\x07O&\x9e\xfc\x03\ -\xf4\xe2E/\xb4\x0b\xed\xb6\x02\x0aF\x8cp3\xea\xc1\ -\xb3'\x89\x89\xb2]v\x0b\xad\x10\xd3\x92\xa8\x07\xf1 \ -\x89\x22\x891\x0a&F\x81\xfdb\xa3P\x08\xf6`$\ -\x98P\xeaGB!m\xc2\x87\xed8\xef2%\xa5\x19\ -\x92\xeen\xf9H\xec\x93<\x97\xd9\xdd\xf7\xf9ef\xe7\ -\xddv\x1c\xc5\xea\x5c\xcb\x87\x03\x8c_j`8\xf9\x99\ -\x8b\x93c\x0c'\xa5\x5c\x9c\xf4\x9a\x5c\xde9a\xb0}\ -U\xbc|\xd3\xe5\x97\x7f1~\x19\xe5{\xc7\x01YQ\ -?\x8a!\x86\x0c N\xca\x02\x10v#\xc3\x0f\x9ft\ -\x0ac\x87\xc8m;\xa3J\xbf~\x04\x83}]\x83\x93\ -g\x98\xd6\xe13\xe4\xd2.\x10B{\xf0L\xf5\xaf-\ -\xa54\xe6nS\x8f\x91+\xeb\xaao\x99<\xccp#\ -\x1d\xac\xa8%<\xa2\x96\xa9\xe6\x95\x0d\xcb\x0f\xaen\x93\ -\x97\xcf\x07\xb4xM\xfb(\xae%5\xe0g*\xc8\xe3\ -\xf6\xc4p\x8a\x07\x02\xe0\xbd\xab\xf4\x0f\x1d'\xc3\xebb\ -\xda\xd4v\xb7\xa0f\xae\x84\xa3\xd9\xdb\xfd\x09$~J\ -\xa1.u\x15\x85\xf5\x8c\xe1\xe9d\xd6p\xec\xcf_$\ -O\xa7\xd1S9\x81n<\x99X\xf0\x05\xd48\xde`\ -\x17H\x19\xeb\xc2K\xaa\x1a\x80\xad\xf2%2d\xa8\xbe\ -\xb7\xb7\x82\x15\xd4\xb1\x9a\x0e\x1d\xdd\x1b\x9c_\x07*t\ -\x0e\xb0\xd0\xef\xa6\x92\xa8\xa1SOyE%\x84k\xed\ -%e\xcd\xa9\x92WO\x1bp\x9c4\x09KM\x86\x0d\ -yDu\xfc\xf2\xc3(\xea\x94\x97\xa9`\xb7\x9e\xcf\xe0\ -eU\x90[PPh\xe0\x07\x15r\xf2\xe7\x0a\xba\xf6\ -(\x8a!\xb5\x10)kN\x18\xec\x0ey\x87\xee\x92!\ -C\xb0\xac0s\x9d\xca\x0a\x15\x0e\x0cp\xe4Y\x03\x92\ -\x06\x08\x06H\x98I\x17?RC\xca\x17/\x5c\xfc#\ -\x048\xb9\x91\xb3d\xc8\xd8\x10\x1eA\xc9\xdc\x7f\x9f\xa4\ -\x82\xe5\x9c\x83\xcb\x99\x06\x97\xf3[\xbc\xdc>Q\x9b;\ -\x15\xd1\xf7\x93\x98\xe2\x84\xfb\xddw\x03PPN\x90!\ -\x87\xabU\x096\x86'\xb24\xa8|\x9b\x01\x04_\x7f\ -<\xbeX\xc5I\xf5$\xa68\xb9\xfcR\x1a\x8a\xb3-\ -\xfaA2\x84\x1b\xf6h\x02v+\x0d*\xdff\x01a\ -w\xd7\x06\xb4>\x12S\x9c\xc8\xd7b\xc3'\x0co\x8e\ -\x0c\xb4\x12\x1aT\xbe\xcd\x02J\xb8\x05A\xeb!1\xd6\ -\x05M\xb8K\xdd|s\xe4l\x160\xf6{\x15\xb9y\ -e\x89\xc4X\x17\x84\xd1\x80\x0am\x16\x10\x0c\xf7\x91\x18\ -\xeb\x82\x224\xa0B\x17\xdbf\xf2\xbd\xad\x80\xc54\xea\ -Bo+`\xce4\x90\xcd\xfc\xff\x00n\xa5I\x8cu\ -A\x917\xdf\xb2[\xe22\xa0]\x97\x01\xed\xba\x0ch\ -\xd7e@\xbb.\x09 \xfe=\xb8\xd4\x1f[\xa5\x06\xd8\ -1\xd4\x84\xda$\xc6\xba\xbc\xa2\x1a\xef\xf9\x9c\xa6\x86\xd8\ -q\xcf\x974bEm\x96\xc4X\x97/\xa0\xbd\x12\x06\ -\x13\xd4\x10;\xe6\x07\x12\xc8+\x98\xfcOB\x13\x9c\xab\ -4uO,\xd2B\xec\xb8\xb9;\xba\xe0\xf4+\x17I\ -\x8cu\xc1\xa1\x0f^\x8a\xb9n9I\x0d\xb2\xe2\x884\ -\x8fXA\x8d\x9b\xfe_\xbc\x99\xf0n\xab\xab\x0b\xe9\xa9\ -\xbe\xa9\x15j\xa0\x19\xbf\xc05\xea\x82z\x8a\xe1e\x1f\ -)_\x1a\xb1\x82\xf2\xa0)\x12M\xbd\xb4\x01\x09pW\ -\xc3\xd1\x14\x9e\xbd )[:\xc1\x89\x14\x1bP\x830\ -\x93V\x96\x1b\x9e\xa9\xc53\xe7\x16\xd4\x0e\xcb\xa7[\xc5\ -\x08\x0e}X\xdcz\xe0%\x87\x9d\x08-\x88\xd6'a\ -\x0c\xae\xc1=\xcd\x91\xe8\x02\xbcsNN\xf2\x922[\ -+\xd88p\xae\x82[P\x9f7\xa0\xcdV\xf1\xf22\ -|\x15\xf2\x0dcp\x0dZ\x09\xdc[\xb2\x13\xd6\xdd#\ -\x87\xe3\x1f?I\x97\x1f>\xc9\xe5\xcd\x00\x00\x00\x00I\ -END\xaeB`\x82\ +\x00\x00\x00\x06bKGD\x00\xff\x00\xff\x00\xff\xa0\xbd\ +\xa7\x93\x00\x00\x00\xfeIDATX\x85c`\x18\x05\ +\x94\x01F\x5c\x12\xee\x1d\xc7\xffS\xcb\x12vV\xa6\xb3\ +\x9b\x8a\xcdM\xa8e\x1e\x03\x03\x03\xc4\x81\xd4\x00\xee\x1d\ +\xc7\xff\xc7\xcc\xb8\xf0\xcf\xbf\xf7\xd4Ir\xdc\xc1DU\ +_\xe1\x00\xe5\xfe\x9a\x8c\xbc\x5cl\xa6\xbe\xbd\xa7\xce\x92\ +\xaa\x97.\x0e\xe4\xe1de(\x0f\xd0d\xe4\xe7b3\ +$\xd5\x91tq \x03\x03\xf9\x8e\xa4\x9b\x03\x19\x18\xc8\ +s$]\x1d\xc8\xc0@\xba#\xe9\xee@\x06\x06\xd2\x1c\ +9 \x0ed` \xde\x91,\xf4pL\xc6\x0c\xbcE\ + #\x03\x03\x83\x11.I\x9a;pG\xb9\x05A5\ +\x1e\x9d'p\xca\x0dX\x14\x13\x0bF\x1dH) 9\ +\x0d&\xcf\xba\xc0\xf0\xf4\xfd\x0f\xb2,\x93\x16\xe4`\x98\ +\x9bf@\x92\x1e\x92\x1dH\xaa\x05\x94\x82\x91\x1d\xc5\xe4\ +D):\x18\x8dbJ\xc1h.\xa6\x14\x8c\xec(\x1e\ +\xcd\xc5\x83\x01\x8c\xe6bJ\xc1\xa0\x8f\xe2Q\x07R\x0a\ +F\x1dH)\x18u \xa5\x00o9\x88\xafC=\x0a\ +\x86\x0a\x00\x00:\xa4\x98\x86{\x86\x00\x81\x00\x00\x00\x00\ +IEND\xaeB`\x82\ \x00\x00\x03\xef\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -141,1227 +98,94 @@ \xf3^f\x01\x9e\x81\xef\xf1\x9b\x97\xa5\xebv\xd9\xe3D\ \x1c\x84\x22E\x8a\xb4\xeeE\xc8_\xbeM\x86Tx\x85\ \x84\xcc\x00\x00\x00\x00IEND\xaeB`\x82\ -\x00\x00J\xc7\ +\x00\x00\x03\xf8\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ -\x00\x03\x9f\x00\x00\x00\xa3\x08\x06\x00\x00\x00\x0dA\xaa7\ -\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\ -\x00\x00\x00\x09pHYs\x00\x00\x0e\xc4\x00\x00\x0e\xc4\ -\x01\x95+\x0e\x1b\x00\x00\x00\x19tEXtSof\ -tware\x00www.inksca\ -pe.org\x9b\xee<\x1a\x00\x00 \x00ID\ -ATx\x9c\xec\xddy|T\xf5\xbd\xff\xf1\xd7\xe7L\ -\x12\x08\x09ZDp\xab\x82\x0a\x8a\xa0\xd6\x0d\xad{\x10\ -\x84LX\x5cjb\xdd \x13P\xda\xaa\xa8\xdd\xf4w\ -{\xefmz\xefmk7[\xb1\xdaF!\x13pO\ -\xea\x06\x92Ip\x01\x15w\xb1\xd6\x15\x10\xad\xa8u\xc5\ -\x0d\x12\x96d\xe6|~\x7fLP\xaa $s\xce|\ -\xe7\x9c\xf9>\x1f\x8f\xd4\x02s\xce\xf7\x8dN2\xe7s\ -\xce\xf7\xfb\xf9\x0a\x96eY\x96eYV\xcex`\xbf\ -\x99G\x8a\xa3\xdf\x15\xe48\x94\xa1\x08;\x02\x1f\x0b,\ -W\xf4a\x84\x9bG\xbdr\xc9\x8b\xa6sZ\x96eu\ -\x97\x98\x0e`Y\x96eY\x96e\xc1C\xc3\xae9\xc8\ -E\xaf\x06Fm\xe3\xa5*0?\xe9\xa6.\x1b\xb3\xe2\ -\xb2\xd7\xb3\x91\xcd\xb2,\xcb\x0b\xb6\xf8\xb4,\xcb\xb2,\ -\xcb2l\xd1\xfe\xd7|\x0f\xd1?\x01\xbd\xbaqX\x9b\ -\xa8\xd4\x94-\xbf\xb8\xc9\xaf\x5c\x96eY^\x12&\xc4\ -\x87\xa2\xb2\x83\xe9 \x9eII'\x91T\xdb\xbf\xfd\x9e\ -D6\xa2\x05\xeb\x00\xe8Lu\xb0pr\xbb\x89h\x96\ -\x95U\x95\x8dEl\xf8d\x07\xdc\xc2\xf4\xf7wR\xfb\ -R\x10) %Eh\xaa\x04\x80\x08%\xa4H\x7f?\ -8\xd2\x86#\x9d\xa8\xa6\x10]\x03\x80\xca:\x92\x91\xb5\ -\xf6{\xc6\xb2\xb6bb]\x1f:z\x1fh:F\xde\ -+H\xadfAM`\x9f\x00>8l\xe6\x7f\x08\xfc\ -\xb2'\xc7*\xb8\x22:}\xd4+\x97\xcc\xf2:\x97e\ -Y\x96\xd7\x84h\xbc\x05\x18g:\x88\x01\x9f\x01\x1b\x80\ -v`\x0d\xb0\x01\x916\xd4]\x8b8\xed\xa8\xae\x06Y\ -\x8d\xba\xab!\xf2\x01\xea\xae\xc6\xd1\x8fH\x15\xac\xe6\x98\ -\xd7WS[\xeb\x9a\x8do\xe5\xb5\xd1s\xfaS\x98<\ -\x00G\x86\xe2\xb2\x07\x0e\x03P\xd9\x05\xd8\x0dt\x00\xc8\ -.\xc0N\x1e\x8e\xb8\x1e\xf8\x10x\x17a5\xf0!\xaa\ -\x1f \xf2>\xae\xbeK\xc4YA$\xb5\x82yS\xd7\ -z8f\xee)\x8f\x9f\x8eh~\xfd\xbcL\xff<\xec\ -\x00@\xc5E\xf4\xb3/\xfe\x8c\xcfp\xf53$\xf2)\ -\xca\xa7D\xe4S\x92\xf2);\xf4\xfa\x94\xa6\xaa\xf5\xa6\ -\x22gUt\xce\xa1\xe0>k:\x86\xc5-$b\xe7\ -\x98\x0e\xd1\x13\x8b\x87\xcd\x85\x1b\xc2\xf8Y\x9a\xcf\xc5g&\x14x\ -\x1f\xe1M`\x15*\xab\xc0]\x85\xf0\x06\xe2\xbeAD\ -V\x85\xe6\x22D\xe5}\xe0C\x22n\xfa\xf7\x1d\xf7\xbd\x5c\xaa\ -K\x0aL\x07\x08(\x01vE\xd9\x1582\xfd\x9e\x90\ -\xf4?4\x02.\x10mX\x8d\xba/ \xceK\xc0\x0b\ -8\xee\x8b\xb8E/\x928w\x8d\xc9\xe0\xdd\xe7\x14\x80\ -\xf61\x9d\xa2[6\x16\x07w-\xf3\xc9\xb3w'\xe2\ -\x1c\x8b\xa3\xc7\xa1r,I\x0e\x05\x9c\xcf\xff\x5c?\xff\ -\x9f\x5c\xb5;\x22\xbb\xa3\x8c\x02M'w\x1c\x88\xc6\xdf\ -EX\x02<\x8a\xeb.\xa5o\xdf\xa7h\xaa\xea0\x1d\ -\xd6\xf2U10\x14\x18\x8a~\xe9=\xeb\x08$\x8b\x5c\ -*\xe2\xabP^\x01y\x19\xf4\x15D_\xa2\xa4\xf4\xf9\ -0\xde\xe9\xb5\xac-\xd1d\xea\xff\xf0\xa0\xf0\x04P\xd8\ -\x9d\xf5\x91+\x80+\xbc8\x9f\xff\xb4/H?\xd3)\ -zL\x02vm\x04\x10m8\x16t\x16\xdd[W\x9c\ -\xa3\xf4\x0f\xb4\xd4\x98-<+f\x0d\xc2\x8d\x8c@\xe4\ -@pG\x80\x1c\x00\xec\x05\xecB\x0a@\xbe\x98\xcf\xb0\ -\xe9s\xb0gW\xa8B\xbaP\x05t\xcb\xdf3\x9b\x7f\ -\xceJ\xd7\xffw\xbb\x06K9\x10\x8do$}\x03x\ -%\xc8k\x88\xae\xc4\x95\x95\x88\xbb\x92\xf5\xf2:\x8bc\ -\x1bz\x94\xac\x07l\xf1\xe9\x1b\xdd\x19\x91Q\xa0\xe9\x8e\ -u\xae\x00\x9d\x10\x8d\xaf\x02y\x01x\x02\xe1\x11J\xfa\ -\xad\x15.l\x1e2\xf3\ -W\x15+g\x04\xecF\xb7\xe5\xbb\xf2\xf8`\xd0;\x09\ -G\xe1\x99\xa0\xb44\xbb\xb3\x90&\xd6\xedLg\xafc\ -\x10\xf7Xp\x8e\x01=\x18e\x87t1\xd9\xf5\x10*\ -\xb7\xf5\x02\xf6I\x7fiW\xe4\xae\xdc\xc5(\xd1\xf8\xdb\ -\xa8\xaeDd%\xc22\x5c\x9eE\x0a\x9f\xf5\xe3\xa1\x99\ -->\xb3o\x10\xe8 `\x02\x0a\xb4\xb5wP\x1e\x7f\ -\x06\xd1% K\xe8\xc5#\xdc\x1d\x0b\xc6\x94\x19\xabg\ -&\xd6\xf5!Y4\x1a\xa5\x12qO\x03J\xbf\xf2d\ -(\xfc\xfa\x02\xa3\x11F\x83\xf3k\xa2\xf1\xd7\x11\xbd\x17\ -W\x9bh\xa9y\xd4\x16\x1a\xf9L\x0b\x80\xe1\x08\xc3A\ -\xcf\xc3\x01\xa2\x0d\x1f@|\x11H3)\xa7\x85\x85\x93\ -?0\x9d\xd2\xb22\xe5&\x93SA\x22\x1e\x9f\xb6\xb4\ -\xb8P\xab\x00\xdb|\xc8\xfa\xc2\xa4\xd9}\xe9d\x1e0\ -\xd0t\x14\x0f\xbcDg\xe7Y4U\xa5|\x1d%z\ -\xd3\x0eh\xe78\x1c\xc6\x01\xc7\x92d\x7fD\xbb\x1ee\ -\x86\xee\x12E\x80=\x11\xd9\x13\x18\xf5E-\xdd\xa9D\ -\xe3+\x81gQ\x96\x82s\ -O\x048\x16\xe5X(\xf8#\x15\xf1\xa7q\xb9\x0b\xb8\ -\x8d\x96\xd8\x1b\x86\xb3Y\xdd5\xb6~O\x1c\xe7\xfb\xb4\ -\xb5\xd7 \xecb:N0\xe87\x81\x8bp\xb9\x88h\ -\xfd\xdf\x81\x1bX\xbf\xfeF\x16_\xd8\xb6\xad#\xad\xbc\ -\xe0\x00#QF\x22\xfc\x9ch\xc3\x9b\x10\xbf\x95\x08\xb3\ -{\xf2!hY&,\xde\xefO#\x15\xf6\xf3\xe7\xec\ -2j\xd1~\xbf\xdfy\xd4\x8a\x1f\xaf\xf6\xe7\xfcV\xa0\ -\xacm\xbf\x1aa\x82\xe9\x18\x1e\xd8\x00\x9c\xc6\xfc\xc9o\ -zz\xd61u;R\xd8+\xfdp@S\xa3\x09\xc0\ -\xfc\xd9\x1c\xb0\x0f\xca>\xa4d\x16`\x8b\xcf\x90\x11\x94\ -#\x11\x8e\x04~IE\xc3\x83\xb8z=}K\xee\xf4\ -}\xba\x81\x95\x99\x09\xf1\xa1\xb8z\x11*\x17\x80\xf66\ -\x1d'\xb8\xe4P\xe0:\x8a\xfb\x5cIE}\x03I~\ -\xcf\xc2\x9a\xb7L\xa7\xb2r\xca^\xc0\xe5\xa4\xf8)\xd1\ -\xf8c\xa8\xde\x88\x14\xdd\x1a\xbc\xe6nV>q\x1d9\ -\xcb\xc7+\xdc\x02\x9c\xc2\xf1\xc0\x1c\xff\x86\xb0\x02\xa1\xbc\ -a\x06\xa2\x17\x9a\x8e\xe1\x01E\x98Js\xec\x09\x8fN\ -'\x8c\x8b\x8f\xc6\x91\x18p\x1ahq\x9e?\xe1\xcc*\ -g\xdb/\xb1r\x84\x83\xea\x18\x84F\xda\xd6-\xa3\x22\ -\xfeC\xc6\xd4y\xd2!\xcf\xf2\xd0\xd8\xf80\xca\xe3s\ -I\xb1\x0c\x95\x19\x80-<\xbd\xb1\x03*3\x88\xc8k\ -D\xeb\xeb\x980g\x0f\xd3\x81\xac\x9c#\xc0\xb1\x88\xfc\ -\x15:\xdf\xa6\xa2\xfej&\xce\xdd\xcbt(\xcb\xfa\xb2\ -Zj\x1d\xc1\xa9\xf2s\x0cAN\xf7\xf3\xfcV\x00\x94\ -\xd7\x8fC\xf8\x83\xe9\x18\xde\x90Z\x9ac\xb7d|\x9a\ -\xdaZ\x87h|\x22\xd19O\xe1\xc8}\xc0\xd9\xa4;\ -\xb3[Yd\x8b\xcf@\xd2!(\x7f\xa0\xb0\xe8-\xa2\ -\xf1+\x994\xbb\xaf\xe9Dy\xef\xe4\xd9\xbb\x13\x8d7\ -\x10\xe1E\x84\xf3\xb0\xdf[~)\x04\xb9\x80\x94\xbb\x92\ -\x8a\x86\xab\x18\x7fsp\xdb\xf4[~\xea\x8b\xca\x0c\x92\ -\xa9\x95T\xc4\x1b)\x8f\x07x/A+l\x8e\xdf\xbf\ -\x7f\x19\xa8\xaf7\xd0\x14\xc6.\x1aqm\xe9\xb6_i\ -\x85R\xb4~8\x22\xb7w5p\x0b6\xa1\x89\xc4\x94\ -\xff\xcd\xe8\x1c\x95\x8d\xc5T4\x5c\xc8\x93\x83^\x03\xe6\ -\x81\x1e\xe1M8\xab'\xec\x05r\xb0\xf5\x05.\xa7\xd3\ -y\x8dh\xfcr\xca\xe2\xf6)[\xb6\x1d^WHE\ -\xfc\x12\x0a\x9cW\x80)\xa4\xd7\xecZ\xfe\xeb\x8d\xeae\ -\xb8\x1d+\xa9\x88_Be\xa3\xfd\xf7nmIa\xba\ -\xab4OR\x11\xbf\x83q\xb3\x86\x98\x0edY\x0e\xee\ -\xd9Y\x18\xa6\xb7&\x93\x15Y\x18\xc7\xca5\x13\xebv\ -\x06\x99\x87G\xfb\xc7\x9a%\xcf\x10\xe9\xa8\xeeyS9\ -\x15\xca\xe3\x95\xb4\xb5\xbf\x8c\xea\x9f\x81\xc1\x1e\x86\xb3z\ -\xc8\x16\x9f\xe10\x00\xb8\x92b^\xa4\xbca\xb4\xe90\ -y\xa3\x22~\x08\x03\x8b\x9eB\xf9\x13\xb0\x83\xe98y\ -j'\x94?\xd1\xde\xbe\x94q\xf5\x87\x99\x0ec\xe5,\ -A9\x1d'\xf22\xd1\xfa:\xa2\xf5\x03L\x07\xb2\xf2\ -S\xe3\x88\xda\x22\x119-\x1bc\x898Y\x19\xc7\xca\ -!e\xf1\xde$\x0b\xe7\x01\xfb\x9a\x8e\xe2\x81U\x14v\ -N`\xfe\xf4u=:\xba<>\x92h\xc3#\x08\x8d\ -\xd8\xa23\xa7\xd8\xe23\x5c\xf6E\xf4~*\xe2\x8d\x8c\ -\x9e\xd3\xdft\x98\xd0\x8a\xce\xecEE\xfc\xb7(\xcf\x00\ -\x87\x98\x8ec\x01\xca\xb7p\xe4\x09*\xe2\xb5\x94\xd5\x06\ -\x7f\x9a\x91\xe5\x97\xf4\xb4md\x19\xd1\xf8\xc5\xd4\xd6\xda\ -\xcf@+\xab\x06\xb8\xfd+\x14v\xca\xceh:a\xd1\ -`;#*\x7f\xa8\xd0Gn\x009\xdat\x12\x0f\xac\ -E\x9dI\xcc;\xff\xfdn\x1f\x19\xbdi\x07\xa2\x0d\xf5\ -\x08O\x02\xc7z\x1f\xcd\xca\x94\xfd\xe0\x0d#\xa5\x92\x22\ -\xf7\x05*\xea\xc3\xd0Z;\xb7T\xcc\x1a\x04}\x17\xa3\ -\xfc\x04;\xc56\xd7\x14\xa2\xfc\x9c\xe2A\x8f1\xbe~\ -\x1f\xd3a\xac\x9c\xb6\x130\x93'\x06=b\xdf+V\ -V\xb9\xfa\xdd,\x8eVJ\xef\xf61Y\x1c\xcf2\xa9\ -\xa2\xe1\xe7\xa8\x9ek:\x86\x07R\xc09\xb4Ly\xbe\ -\xdbGV\xd4\x1f\x05\xc9\xa5\xa01\xec\x96)9\xcb\x16\ -\x9f\xe1\xb5\x1b*\xf3\x88\xc6\xaf\xb4w\xf7=R\x1e?\ -\x1d\x8d<\x0f|\xdbt\x14\xebk\x8d\xc4\x95g\xec\xcd\ -\x17k\x9b\x84cp\xe5\xefD\x1b.0\x1d\xc5\x0a\xbf\ -%\xfb\xff\xa6\xaf\x08\x13\xb39\xa6\x88k\xa7\xde\xe6\x83\ -\xf2x%\xca\x7f\x9b\x8e\xe1\x09\x95\x1f\x92\x88\xcd\xef\xd6\ -1\x95\x8d\x11\xa2\xf1\xffA\xe5QP\xbb\xb6?\xc7\xd9\ -\xa2$\xdc\x04\xb8\x9c'\x07\xdfC\xf4&\xbb&\xb1\xc7\ -T\x88\xc6/Gh\xc2\xae\xed\x0c\x8a~\xa8\xcc\xa3\x22\ -^\x0bj\xef~Z_g\x07\xd0:\xca\xe3s\xa9l\ -,2\x1d\xc6\x0a\xaf\x0e\xa7\xf84\x85>\xd9\x1cS\x95\ -S\x16\xd9\xa5\x08\xe1V1\xfb\x08\x84\x06\xc2\xf0\xa4O\ -\x98MK\xf5\xccn\x1d\x13\x9d\xd9\x8b\xb5m\xb7\x00\xff\ -\x85\x9d\x91\x16\x08\xb6\xf8\xcc\x0b:\x01:\x1fb\xdc\xac\ -,\xad3\x09\x91\xca\xc6\x22\xa2\x0d\xb7\x00Wb\xbf_\ -\x82FP~N4\xde\xc0\xe1u\x85\xa6\xc3X9N\ -8\x8f\xb6\xf6VN\x8d\x7f\xc3t\x14+\x9cD9\xcb\ -\xc0\xb0\xfd\xddw\xfb\x9f``\x5c+\x1b*f\x0dB\ -\x9d{\xc9\xf2M\x0d\x9f\xdc\xc7\xbaU\xdf\xeb\xd6\x11\xe3\ -o\xee\x87\xf4\xbd\x0f\x11_\xf7\xcd\xb5\xbce/\xa6\xf3\ -\xc7!8\xce\xfd\xf6\xc2\xaa\x1b&\xd6\xf5am\xfb=\ -@6\xd7\xe8X\x9e\x93\xc9\xecRt\x17\x13\xeb\xc2\xf0\ -\xe1l\xf9\xab\x8c\x8d<\xca\xc4\xb9{\x99\x0eb\x85\xcb\ -\xc3Cf\x0e\x00\x8c\xac\xbftDO71\xae\xe5\xb3\ -I\xb3\xfb\xa2\x91y\xc0.\xa6\xa3dLXF/\xaa\ -X\x5c\x9b\xdc\xeec\xa2\xf5\x03p7>\x8ar\xbc\x8f\ -\xc9,\x1f\xd8\xe23\xaf\xc8\xa1l`\x01c\xe7\x96\x98\ -N\x92\xf3\xca\xae-%Y\xb4\x10\xa1\xdct\x14\xcb\x03\ -\xcaxRE-\xf6\xbdom\x87\xe1$SK\x88\xde\ -\xf0M\xd3A\xac\xf0H\x15j\x15`d\xfa\xab\xc0i\ -\xb5\xd8\xde\x0f\xa1R\xd9\x18\xa1\xd3\xb9\x198\xd8t\x14\ -\x0f|\x84\xc3$\xee\x8e}\xba\xddG\x94\xc5{\xa3r\ -7\xc8\x01>\xe6\xb2|b\x7f\x18\xe5\x1b\xe1\x18\x22\xc9\ -\xebL\xc7\xc8i\x95\x8d\xc5\xf4.\x9e\x87m\xd1\x1d.\ -\xca\xf1DR\xf3\xa8l,6\x1d\xc5\xcay{BA\ -\x8b\x9d)byF\xc5\xc4\x94\xdb\xf4\xd0\xb0{\xd9\xb0\ -\x9d\x8f25\xbe\xe5\x83\xf6\xb6\xab \xbb\xcd\xab|\xd2\ -\x89\xe3Tro\xec\xd5\xed?D\x85bf#\x1c\xe3\ -_,\xcbO\xb6\xf8\xccK2\x99h\xfd\x99\xa6S\xe4\ -\xa4\xb2\xda\x02\xda\xda\xff\x86\xc8(\xd3Q,_\x9cD\ -\xdb\xbaF\xbb\x17\xa8\xb5\x1dF\xb0A\xef\xb4M\x88\xac\ -L\xdd7\xe2\xea\xbd\xc0\xec\x85\xb2k\xbb\xde\x86Gy\ -\xc3TTf\x98\x8e\xe1\x91\x0bY0eQ\xb7\x8e\xa8\ -\x88_\x01\x9c\xedO\x1c+\x1b\xc2P|\xae\x02\x9d\x87\ -\xc8\xfd\xc0b`i\xd7\xd7\xeb\xe9/YMz\xcf \ -\xeb\xdf\xc8uL\x98\xb3\x87\xe9\x149\xa7\xf7\xa0?\x01\ -\x15\xa6cX~\xd2\x09\xf4\x1e\xfc\x17\xd3)\xac\x00\x10\ -\x19E[\xfb\xefM\xc7\xb0\x82\xad0\xc59\x18\xeeD\ -*\xca\x19&\xc7\xb7u\xc4\xdc\x89\x94\xfe/P\ -c:H\xce\x88\xd6_\x04\x5ch:\x86\x95\x05\xa2\xd3\ -\x88\xc6\x9f'\x11\xbb\xc6t\x94,{\x13\xe5\xc7\xa0}\ -\x10\xa7\x17h\x1f\xa0\xeb\x9f\xd2\x8bt\xc7\xc4\x01\xc0\xee\ -\x08\x03Q\xf6\x00\xfa\x1aMl\xdeED\x1b\xee%Q\ -\xbd\xd0t\x10+\x98\x14sSn7\xb3\xf7\x83\xfb\xcd\ -\xfc\xd6I+f\xfc\xc3t\x10\xab\x87\xc6\xc6\x87\xa14\ -\x82\x06\xff\xda]\xb8\x93\xa3\xde\xf8OZ\xbas\x90\x0a\ -\xda\xf0\x17\xa0\xb7O\xa9\xac,\x09\xfe\x1bx\xbb\x88\x92\ -\xe0C\xe0\xc3\xad\xbe\xe4\xf0\xbaB\x06\x14\xec\x03\x05\xc3\ -q\xdc#q9\x01a$\xa1.H\xf5\x5c&\xce\xad\ -e\xfe\xe47M'1\xae<>\x12\xf8\x83\xe9\x18V\ -6\xc9U\x8c\xaf_\xca\x82\x9a\xc7L'\xc9\xa2\xcfh\ -\x895u\xeb\x88\xca\xc6b\xd6\xb5\xed\x06\x91A\xa4\xf4\ - \xe0 \x84C@\x87\x13\x8e\xf6\xfe\xdb\x22\xa0\x7f\xa1\ -,>\x82\xc5\xb1\x0d\xa6\xc3X\xc1\xf2\xd0\xd0?\x1e\xe0\ -\xc2A\xa6s\x008\x0e\xa7\x03\xb6\xf8\x0c\xa2\xd1s\xfa\ -\x13\xd1\xf9\xa0!X\x87\xae\x7f'Y0\x99\xdaZ\xb7\ -[\x87U\xcc\xa9D9\xc9\xa7PV\x16\xe5I\xf1\xb9\ -\x1d\x96N\xef\x04\x96w}\xdd\x05\xc0\xa9\xf1o\xd0A\ -\x05\xaag\x82\x8c'|\x9b\xd7\x16\x92L\xfd\x18\x08\xcb\ -\xda\x81\x9e\x19\x7fs?\xdc\x8e& \xeck\xbb>\x22\ -=\x1d\xfd5\xd05\x88|\x86\x92\xfe\xe1\xaf\x94\xe2\xd0\ -\x1b\x97=\x10\xf6\x06\x06\x03\xbd\xccE\xcd\x06-\xc0\x95\ -\x9b\x19\x7f\xf3a,8\xe7\x13\xd3irVS\xd5z\ ->_\xc6\xc0\x17ks*\x1b#|\xd6>\x14GO\ -\xc4\x91\xb1('\x13\xde\xa7\xa4\xfb\xd0G/\x03~m\ -:\x88ONCx\xcdt\x88\x8c$u\xfb;ef\ -\x91F\x22\xe7\x9a\xce\xb0\x89\xc2\xe9\xc0\xcfM\xe7\xb0\xba\ -\xe9\xf0\xbaB\x8a\xdc\xbf\x01CLG\xf1\xc0\xbb\x90\x9a\ -\xc4\xc2\x9a\xf6n\x1f\xe9\xea%f'\xaf\xfbM\x92\xa0\ -\xcf\xa3\xf2,\xc2[\xa8\xbb\x0a\xe1-D>Ft\x0d\ -\xea\xa6\x90\xe25\x9f\xbf<\xd5\xb1#\x11uH\xf2\x0d\ -\x84\x018\xd2\x1fWvF\xdc\xbd\x10\xd9\x0be/\xd2\ -\xd7r9\xb7\x15\x8f->\xbfN\xba\xed\xf3-\xc0-\ -\xe9\xb6\xfb\x05?\x05\xbeG\xb8\x9e\x86VS\x16\xffi\ -^\xdf\xd1Ou\x5c\x8d0\xc8t\x0c\xef\xc9jp\xef\ -D\xe4!4\xf90\x89\xf3\xdf\xde\xeeC+\x1b#\xac\ -]?\x02\xdc\xe3@ODd<\x10\xc6mJ\x06\xe3\ -v\x5c\x03\xe4\xcc\x05b`4U\xa5\x80e]_u\ -L\xac\xebC\xaa\xe8T\x94\x0b\x80\x13\xcd\x86\xf3\x81\xca\ -\x8f(\xbb\xf6\x1a\x16_\xd8f:\x8a\xe7\x5cy\x95\xd6\ -\xea\x97L\xc7\x08\x1bEe1\xd7\xe4\xc2\x94\xdbM\x0e\ -|p\xff?\xed\x7f\xd2\xf2K\x97\x9b\x0ebm/\x15\ -\x064\xcc\x06\xcaL'\xf1\xc0z\x5c\xe7TZ\xbbq\ --\xb2\xc9\xf8\x86\xc3q5|\xddm\x85\x7f\xa2z\x0b\ -\xae\xb4R\xb4q)\xf3\xa7\xaf\xeb\xc6\xd1\xdbw\xd3\xfc\ -\xd4\xf87\xd8\xc80D\x0f@\x19\x06r(0\x120\ -\xf6\x14\xdd\x16\x9f\xdb+}\xe1>\x83\xf1s\xff\x82\x9b\ -\xba\x8dp\xec\xad\x04\xd0\x97>:\x06\xb8\xd7t\x10#\ -\xc65\x8cG\xf4<\xd31<\xf6\x10*W\xf1\xe1\xc6\ -D\xd7\x13\xfd\xeeK\x17\x16\xcfw}]\xc7\xd8\xb9%\ -\x14\xa4NA\xf5\x22\x90\xa3\xbd\x0c\x9b\x03\xcea\x5c\xc3\ -\xad\xb4V/0\x1d$\xd0\xd2\x1f\x9a\xe9\x9bu\x15\xf5\ -G\xa1\xfc1d\xef\x95\xfe\x14\xf7\x89\x01\xf9\xb6N\xd8\ -\xea\xa1\x07\x87\xcd<\xdaA\xf66\x9dcs\x22\x91\xd3\ -\x80+M\xe7\xb0\xb6Sy\xfcg\x88\x84\xe1\x1aEA\ -c\xb4Ny\xaaGG\xbb\xee\x05\x86{vym>\ -\x8e^\xc9\x82\xd8\xe3 \xea\xebH\xe9\x07iOt}\ -uQaB\xc3\x10\x92z\x04\x8e3\x12\xf4\x08\x94\xc3\ -\xc8\xd2C\x860t\xbb\xcd\xae\x05\x93_\xa1\xb4\xe4\xdb\ -@\xab\xe9(\x9eQ'|O)\xb6\xc7\xc4\xba>8\ -\x1a\xa2\xae\xa7\xfaw\x1c\xe7$\x12\xb12Z\xaa\xe7\xf5\ -\xb8\xf0\xdc\x92\x85\x93\xdbi\x8e\xddB\xa2\xe6\x18T\xc6\ - \xf4\xec\x03$W9\xfc\x95\xb1s\xc3\xf8d\xd7\x8c\ -\xe6\x9a'9\xea\xcd\xe3P~J\xb8\xba\x8d\xdb\x06m\ -\xd6vsr\xa3\xd1\xd0\xbf\x11Q\xbb\xe5JPT4\ -|\x07\x91_\x98\x8e\xe1\x09\xe5?H\xd4\xdc\xde\xf3\x13\ -8aY\xeb\xb9\x1c\x91\x13I\xc4&\xa5\xfbM\xf8\x5c\ -xn\x95(\xf7\xc6^\xa5\xa5\xe6V\x9a\xab\x7fHs\ -\xec\x04JKv\xc4I\x1d\x84h\x0dP\x0f\xbc\xe1\xd7\ -\xe8\xb6\xf8\xec\x89\xa6\xaa\xf5\x94\x96\x9c\x06\xfaw\xd3Q\ -\xbc\xa1G\x98N`Dg\xd1O\x80=M\xc7\xc8\x9c\ -$\x81\xff\xe5\x83\xce\xa3\xba\xbd_VO\xb4T?@\ -I\xc91\x08?\x02\xba3E$\x87\xe97\x89$\x7f\ -j:E\xa8\xd4\xd6\xba\xb4\xc4~\x87:\x95]\xef\xd1\ -08\x84\xf2\xd9\xfb\x9b\x0ea\xe5\xbeE\xe9\xbd\x84+\ -M\xe7\xf82UFv\xed;j\xe5\xb2q\xf5\x87\xa1\ -:\x87p\x5c\xa7\xcf\xa1%\xd6\xf3\xa7\xed\xe3n\xdc\x0d\ -4\x0c\xeb]\x9f\x06=\x9e\xe6\xea\x87M\x07\xd9\xa2\xa6\ -\xaa\x14\x0b\xa6\xbdHsM\x9cDl*\x89\xd8\xde$\ -\xdd=P\xaa@\xaf\x07\xe9\xfet\xe9\xad\x08\xc3\x9b\xda\ -\x8c\xa6\xaa\xf5h\xa4\x9ap\xdc\xd5\xdf\xd7t\x80\xac\x9b\ -0g\x0f\x84\x10\x14\x1b\xd2\x86\xe8D\x12\xb1\xff\xf6\xf4\ -I\xe7\xb64U\xa5h\x8e]\x85:G\x03!\xe9\x96\ -,?N\xaf\xed\xb6<\xd52\xe5.\x94\x1f\x99\x8e\xe1\ -\x19q\xa2\xa6#X\xb9O\xdf\xeb?\x9a\x1cl\xf4\x01\ -H$)\xa7\x9a\x0ea}\x8d\x93g\xef\x8e\xe3\xdcC\ -(\xfa,\xc8\x12X;=\xa3S8\x9d'x\x14\xc6\ - y\x9b^\x8c%Q\xb3\xf5]7r\xd1}S\xdf\ -\xa1%\xd6D\xa2f:\x89){!\x1c\x8cp)0\ -\x1fX\xb3\xad\xc3\xb7\xc6\x16\x9f\x99h\x99\xf2<\xe9\xff\ -\x00A\x97\x8b\x1f\x90\xfeJ\xb9\x97\x13\xfcm\x22>\xc5\ -\xa1\x8c\xe6X\xb7v\xca\xf2T\xcb\x94\xe7\x11\x8e\x0a\xc9\ -,\x80>P\xf03\xd3!B\xa9e\xca5@n\xde\ -\xed\xed69\xd6t\x02+\xf7\x09\xe4\xdc\x94\xdbMD\ -8\xddt\x06k+\xca\xae-\xa5\xd0i\x06\x0d\xfe\x8d\ -P\xe1\x9f\xe0\x9eNb\xc6\xc6\xccN\xe4\x04\xbf!\xa4\ -\xea\xaf\xbb\xd6^\x06\x98(\xcd\xb1\x17h\x8e]M\x22\ -6\x89\xf5\xab\xfaS\xda\xe7\x91\x9e\x9c\xc9\x16\x9f\x19\xd3\ -\xdbL'\xf0@\x11\x87\xd7\x85\xa9\x83\xef\xd7\xab\x88\xef\ -\x0aL3\x1d#C\x9d\xb8Z\xc9\x82\xea\xa5\xa6\x83\xd0\ -\x1c{\x8f\x82\xce\xb1\xa4\xb7)\x0a\xba\x1a&\xce\xb5S\ -\xd2<'\x8a\xea\xafL\xa7\xf0\x84\xe8\xe1\xa6#X\xb9\ -m\xd1\xe0xo\xd0\x5c~\xbax\x5c\xeb\xbe\x7f\x19h\ -:\x84\xf5%\xb5\xb5\x0e\xbd\xfb\xdc\x84\xf2-\xd3Q<\ -\xb0\x06IM\xf2\xe4I\x9f\xe8\xce\x1e\xe41\xcbI\x85\ -\xaf\xa1\xe1\xe2\xdad\xd7Vl\xddf\x8b\xcfL\x15\x14\ -{\x7fT\xef\x06\x8aLG\xf1@+\ -\xebW\xfd\xc4\xfb\xd3\xea'\xde\x9f3\xdb\xf4G\x94\xc7\ -\x07\x9bN\x91+l\xf1\x99\xa9\xceH\x99\xe9\x08\x99\x93\ -{M'\xc8\x8a\x09s\xf6\x00\x82\xdc\xb2\xfb#\xb4\xf0\ -*\xd3!\xb6i\xde\xf9\xef\x03\xbf3\x1d#C#\x19\ -7+\x0c\xfb\x8a\xe5\x16qB\xb2V2\x99a\xf7F\ -+\xac$\x00Sn?'j\xa7\xde\x9a4n\xd6N\ -\x883\x1f\x08\xc3\x13\xe8\x97\xe9\xec8\x93\xc5\xb5>\xf4\ -\x0f\x91\x97\xbd?g\xd6\xed\x80p\x0f\x13\xeb\x82\xdf<\ -\xc9\x03\xb6\xf8\xcc\x94\x92\xd9\xfeE\xe6\xbdKI\xc9C\ -\xa6CdE\xca\xad$\xd8\xef\xf9Y$\xce\xed\xf1\xbe\ -J\xd9Ux5A\x7f\xfa)\xcewMG\x08\x1f7\ -\xc8S\xde\xbfP\xe8\xac3\x1d\xc1\xca=\x8bF\x5c\xbb\ -+0\xcat\x8e\xed%H\xb4\xf5\xe0\xdf\x85`/\xc9\ -\x00:\xbc\xae\x10'\xd2\x04\x0c5\x1d%s\xb2\x1ad\ -\x12\xf7O\xff\xcc\x9f\xd3\xcb\xd3\xbe\x9c7\xfb\x0e&Y\ -\xf8p\xd7\x8e\x0by-\xc8\x17\xe2\xe6E\xeb\xcfD$\ -0\x1f4[&\x7f\xa5\xa9\xaa\xc3t\x8a,\x99`:\ -@\x06\x94\x08\xb3M\x87\xd8n\x89s\xd7\xa0\xcc5\x1d\ -##b7b\xf7\x9es\x94\xe9\x04\x9eh[\x97\xdb\ -S\xdf-3\xdc\xd4w\x81\x88\xe9\x18\xdbK\xa1Oa\ -G\xafq\xa6s\xe4\xa5\x81\x85\x7f\x06N2\x1d\xc3\x03\ -\x1bP\x99D\xa2\xfa5\xdfFh\x9e\xfc*\xf0\x9eo\ -\xe7\xcf*9\x00\xe5q\xca\xe7\x1cm:\x89I\xb6\xf8\ -\xec\xa9\xf29G\x83\xdc`:F\x86>\xa0s\xe3\xd5\ -\xa6CdE\xba{\xe9\xf1\xa6c\xf4\x98\xeab\xee\x8d\ -\xbdj:F\xb7\xa8\x04\xbd\x81\xc2aL\xbaa\x17\xd3\ -!B\xe3\xe4\xd9\xbb\x83\x86\xa1\xf8\xfc\x88\xc5\x17\xb6\x99\ -\x0ea\xe5\x1e!@Sn\xbb\x88\x9dz\x9b}\xd1\xf8\ -\xe5 \x17\x98\x8e\xe1\x01E\xf5|Z\xa6\xf8\xbc\xe5\xa0\ -(p\xbb\xbfcd\xd5`D\x1f&\x1a\xff\x0fjk\ -\xf3\xb2\x0e\xcb\xcb\xbft\xc6\xca\xe3\xdfE\xdc\xfb\x80`\ -o\xc7 z\x85o\xd3$rMRN$\xc8\x0b\xfa\ -E\xe6\x99\x8e\xd0m\xad\xd5/\x01/\x99\x8e\x91\x01\xa1\ -\xb3\xd0>\x15\xf0J\x813\x03\x08\xfe\x9aO\xd5\xe7M\ -G\xb0r\xcf\x03Cf\xee\xab\xcaH\xd39\xbaMe\ -b\xe3\x88\xda\xe0~6\x06M\xf9\x9c\xd3\x80_\x99\x8e\ -\xe1\x09\xe1\x7fh\xa9\xb9)+c\xb9\xce-Y\x19'\ -k\xb4\x00\xf8%O\xee\xf5\x0c\xe5\xb3\x8f3\x9d&\xdb\ -l\xf1\xd9\x1d\xe5\xf1\xc1\x94\xd7\xdf\x8ep+\x10\xecu\ -\x12\xc2\x9d4\xd7\xc4M\xc7\xc8\x1e9\xd6t\x82\x8c\xb8\ -\xa9\x806\x85\xd2\xbbM'\xc8\x8c\x9eh:A(\x94\ -\xcf9\x18\xb8\xcct\x0cO8\xf2\x8c\xe9\x08V\xee\x89\ -\x14\xca\xd9\x80\x98\xce\xd1\x03;\x0eH\xf6\x0b\xc3\xf4\xcf\ -\xdc\x17\x9ds(\xe2\xdeH(\xae\xbd\xe5o4W\xff\ -\x22k\xc3\xb5Ny\x0a\x08a\x7f\x129\x14q\x1e\xa6\ -\xa2\xe1\x16\xa27|\xd3t\x9al\x09\xc17@\x16T\ -\xc4\x0f\x22Z_\x87\xb0\x0c\x91*\xd3q2&,C\ -\x8a\xf2k+\x09\xe5\x18\xd3\x112\xf0\x1a\xad\xd3V\x9a\ -\x0e\xd1#n$xOl7'\x81~\xdf\xe4\x86\x09\ -s\xf6@\xdc{\x08\xf2\xcc\x83\x7f\xe7\xdd\xe6\xe9Vh\ -\xa8\xea\x99\xa63\xf4\x94\x88c\xa7\xde\xfam\xdc\x8d\xbb\ -\x81{\x0fA\x7fp\x91\xb6\x94\x82\x8dS\xb2\xbe\xdf\xb8\ -\xba\xff\x99\xd5\xf1\xb2GP=\x0b\x0a^\xa3<>\x97\ -h\xc3\xbe\xa6\x03\xf9\xcd\x16\x9f[\x13\xad\x1fNy\xfc\ -\x0a\xa2\x0dO\xa3<\xdf5?\xbf\x97\xe9X\x1ex\x17\ -\x97(\x0b\xce\x09\xc1\xbeI\xdb)=\xa7\xfep\xd31\ -2\xf0\x84\xe9\x00=\xb6\xf1\x9f\xcf\x02\xc1\xed\x0c\xaa\xec\ -\xcf\xb8Y;\x99\x8e\x11X\x15\xf1CH\xb9K\x80\xc1\ -\xa6\xa3xd-\xebx\xc4t\x08+\xb7<4|\xe6\ -\xa1\xc0\x08\xd39zNOm\xa410\x8d\x92\x02\xa7\ -\xb2\xb1\x98H\xf2n`O\xd3Q<\xf0/\x22\xce)\ -\xcc\x9f\x9e\xfd\xcf\xf5\x96\xa9K\x80\x80\xcf\xa6\xfaZE\ -\x08\xe7\x81\xbeL\xb4\xbe\x8e\xb1s\xf76\x1d\xc8/\x05\ -\xa6\x03\xe4\x84\x93g\xefNA\xe4@T\xbf\x85p\x08\ -\xe9V\xe9\xbb\xa5'\xd0d\xf7\xc6\x8e\xcf\xdeB\x9c1\ -$\xa6\xbca:HV=\xfe\xcd}p\x82|\xb7Q\ -\x83[|.\xaeM\x12\x8d?\x0d\x04u\xfa\xaaPP\ -\xf8-`\x91\xe9 \x81\x12\x9d\xd9\x0bv\xf81\xaa?\ -\x03\x8aM\xc7\xf1\x8c\xd0\xc8\xe2\xd8\x06\xd31\xac\xdc\x92\ -R\xce\x0a\xe2|\xdbM\x14\x06\xf6?\xe0\x9dcy\x85\ -\x87Mg\x09\x1f\x15\xda\x1a\xe2\xc0\x91\xa6\x93dN\xda\ -pS\xe3I\xc4\xfee.\x83^\xd0\xb5\x8cj\x80\xb9\ -\x0c\xbe+\x02\xb9\x80Hj\x1a\x15\x0d\x0f\xa2:\x93D\ -\xf5\xbdY\x7f\xd2\xec\xa3\xf0\x17\x9f\x95\x8dE\xb4}6\ -\x10\x89\xec\x81\xeb\xec\x02\xeen \xbb\xe2\xb0\x1b\xae\xee\ -\x87\xc8\xc1@\x7f\xd0`\xae\xd6\xd8n\xf2\x22\xaa\x13\xf3\ -\xae\xf0\x04\x888#\x02}\x0f\xc1\xe1Y\xd3\x112\xf4\ -\x04\xc1->\xc1u\x0f\xc4\x16\x9f\xdbgb]\x1f\x92\ -\x855 ?\x02\x1dl:\x8e\xf74\xe8\x1d\xce\xb7\xcc\ -\xd1?\x13m\x08F\x07_79\x85\xd6i\x1f\x9b\x8e\ -\xb1\x89\xa2\xb2X\xaf\x09\xfcr\x1c\x079\x1dl\xf1\xe9\ -\xb9h\xc3\xaf\x81\xc0N\xc9\xde\x8c\x8br\x0e\xadS\xff\ -a4E\xa2\xe6C\xa2\xf5\x17\x83\xdcf4Gv8\ -\xa8\x8e\x01\xc6\x10\x9d\xf3\x22Z\xffg\xdc\x82\x9bX8\ -\xb9\xddt\xb0L\x05\xbf\xf8\x14\xc6P\xd1p\x1f\xaa}\ -I\xff}\xfau\xfd\xb3/\xd0\x9b\xb6\xf6b(H?\ -\xc0\x14\xe5\xf3\x0aS\x01\x09u\xb5\xb9\x19\xbd\x8b\xf5\xeb\ -&\xe7\xed\xf6\x00*\xc3LG\xc8\x88K\xb0\xb6X\xf9\ -\x0ay9\xe03\x08\x86\x9b\x0e\x90\xd3*\x1b\x8bio\ -?\x118\x93$\xa7\x03;\x98\x8e\xe4\x0b\xd5E$j\ -\x9e4\x1d\xc3'e\x81\xf9\x1eu\xa5\xb7\xe9\x08\x9b{\ -\xf0\x80\xab\x8fw\xd4\x19\xe4\xe7\x18\x02\x1f\x00\xa5\x0a}\ -|\x1bC\xe54E/\x93\x10=]1\xae<^\x0d\ -\x5cn:\x86'\x84\x9f\x90\xa8\xce\x8d\x1e\x0e\x89\x9a\xdb\ -\xa9\x88\x1f\x82r\x85\xe9(\xd9\xa3\x07\x22\xf2W\x22\xa9\ -\xdfR\x1e\xbf\x07\xd5\xb9\xb4\xc6\x1e\x08\xea\xd3\xd0\xe0\x17\ -\x9f\xb0\x1b\xaa\xbb\x99\x0e\x91\xa36 \x5cAslf\ -P\xdf\xa0\x9e\x10\xf6\x0a\xcau\xd5\x16|F\xa2\xe6C\ -\xd3!2\xa3+L'\xc8\x88\xc8\x10\xd3\x11r\x87\x0a\ -\xe3f\xef\x8b\xe3\x1c\x02\x1c\x02r\x04m\xed'\x10\xa6\ -\xa9\xb5[\xa6\x88\xf3_\xa6CX\xb9\xc7A\xb2\xb1\xb7\ -\xe7m\xc07\x81\xd3\xfd\x1a@a\xaf\x87\xf6\xbb\xfa\x08\ -V\xf0\xb4_c\xe4\x95q\xf1\xe3\x11\xfej:\x86G\ -\xeai\x8e]e:\xc4\xbf9r\xd5\xcfxr\xaf\xfd\ -A\xf2\xadY\xd6\x0e\x08\xe7!r\x1e\xd1\xf8+\xd0\xd0\ -\x80\x1b\xb9\x91\xd6\xf3\xde5\x1d\xac;\xc2P|Z[\ -v\x1f\xea^Lb\xear\x88\x99\xceb\x96\xca^\x81\ -\xb9\xab\xffU\xaf\x99\x0e\x901\xa7h9n\x87\xe9\x14\ -=\xa7\xba\x97\xe9\x08YS\x16\xefMov%\xa2\xbb\ -\x93\x92]\x11\xf6\x00\xdd\x05\x91=P\x86B\xc3\xc1\x10\ -\x09\xf6\xfe\xc6=!r\x1b\xcd\xd5\x8f\x9a\x8ea\xe5\x96\ -g\x0e\xaf+\x5c\xdb\xbe\xf1\x0c\xbf\xc7Q\xe1V\x5c\x19\ -\x82\xa8o\xc5'\x80\xebDN\x03[|fl\xec\xdc\ -\xbdqRw\x10\x8e&\x95\x0fSZ\xf2}\xd3!\xbe\ -\xa2\xb6\xd6\xa5,~6\xc5\xdc\x01T\x98\x8ec\x86\x1c\ -\x00\xfa\x1b\x9c\xd4/\x89\xc6\x13\xa8\xccb\xc3\x1b\xcd,\ -\xaeM\x9aN\xb6-\xb6\xf8\x0c\x9f\xb7@\xfe\x93D\xf5\ -\x5c\xd3Ar\x87\xbbGp\x17\xf4\xea\xfb\xa6\x13dl\ -\xc19\x9f\xa4\xd7\x93i\xa9\xe9(=\xb4'\xa8\x84c\ -\xf6\x80\xecM4\xbe\x04\xe8\x0d\x94\x02\x85\xc0\x8e@\x04\ -\xf8\xc6\xe7/se\xb3o\x19\x09\xf0\xbd\x1bO|H\ -\xd2\xb9\xd4t\x08+\xf7\xacm\xef\x1c\x07\xec\xec\xe7\x18\ -\x82\xbc~\xe2+\x17=y\xff>\xd7\xbfRX\xb4\xb1\ -C}\xdc\xb2H\xd03\x80\xff\xf0\xeb\xfcy!z\xd3\ -\x0e\xd09\x9fp4\xc4y\x8d\x82\x8e\xef\xd0\x14\xcb\xcd\ -\xbb\xc7\x8bc\x1b\xa8l<\x8d\xb6\xf6\xdb\x81SM\xc7\ -1G\x0b\x80\x89\x88N\xa4x\xd0{D\xe3spS\ -\xb3ry\x8b>\xbb\xd5Jx\xacC\xf8\x05\xeb\xd9\xcf\ -\x16\x9e_&\xc1\xdd*Ce\xb5\xe9\x08\xde\xd0\x0fL\ -'\xc8@1\xe3f\xf73\x1d\xc2\x1bZ\x0a\x1cKz\ -\xeb\xa1\xfd\x81}\x80\xfel^xZ\x9bST\xa6\xb1\ -pr\x90\xdf\xbf\x96O\x84\x94\xefSnU\xf4\x16A\ -\xf4\xe4\xd7\xa7\x7f\xa6\xf0\xa0\xcf\xc3\x0d}p\xf8\x9f\x03\ -\xbce\x8ca\x87\xd7\x15B\xe7\x9d\x04z\xdb\x9d\xcf}\ -\x8c8\x15\xcc\x9f\x9e\xdb\xd7 MU\x1d\x94\x96\x9c\x89\ -0\xdbt\x94\x1c\xb1+p9Nd\x05\xd1\xf8\x03\x94\ -\xd7\x9fEY<\xa7\xd6\xc9\x83->\xc3\xe03Dg\ -\xe2\x16\x0c\xa19Vk\xb7\x01\xd8\x12\xe9o:A\x8f\ -I\xa0\x8b\xb6\xcd\x05{\xdd\xaaS\x10\x92\xe2\xd3\xea\x16\ -\xe1\x7fh\xc9\x91&\x1bVN\x99\x7fx]\x1fE&\ -\xf9=\x8e\x8a\xf3yWO\x81;\xfd\x1e\xcfQ\x7f\xa7\ -\xf6\x86\xda\xc0\xa2\x99\xc0h\xd31<\xd0\x09ZI\xf3\ -\x94`\xf4kh\xaa\xea\xa096\x0dd:H\xceO\ -9\xcd\x12\x01NB\xe4\x16\x8ay\x93\x8axm.\xed\ -Yn\x8b\xcf\xe0z\x03\xe1RR\x91=h\xae\xb9$\ -h\x8b\x8d\xb3\xa6\xac\xb6 \xc0\xd3=\x01g\x8d\xe9\x04\ -\x9e\x10r\xfb\xee\xe9\xb6H\xca\x16\x9f\xf9F\xb9\x91\xe6\ -\xea_\x98\x8ea\xe5\xa6\xbe\xed\x1bO!=u\xdd?\ -*\xcf\x9d\xf4\xf2E/}\xfeK\xb7\xe3.\xc0\xd7\x8b\ -ku\xfdkj\x14j\xd1\x86\x1f\x01\xdf3\x1d\xc3\x13\ -\xaa\x17\x93\xa8\xf1\xfb)\xbb\xf7\x12\xd5\xd7#Z\x01\xd8\ -\xeb\xe1\x7f7\x00\xe5\xe78\x05\xab\xa8h\xb8\x8a\x89s\ -\x8d\xf7\xb1\xb0\xc5g\xb0(\xc2#(g\xb1~\xd5P\ -\x9acW\x87a\xbf\x1f_\xf5\xdd\xcd\xb7\xf51\xd9\xa1\ -\x1bM'\xf0\xc8:\xd3\x012\x92\x12;-5\x9f\x08\ -w\xb2aUM8\xd6\xf9Z~P\xf0\x7f\xca\xad\xc3\ -\xad\x9b\xffz\xd4\x8a\x1f\xaf\x06\xfcm|%z\xc8\x03\ -Cf\xee\xeb\xeb\x18a\x13\x9d\x13\x05\xfd\x8d\xe9\x18\xde\ -\xd0\xdf\xa7\xf8\xe2\xdc\x00\x00 \x00IDAT\xd2R\ -Sg:E\x8f5\xc7\xee\xa3\x17\xc3\x11\xb9u\xdb/\ -\xce7Z\x8a\xeae$S+\xa9\x887R1\xfb\x08\ -SIl\xf1\x19\x0co\x01\xbf\xc1M\xedGs\xec\x04\ -Zb\xb7\x05\xa1\x9bUN\x88\x14\x05\xbb\xf8T\xcd\xcd\ -\x85\xfe\xdd\xa5\x04\xfc\xef\xe1\x84\xa1k\xa1\xb5=Tf\ -\xb1n\xd5\x99\xf6g\xac\xb55\x8f\x1ct]?\x81q\ ->\x0f\xa3\xa2\xc9\xc6-\xfc\xee]>\x8f\x8bS\xa0\xf9\ -\xb6}E\xcf\x8dk\x18\x01\xeem\xa4\x9b\xb6\x05]3\ -\xa5\xa5\xc1o8uw\xecS\x9a\xab\xcfF\x89\x01\x1f\ -\x9b\x8e\x93\x83\x0aQ*Q\xe7)\xa2\x0d\xf3\xa9\x88\x1f\ -\x92\xed\x00\xb6\xf8\xccY\xd2\x06\xd4#r\x22\x89\xeaA\ -$bW\xe4r\xe7\xaa\x9c\x95\xea\x08vGg\xc7\x09\ -x\xd1\xd6E\x08\xf6Zd\x91`\xdf\xc4\xb0\xb6\x87\x22\ -\xfc\x82\x96\xea\xf3m\xe1i}\x9ddg\xb2\xca\xcf\xae\ -\xb3\x00(KF-\xbb\xec\x8d\xaf\xfc~J\xef\xc0\xe7\ -\xfe\xd3\x92\x7f{'\xf6\x8c2\x15G\x97\x02;\x98\x8e\ -\xe2\x81\xe7X\xbf\xeeL\x9a\xaaR\xa6\x83x\xa6%\xd6\ -\x80\x9b\x1a\x0az=\xf9\xde\xb3}\xcb\x04t\x02\xca\xb3\ -T\xc4\x1b\x197+k{\x9a\xdb\xe23gi)P\ -\x86\xba\xe7P\xdep\x06\x13\xeb|m\xe7\x1eZ\x1d\x11\ -\xd7t\x84\x8c\xb8\x1a\x8e\x1f\x98J\xa7\xe9\x08\x19\x91\x94\ -->\xc3m\x0d\xe8Y4\xc7jM\x07\xb1\x02\xc1\xf7\ -)\xb7\x02[\x9c68j\xe5\x8c\xb7\x81g\xfc\x1c\xdb\ -\x85o/\xdc\xff\x9a\xdd\xfd\x1c#$z\x11\x8e\xbd<\ -\xdf#\xa5\x93X|a\x9b\xe9 \x9ek\x9d\xf61\x89\ -\x9a\xe9\xa8F\x81WM\xc7\xc9Q\x82R\x89\x13y\x99\ -\x8a\xf8\xb5T\xc4w\xf5{@[|\xe6\xb6}@.\ -@h$Y\xf4>\xd1\xf8?\xa8\xa8\xbf\x9ah\xfcT\ -&\xcd\xce\xbf\x8d\xde{\xa2\x97\x04\xbb\xe8A\x82\xfd\xe4\ -v\x13\xa1\xd0t\x84\x8c\xa8\x04\xfb&\x86\xb5u\xcac\ -\xb8\xa9\xc3I\xd4\xdcn:\x8a\x95\xfb\x16\xee\x7f\xcd\xee\ -\x0a\xc7\xfb\xa2v\xddg\x1ePT\xa7\xd2\x5c\xf3\xa4\xe9\ - Y\xb1tz'\xcd\xb1\xabA\xf7\x03\xfej\xb7e\ -\xd9\xaa>\xc0\xe5ld%\x15\xf1\xefQ[\xeb\xf9\xcf\ -;[|\x06W\xef\xf4\x5cm\xa9\xc7\xedx\x97h\xc3\ -|\xca\xe3\x95\xe9\xadE\xac\xcf\x95\xad\xea\x00\x82\xfb\xd4\ -J\xdc`\x17m\x9bH\xc0\x8bO\x8d\xd8\xe23\x5c\xe6\ -\xa3\xee!$b\xbf\xa1\xb66\xb8?\x1f\xac\xac\x13\xc4\ -\xf7)\xb7\xe8\x96\xa7\xdcnr\xe2+\x97\xbc\x0a\xbc\xe8\ -s\x8a\xb2E\xfb\xfd\xde.\xf7\x093\xe5\x8f\xb4\xd4\xe4\ -_W\xd8D\xcd\x87$b\xdfGS\x07\xa2\xdcH\x90\ -\xaf\x11\xfd\xd5\x1f\xe5/<\xb9\xd73\x94\xcf9\xda\xcb\ -\x13\xdb\xe23\x1cz\x81N@h\xa4x\xd0\xabD\xe3\ -\x97\xdb\xa7\xa1]\xd2\x17\x96\x9f\x99\x8e\xd1c\xea\x84\xe3\ -\xc9'\x1a\xf0\xe2\xd3]o:\x82\xe5\x89\xa7QF\x91\ -\x88M\xa2e\xear\xd3a\xac`Y\xb4\xdf5\xc3\x14\ -\x0e\xf3s\x0c\x81u\x14D\xee\xd9\x8e\xd7\xdd\xe9g\x0e\ -\xa0@\xa4\xd7D\x9f\xc7\xb0L\x12\xbe\xc3\xa4\x1bv1\ -\x1d\xc3\x98\x96\xa9\xcbi\x89M\xc6I}\x0b\xa1\xc9t\ -\x9c\xdc%\x87\x22\xee\xa3\x94\xc7\xe72v\xee@/\xce\ -h\x8b\xcf\xf0\x19\x0c\x5c\x89\xdb\xf1\x06\xd1\xfa\xff\xf3s\ -\xcev\x80|b:@\xcf\xb9%\xa6\x13xB\xa5\xd8\ -t\x84\x8c\xa8\x1b\xe0\xf7\x90\x05,G\xe4L\x12\xd5G\ -\xd1\x12[l:\x8c\x15L\xe2\xa8\xff{{\x22\xf3F\ -\xbd\xb4\xed\xc6/\x1aI\xf9\xbe\xe5\x8a\x8a\x9dz\x1br\ -\x83\xe8,\xbc\x97\x89u}L\x071j\xc1\xb4\x17i\ -\x8eU\x81\x1c\x07r/\xb63\xee\x96\x08\xc2yDR\ -\xafP^?=\xd3\xa9\xb8\xb6\xf8\x0c\xaf\x1d@~\xc6\ -F^'\x1a\xbf\x9c\xca\xc6`?y\xcaL\x80\x0b\x07\ -\x09\xc7\x13l\x87\x9dLG\xc8HQa\x80\xdfCy\ -\xedQ\x94*JKF\xd0\x5c\xdd\x08b/*\xacL\ -|\xd7\xef\x01t\x1bSn7\x19\xf5\xd2e\xcf!\xbc\ -\xe6s\x9c\xb1\xcdCf\x86a\x1b\x11k\xab\xf4\x08R\ -E\x0d\xa0b:\x89q\x89\xeaGITOD\x9dC\ -\x80[\xec\x9a\xd0-\xda\x09\x91\xbf\xf2\xe4\xe0'\x19W\ -\xdf\xe3Y \xb6\xf8\x0c\xbf~\xc0\x95\xb4\xb5\xbf\xc8\xb8\ -\xfa1\xa6\xc3\x18\xf2\x8e\xe9\x00=&\x84\xe3\xc9\xb5\xd2\ -\xdft\x84\x8cl\xe4#\xd3\x11\xac\xed\xd6\x0e\xfc\x15'\ -2\x9cD\xec8ZbM\xa1\xda\xbb\xce2\xe2\x81\xfd\ -f\x1e\xa9\xb0\x9f\xcf\xc3|\xb2!\xa5\xad\xdb\xfdj\x15\ -\xbf\x9f~\xf6*.\x94\xa8\xcfcX\xa6)\x95T4\ -\xfc\xdct\x8c\x9c\xd12\xe5y\x12\xb1sP\x1d\x8a\xe8\ -L`\x9d\xe9H\xb9G\x8f\xc0q\x9e$\x1a\xbf\xb2'\ -\x0f\xb7l\xf1\x99?\x86\xe2\xc8B*\xea\xaf\xce\xbf\xa7\ -\xa0\xf2\xb6\xe9\x04=\xa6!)>!\xc8\x8d+>f\ -\xe1\xe4v\xd3!\xac\xaf\x95B\xe4~\x90)\x14\xba\xbb\ -\x91\x88}\x9f\x05\x93_1\x1d\xca\x0a\x0fG\xfc\xdf\xdb\ -S\xe1o\x15+gl\xdc\xde\xd7\xbb\xb8\xbeO\xbd\xc5\ -\xb5So\xf3\x82\xf2\xdfD\x1b\xce1\x1d#\xa7\xb4\xc4\ -\xde\xa0\xb9\xe6\x12\x0a\x93\xfb \x5cI\x90\xfb\x87\xf8B\ -\x0b\x80\xcbik\x7f\x9c\xf1\xb3\x0e\xec\xce\x91\xb6\xf8\xcc\ -/\x82\xca\x0c\xda\xda\x970a\xce\x1e\xa6\xc3d\xd1\xbf\ -L\x07\xc8@\xb0\x9f\x18\x02\x1c^W\x08\x04w\xea\x96\ -\x10\xee\xfd\xcf\x02K\x92\xc0\x03\x88\x9e\x8f\x9b\x1aHs\ -\xf5\xc9$\xaa\xe72o\xeaZ\xd3\xc9\xacp\xa9\xa5\xd6\ -A\xa4\xd2\xefq\x1c\xd5nu\x1e=i\xd9\x8c\xc7\x01\ -\x7fo\xae\x0a\xe3\x17\x0d\x8e\xf7\xf6u\x0c+\x17\x08\xe8\ -,\xaf\xbb\x9a\x86\xc2\xbc\xf3\xdf\xa79\xf6\xff\xe8\xec\x18\ -\x84\xf2\xff\x80wMG\xca1\x87\xe1F\x9e\xa2\x22~\ -\xf6\xf6\x1e`\x8b\xcf\xfc4\x92\x94\xfb\x14\x15\xf1CL\ -\x07\xc9\x0aU\xbf\xd7\xc5\xf8iO\xd3\x0126\xb0\xd7\ -^@p\xd7\x93\xa8\xd8\xe23w\xbc\x0ez=J\x15\ -\xbdt\x00\x89\xd8\x18\x9akf\xd1:\xedc\xd3\xc1\xac\ -\xf0:ax\xbfQ\xa0\xbe\xde\xb0\x15x\xe7\x83\xe5\xbb\ -=\xdc\xbdcDEu\x9e_\x99\xba\x94J\xef\xb5'\ -\xfb<\x86\x95\x1bz#:\x8fh\xc3\xbe\xa6\x83\xe4\xa4\ -\xfb\xa7\x7fFK\xecJJK\x06\xa3T\xa5g\xdb\xd8\ -\xe6D]\x8aQn\xee\x9a]\x19\xd9\xd6\x8b\x83\xbf'\ -\xa4r#-\xb1\xc9_\xf9\xfd\xb2\xda\x02J\x86\xf6\x05\ - \xe9\xf6\xa6\xa03\xddm3\xa9}q\x22\xbb\xa0\xee\ -@`\x00\x8e\x0cDe\x17`\x00\xe8>\xc0P $\ -\xdb[|\xad\xddQ\x1e\xa4\x22~\x22\xcd\xb1\x17L\x87\ -\xf1\x95\xba\xaf \x81\xbd\xcf2\x80\x89u}\x98?=\ -\xb8k\x0e\xd4\x1d\x82\x04\xb7\xf6\x04\x96\x99\x0e\x90\x9f$\ -\x09\xfa\x22*\xcf\x80>\x8a$\xef'q~p\xa7\xd0\ -[\x81%)\xce\xf2\xfb\xf6\x99\xc2\xadUt\x7fm\xb2\ -\x1b\xe1Nq\xf9\x81\x1f\x996Q8\x1d\x98\xef\xe7\x18\ -V\xae\xd0\x9d\x81y\x8c\xa9;\x86\xfb\xa7\xdbi\xa6[\ -\xd2T\xd5\x014\x01M\x94\xcf9\x18\xc7\xfd\x01*\xe7\ -\x80\x96\x9a\x8ef\x9c\xca\x0c\xda\xd6\xedCY\xbc\x92\xc5\ -\xb1\x0d[{Y\xf0\x8b\xcf\xadY\x5c\x9b\xa4']N\ -+\x1b\x8bho\xdf\x1f\xd5\xe1\xa8s \xe8A\x08\xc7\ -\x11\x86\xe9\x8f_\xd5\x0f\xa5\x95\xf1\xf5\xc7\xb1\xa0\xe6u\ -\xd3a|\xb3\xd1YN1)`\x9bwcrRg\ -\xc1\x9e@\x80\xf7$\x94\x80\xdfEU\xbbv\xd0\x7f\x1b\ -\x81\x15(\xcf!<\x8d:\xcf\xd0\xb7\xf89\x9a\xaa\xec\ -\xfe\xaa\x96Q\x8d#j\x8b$%\xa7\xf9\xfdxC\xe9\ -\xde\x94\xdbMV\xbf\xbc\xdb\xe2\x01\xc3\xde[\x8d\x8f\xeb\ -\xea\x05&-*\xab-\x18\x95\xbe\xae\xb26\x11\xb9\x09\ -\xa5\x05t.\xe1\x9aI8\x9c\xc2\xa2[\xa8l\x9cd\ -\x9b\xb5mC\xcb\x94\xe7\x81\xef\x11\xbd\xe9\xa7\x90\xfc.\ -p1h\xb7\xd6?\x86\x8fN\xa0\x98{\xa8l{*}G\xe3\x85\xae\xaf\xdb\x01\ -\xa8\xaduxj\xd0\xc1\xa8\x8c\x07=\x03\x08\xd3t\xd5\ -\xddp\xe5\x0e\xca\xe2G\x7f\xdd]\x8a@[\x1c\xdb@\ -4\xbe\x12\xd8\xdft\x94\x1eq\x9c\xbd\x09r\xf1\xe9\xc8\ -\x104\xd03S^6\x1d $\xda\x81\xb7Q\xfe\x89\ -\xa3+\xc0Y\x01\xba\x02\x97W\xf9\xf6\xaa7\xa9\xadu\ -M\x07\xcc[.'\xa0\x91`\xfc\x8c9v\xd5j\xee\ -\xcb\xdep\x03\xdd~\xe3\x15\xdf\xb7\x8aZy\xd2\xb2K\ -\x96\xf6\xe4\xc0*\xaaR\x0fr\xf5|Ab^\x87\xda\ -Da'\xde\xdf\xa9\x0c\xb8\xdf\xaf1\x82I\xd7\x90\x88\ -\xddLE|\x17\x94?\x98N\xe3\xb1\x0a\xda\xda\x7f\x0f\ -\x5cf:H $\xce]\x03\x5c\x0fz\x03\xe5sN\ -B\xdc\x0b\xc1\x99\xd8\xd5\x94'\x1f\x8d\xa5\xad\xfd.\xca\ -\xe2\xa7n\xa9\xb6\xc8\xd7\x7f)\xdd\x93\xbe(z\xae\xeb\ -\xeb\x97\x8c\x9fu \x1a\xb9\x08e2Pl6\x9c'\ -\x0e\xa1\x98\xdf\x023L\x07\xf1\x8d\xf2\x14\x12\xd0\xe2S\ -\xe5`\xa0\xc5t\x8c\x9e\xd3#L'\xc8@'\x05\x1d\ -\xcf\x9b\x0e\x91\x83>\x03\xd6\x00k\xbb\xfe\xb9\x06\xe13\ -\x94OA\xd7\xa0\xfc\x0bG\xde\x07y\x9b\xa4~@\xc7\ -\xba\xb7Y|a\xdbV\xcf\x16\xe0ww8\xc8\xc7,\ -\x9c\xfc\x81\xe9\x14\xdbeav\x87S\xc4\xf7.\xb7 \ -7er\xb4\x83\xdc\xa5\xe0[\xf1\x09 \xe9\xae\xb7\xb6\ -\xf8\xdc\x92\xe6\xd8UD\x1b\xf6\x06\xbd\xc8t\x14\x8f]\ -Jy\xfcUZb\xd7\x99\x0e\x12\x1c\xa2\xb4\xf0\x00\xf0\ -\x00c\xeb\xf7$\xc2t\x90i\xc0.\xa6\x93\x190\x8e\ ->r\x03p\xde\x97\xff\xc0\x16\x9f=\xb1`\xda\x8b\xc0\ -\xf7\x88\xde\xf0\x7fH\xc1\xff\xa2L!\xc8\x0dU\xd2~\ -@tN\x9c\xc4\x94\xbf\x9b\x0e\xe2\x0b\xe1i\xb6\xf0\x0d\ -\x10\x0c\xfa-\xd3\x09z\xac\xb21B\xdb\xbaC\x83\xbb\ -&_\xfe\x11\xe8\xf5\xb6_\xb5\x1cG+\xb6\xfb\xd5N\ -\xe7\x1a\xdc\xd2\xf4\xb4+WS]ww-+/,\ -\xd9\xff7}\x93\xcax\xdf\xa7\xdcj\xea\xb6L\x8e_\ -\x97da\x9f\x02\xd6\xa8\x8f]\xc5U\xe4\xf4Zj/\ -\xae\xc5\xceP\xd8\xa2\xd2>\x97\xb2\xb6}O\x84SL\ -G\xf1\x940\x93\xf2\xf8\x1b\xb4\xc4\x9aMG\x09\x9c\x85\ -5o\x01\xffIe\xe3\xff\xb0\xb6\xfd\x14\x1c\xb9\x00\xd5\ -\xd1\x04\xbf^\xd8~\xaa\xe7R\x11\x7f\x86\xe6\xd8\xd5\x9b\ -\xff\xb6->3\x91n~\x11\xa3<\xde\x84\xc8\x9c\xae\ -\x85\xdaA\x15\x01\xf7\xf7\xc0h\xd3A|\xe1\xc8c\xb8\ -A-\x80\x02<\xcd{\xdd\x9a\x03 \x12\xe4E\xf8O\ -\x98\x0e\xe0\xb1\x8eP\xaf\xef\xb6,\x0fu8\xc5\xa7\x89\ -\xd2\xc7\xe7a\x9e9i\xf9\xa5\x19My\xaeX9c\ -\xe3\xa2\xfdg&\x10\xce\xf4*\xd4\x16\xecz\xfc\xb0~\ -\xdff\x19\x8f\xf98Fp5U\xa5(\xbb\xf6\x5c\x8a\ -\xfb\xbd\ -\x94\xa8~\x14\xd5:\xd312\xa2L\x22\xb4\x1f.\x9a\ -0\x9d\xa0\xc7\x84\xe0m\xf2=\xa6nG\xe0\x04\xd31\ -2\xf0\x84\xdd\xe7\xcc\xb2\xf2S\xaa\x803\xf1yi\x92\ -\x03\x8bG\xaf\xf8\xa1'O>F\xbdta\x9b\xe0{\ -\x1f\xe0\xc1\x0f\x0f\xbf&\x9c\xb3\xa3\xbc\xd4Z\xfd\x12\xc2\ -wC\xf00\xe2K\xf48\xe8\x1b\xeck\xdc\x5c\xb38\ -\xb6\x81D\xf5\xf5\x94\x96\xec\x87\xc8E\xc0;\xa6#\xf9\ -F\xb4\x96\xb2kK\xc1\x16\x9f\xde\x8b\xe8U\xa6#d\ -h7*\x1a\x82\xdb\xe0\xe6\xebl\x8c\xcc\x0b\xf0\x87\xc1\ -x*\x1b\x83\xb5OiA\xaf3\x80\xc2m\xbe.W\ -\xa9\xeb\xfb46\xcb\xb2r\x93\x90\x85)\xb7\xe2\xcd\x94\ -\xdb\xcf\xcf\x07wyy\xbe-I\xa5\xd4N\xbd\xdd\x1e\ -\xcd\xb1\x16\xc4\xfd\xfe\xb6_\x188S(\x8f_a:\ -D\xe84Uu\xd0\x5c}-\xa5%C@\xff\x87\xf4\ -\xde\xd7a\xd3\x8f\xe2\x92\xb3\xc1\x16\x9f\xdeK7\xf3\x08\ -\xc6~i[\xe3\x86j\xb1\xfc\x17\x1e\x98\xf2\x11\xe8\x22\ -\xd31zhw\xda\xd6\x8f5\x1d\xa2[\x1c\xbd\xd8t\ -\x84\x0c\xb8\x14D\x1aM\x87\xb0,+\xfb\xee\x1bq\xf5\ -^\x0aG\xfb9\x86@GJ#\xde\x16\x8b\xbdSw\ -\x03\x9d\x9e\x9e\xf3KD\xe4t?\xcf\x1f*\xcd5\xb3\ -@~o:\x86\xe7\x84_Q\x1e\xff\xae\xe9\x18\xa1\xd4\ -T\xb5\x9eD\xcd\xcfQ\xf7[\xc0\xc3\xa6\xe3xO\xbf\ -\x0f\xb6\xf8\xf4\x89\x06\xbb\x9b\xa4\xc3p\xd3\x11|\xe3\xf1\ -\x9d\xe6\xec\xd2\x1a\xd3\x09\xb6[E\xc3\x09(A~\x82\ -\xbe\x84{\xa7\xe4C#\x00\xcb\xb2\xbe\xa40\xc59\xf8\ -\xbc\x1d\x82\x0a\x891\xcb.\xf2\xb4\x09\xde\xa8\xe7.\xfb\ -\x14x\xc8\xcbsn\xc1\x88E\xfb]3\xcc\xe71\xc2\ -#1\xe5\xa7\x88\x04\xf8\xbac\x8b\x04\xa1\x9eqs\x8e\ -4\x1d$\xb4Z\xa6.g\xfd\xaa\xd1\xa0\xbf%\xb8{\ -\xd5m\xc9!\x8c\x8d\x0f\xb3\xc5\xa7?6\x98\x0e\x90\xa1\ -\xf0v4+)\xbd\x0d\xf8\xd4t\x8c\x9e\xd1\xd3\x18?\ -\xf7\x00\xd3)\xb6M\x05\xd5\xff5\x9d\x22#\xaa7\x98\ -\x8e`Y\x96\x19\x8a\xf8>\xe5\x16\xfc)HT\xc4\xf7\ -\xa9\xb7\xe2\xb8v\xea\xedv\x13\xa5\xa4\xcfT\xd0\xc7M\ -'\xf1X1\x8e{7c\xeb\xf74\x1d$\xb4\x16\xd7\ -&I\xd4\x5c\x8ej\xb8\xa6oG8\xde\x16\x9f\xbe\x90\ -\xddL'\xc8\x90o\x1bU\x1b\xd7T\xb5\x1e\xc8hC\ -o\x83\x22h\xea\x17\xa6ClSy|2\xc1n4\ -\xf4)\x85\x9dv\xbd\xa7e\xe5\xa1\xfbG\xcc\x1c\x0e\x1c\ -\xe4\xf30\xed\x1d\x85\x1b\xee\xf5\xe3\xc4\xe28w*\xb8\ -~\x9c{\x13El\xf1\xd9\x1dMU\xeb)\xe8\x9c\x04\ -\xb2\xd2t\x14\x8f\xedF\x84{\x18;7?\xf6\xab4\ -\xa5\xa5\xa6\x0e\xf8\x8d\xe9\x18\x9e\x119\xc1\x16\x9f^K\ -\x7f\x13\x06{\xcd\xa4\x86|:\xb6+\x7f&\xa8\xd3\x18\ -\x943\x18W\xefk\xfb\xff\x8cT\xcc\x1a\x84\xc8\xefL\ -\xc7\xc8\x88p\x03\xf3\xa7\xaf3\x1d\xc3\xb2\xac\xecsR\ -r\xae\xdfc\xa8r\xe7\xb8\xe7\x7f\xd2\xee\xc7\xb9G\xbd\ -t\xe1{\x02~?e;\xe2\x81\x03\xfe<\xc8\xe71\ -\xc2e\xfe\xf4\xd5\xa4t\x22\xf0\x89\xe9(\xde\x92C)\ -H\xdd\x1e\xb8\x86\x88ASZ\xf2\x0b\xc2\xd2\x09Wu\ -x\xb8\x8b\x0c\x13\x9c\xe4\x19@o\xd312\xe4\xcb\x87\ -b\xceh\xad~\x09\xf0\xe5\xaes\x16\x08\x8e\xdc\xcc\xc9\ -\xb3w7\x1d\xe4+\xca\xae-E#\xf3\x08\xf6\xb4\xed\ -\x8dt\xba\x7f2\x1d\xc2\xb2\xac\xecST\x04\xf5\xbd\x91\ -\x8a\xa3\xbe\xaf\x01\xf4{\xea\xad8\xd8\xa9\xb7\xdd\xb60\ -\xb6\x0c\x91S\x09['Se\xcf\xee\x11\xb5\ -[\xae\xf4Hs\xf5\xc3\xa8\xc6\x08\xea\xec\xab\xad\xbb\x9c\ -h\xc3\x05\xa6C\x84\xdc\x0b\xa6\x03xdg[|z\ -\xa5\xb6\xd6\xa1\xb8$\x0e\x0c6\x1d\xc5\x03\xc1\xee\xd6\xbb\ -=Z\xa6.\x01M\x98\x8e\x91\x81\x91\xb8\x1b\x1f`\xd2\ -\x0d\xbb\x98\x0eB\xf4\x86oRT\xf4\x10Pf:J\ -\x86\xd6\xe18\xbf2\x1d\xc2\xb2,3\x9c,4\x1a\x12\ -\xd5\xc6#\x96N\xf7u;\x94Q\xcb.{\x03x\xce\ -\xcf1\x5c\xe4\xb8E#\xae\xdd\xd5\xcf1B\xab\xa5\xe6\ -V\xe0\xffL\xc7\xf0\x9e\xfe\x99h\xfdI\xa6S\x84X\ -\xcat\x00\x8f\x14\xd9\xe2\xd3\x0b\xd1\x99\xbdxr\xd0\xcd\ -\xa0g\x98\x8e\xe2\x0d\xf7\x19\xd3\x09\xb2B\xe4r|n\ -\xcc\xe0/9\x94\xce\x82\xe7)o\x18m,By\xfc\ -t(x.\xe0\xdb\xaal\xf2\x07\xbb\xbd\x8ae\xe5\xa7\ -Ee\xb5\x05@\xa5\xdf\xe3\xa8\x92\x95m7\xc4\xe7\xae\ -\xb7\x02\x8e\xa6\x92\x93\xfc\x1c#\xd4\x12\xd5?G\xb9\xd1\ -t\x0c\x8f\x15\x8241!>\xd4t\x90p\x92`\xcf\ -,\xfbB\x9b->3U>\xfb8\xa4\xefs@X\ -6\xdc]\xc3\xfb\xc9\x7f\x98\x0e\x91\x15\xcd\xb1\x17\x10\x82\ ->\x87~ B\x0b\x15\x0dW1\xa6n\xc7\xac\x8dZ\ -1g?\xa2\xf5w\x22\xdc\x01\xf4\xcf\xda\xb8\xfey\x87\ -B7\xd8\x8d\x92,\xcb\xea\xb9w\xfb\x8d\x01|\x9dI\ -\x22\xf0f\xd9\x8a\x19\x8f\xfa9\xc6&\xae\x88\xef\x1d\xbb\ -\xc5v\xbd\xcd\x80(\x1fvL\x05\x1e4\x9d\xc4c;\ -\x91b~V\x97\x05E\xebO\x22\xda0\x96\xca\xc6\xe2\ -\xac\x8d\x99m\x95\x8d\x11\xb2\xb2\x05TV\xbcc\x8b\xcf\ -\x9e\xaa\xa8?\x8a\xf2\xf8\xdd\x88\xf30Jx6\x5cV\ -m\xc1\xe7)A9e\xa3s9\xf0\xa1\xe9\x18\x99\xd1\ -\x02T/\xa3\xb0h9\xd1\xf8\xe5\xbe\xfe\xd0\xaf\x98}\ -\x04\x15\xf18\xea\xbeH\x98.<\xd4\xb9\x88yS\xd7\ -\x9a\x8eaY\x96\x19\x22\xfe_\xd8)z\x8b YY\ -\xebw\xd2\xcb\x17\xbd\x04,\xf3y\x98\xd1\x8f\x1ct]\ -\xee\xf4\x1e\x08\x9a\xa5\xd3;qS\x95\xc0r\xd3Q<\ -\xb6?n\xc7\xddT6\x16ee4\x91\x93A[i\ -k\xff\x88h\xbc\x95h\xc3\x8f\xa8\x88\xfb\xbd]Rv\ -\xb5\xb5_\x0c:\xc4t\x0cO(+\x0bLg\x08\x0e\ -\x15\xa2\xf1\x03P\x19\x8fp\x1e\xcaA\x88\xe9L~p\ -n6\x9d \xab\x1e\x98\xf2\x11\x15\xf1\x1f\xa1\xcc5\x1d\ -\xc5\x03\xbb\x00W\xe2v\xfe'\xd1\xfa;A\xee\xa2\xb4\ -\xa4\xb5ko\xd3\x1eR!:\xf7\x1045\x1e\x91\xd3\ -P\x0e\xf3,m\xce\xd0\xbbh\x99\xe2\xfb\xc6\xec\x96e\ -\xe5\xa6E\x83\xe3\xbd\x95\xb5\xa7\xf8=NJ5+S\ -n7Q\xe4.A\xff\x9f\x8fC\x14&;R\xe3\x81\ -\x9b|\x1c#\xdcZ\xa7}\xcc\xd8\xb9Q\x22\xa9'\x80\ -\x81\xa6\xe3x\xe8\x04\xda\xda\xff\x02L\xcd\xe2\x98\xc5\xc0\ -X\xd0\xb1(\x10\x8d\xbf\x0b,DXH2r?\x0b\ -'\x7f\x90\xc5,\xde\x89\xc6'\x02An\x92\xf9\xef\x84\ -\xa7l\xf1\xb95\x87\xd7\x15\xb2s\xe1A8r\x02\xe8\ -\x090\xe7x\x90\x9d\xc3Ypv\x11\xfeIi\x9f\x05\ -\xa6cd]s\xecF*\xe2\xa7\xa2\x9cn:\x8a7\ -\xb4\x14d20\x99\xb6\xf6N\xa2\xf1\xe7P\x9e\x02Y\ -\x01\xbc\x81\xca{8\xfa\x11)\xb7\x83\xc2^mt\xa6\ -\xbe\x81tD\xa0p'\x1cw\x17D\xf6B\x19\x8c\xba\ -\x87#\x0d\x87\x01;\x22\xa1}\xe3\xbfG\xaa\xe0{\xa6\ -CX\x96Q\x91T1e\xd7\x96\x9a\x8e\x91\x91\xa2\xbe\ -\xca\xc2\xc9=\xda&Lz\xb5MT\xf0w\xd9\x82\xc8\ -+c\x96]\xfa\xbc\xafc|yH\xe5N\x04?\x8b\ -O\x10N\xc7\x16\x9f\x99Y8\xf9\x9f\x94\xc7' ,\ -\x06\xfa\x98\x8e\xe3\xa1\x1a\xca\xeb_\xa4\xa5\xe6\x8f\x86\xc6\ -\xdf\x0d\x98\x822\x85H\xca%\x1a\x7f\x0e\xf4~\x90%\ -t8\x8f\xf1\xc0\x94\x8f\x0c\xe5\xda>e\xb5\x05\x14\x0f\ -\xfe!\xf0K\xd0\xf0\xd4k\x8e.\x0e\xcf_\xa6'\xc6\ -\xd4\xedHQ\xef]\xc0\x1d\x84\xcbP\x84\xa1\xc0~\xc0\ -P\xd2]k\x0b\xd3/\x14\xc2\xd7\x15{\x0b\x5c\xf9%\ -MUa\xe9\xa6\xd5=R4\x0d\xed\x18\x09\xeci:\ -\x8a\xc7\x0a\x81\x91\x08#?\x7f\x0fo\x9a\xf5\x15\x11p\ -; \x02\xe9\xff\xe9\xea\xbd\xa4\x9b^\x17\xda\x82s\x13\ -\xc5\x95i\x81\xbd\x1bjY^Q\xe7i\x8a\x83~\xcd\ -\x9b\xda@\xfa\xc9G\xb7\xa9\xe0\xffZ*\xd7\xcd\xfa\xac\ -\xa2Q\xcb/~f\xd1\xb0\x99\xff\xc4\xd7\xedc\xb4\xbc\ -\xf5\xe0\xdf\x95\x8c{\xfe'\xe1\xde\x1f\xdco-\xb1\xa7\ -)\x8fW#\xdc\x06\x84gI\x9c\xc8\xef)ox\x8d\ -\x96\xeay\x86\x938\xc0a \x87\x01?\xa5\xc8U\xa2\ -\xf5\xcb@\x1eGt\x09nd)\x1b\xfe\xf92\x8bk\ -\x93\x86s\xa6\x8d\x8b\x1f\x8f\xc3\x9fA\xc3\xd2dh\x93\ -w\x19\xf9\xe6\x13\xc1/>\x85A\x94\xc7+q\xb4\x14\ -u\x0aQz#Z\x0cR\x00\xda7\xfd\x22\xed\x87\xc8\ -\xce(\xfdA\xfa\x83\xeeL\xbaIJ!\xean:O\ -\x9e\x93\x17\xd9\xf0\xc6\x1c\xd3)\x8cYp\xce'T4\ -\x9c\x8b\xea\xfd|~\xd3\xc1\x0a5\x95+i\xad\xce\xbf\ -'\xfd\x96e}\xaey\xc8\xcc\x1d@\xa3\xbe\x0fTP\ -p\xbb\xefcl\x89r\x0f\xc2\xa5>\x8eP\x5c\xd8\xd1\ -\xbb\x1c\xb8\xc3\xc71\xf2CK\xac\x89\x8a\xfa!\xa8\x84\ -i\xcb/\x07\xe1f\xc6\xcd>\x8e\xd6\xa9\xb9\xd4\xccR\ -@\x0e\x00\x0e@\xa5\x06q\xa1x\xd0z\xa2\xf1\x7f \ -<\x0b\xfawT^\xa4\x17\xcb\xb8;\xf6iV\x12\x8d\ -\x9e\xd3\x9f\x22\xf7l\x84\xa9!\xd9A\xe0\xab\x849\xd4\ -\xd6\xba\xc1/>\xe1\x04\x84\x13\xd0\xae\xa7\x93\x9f\x17\x91\ -\x9b?\xa9\x94\xcd~\x99\x07O0\xbb\xcf\xc5q\xa7\xe7\ -\xcc\x1d\x1fS\x9a\xab\x1f&Z\x7f\x09\xc8u\xa6\xa3X\ ->SZ\xe8\xdb\xe7\xbfL\xc7\xb0,\xcb\xac\xe2B\xa9\ -D\xb5\xb7\xcf\xc3<>\xea\xa5\x0bW\xfa<\xc6\x16\xb9\ -\x8e{\x97\xa3\x8e\x9f\xc5'\x82\x9e\x86->\xbd\xd1\x5c\ -\xf3k\xa2\x0d{\x82~\xdft\x14\xefh)\x8e\xb3\x80\ -\x09s\x8e\xca\xf1\xed\xcc\x8a\x81o\xa3|\xfb\xf3bb\ -#\x9b\xd6\x8e\xbe\x02\xb2\x1ct\x15\xca*pV\x91J\ -\xae\xe2\x1b}\xdf\xef\xd1\x8c\xc1\xe8M; \x9d\x83P\ -\x1d\x0erl\xba\x8eq\x0f\x02\x9c\x10\x97)\x1d$\xf5\ -:\x800\x14\x9fV\xe6~\xc9\x82\x9a\xc7L\x87\xc8\x09\ -\x89\x9a\xbfP\x11?\x10\xe5\x07\xa6\xa3X>\x11\xfeA\ -\xa1[\x95\xb7S\xcc-\xcb\xfa\x82\xabg\xf9=\xf3I\ -\xc8n\xa3\xa1\xcd=\xf2\xca\xa7KN\x1c\xb6\xd3{\xc0\ -\xae>\x0e3\xa1qDmQ\xd5K\xb5\x1d>\x8e\x91\ -?>\xd8x\x09\x03\x8b\xf6\x05\xc6\x9a\x8e\xe2\xa1=H\ -\xb9\xf70\xb1\xee\x04\xe6O_g:L7\xed\x96\xfe\ -\xd2\x93\x80\xae\xba\xd4\x85\x02\x07\xda\xda!\x1a\xff\x94\xf4\ -\xae\x09\x1f#\xb2\x16t#\xb0\x0eW\x15\xc7I\xa1\x14\ -\x22|\x03\xd5B\xa0\x1f\xf0M\xe8\xdc)]dv\xfd\ -\xf0\x09o\xc1\xb9\x19\xb9\x9e\x85\xb1\xb7 L\xf3\xca\xad\ -\x1e\xd2\x04\xa5%\xbf0\x9d\x22\xa7\x1c\xb9\xeabT\x1b\ -M\xc7\xb0|\xf1\x1aPn\xb7U\xb1,k\xd1\x88k\ -wE(\xf3y\x98T\xb2\xc0\xdc\xe7I-\xb5\xae \ -~\xaf\xb7\xdbq`g\xff\xd1>\x8f\x91?\x96N\xef\ -\xa4\xd0=\x03\xc8j\x83\xaa,8\x9cd\xaf9\xa4\xa7\ -*\x86\xc97H\xf7\x8a9\x0a\xd51(\xe3Q*\x11\ -\xa9B\xf5,\xd03P\x1d\x03\x9c\x08\x1c\x0c\xecd4\ -\xad\x19\xef\xd2!\xb5\x9b~a\x8b\xcf\xbc\xa6\x8f\x93*\ -\xa8\xb4O\x80\xbe\xa4\xb6\xd6E\xda&\x03\x0bMG\xb1\ -\xbc$o\x93\x8a\x9cLs\xec=\xd3I,\xcb\xca\x01\ -n\xea\xbbt\xb5\x5c\xf3\x8b\xc0\x03c^\xbc\xf4}?\ -\xc7\xd8\x16\x17\xee\xf4}\x0cG\xc3\xb3\xefs.\x987\ -u-\x11\xa7\x02\xe4m\xd3Q\xbc\xa5gP\xd1\xf0s\ -\xd3)\xac\xacR\x94i\x9bw\x17\xb6\xc5g\xbe\x12\xfe\ -\x81\xd3k|O[\xd3\x87^b\xc6FJK&\x02\ -w\x9b\x8ebyb\x15P\xc6\xc2\xc9\xff4\x1d\xc4\xb2\ -\xac\xdc \xf8\xdf\xe5V\xc1\xd8\x94\xdbMv()z\ -\x10\xf8\xc4\xcf1\x04N[TVk\x97ry\xe9\xde\ -)\xff\xc2uO\x01\xc2u\x9d\xa6\xfc7\xe5\xf5\xe7\x9a\ -\x8eae\x89\xf0\x17Zb\xcd\x9b\xff\x96->\xf3\x91\ -\xf0\x08\x1b\x9d\xd1,8\xc7\xd7\x0f\xa3\xc0k\xaa\xea\xa0\ -\xb4\xe4L\x90\xbf\x99\x8ebeB_\x81\xe4q$\xaa\ -_3\x9d\xc4\xb2\xac\xdc\xf0\xc0\x90\x99\xfb\xaa2\xd2\xe7\ -a6$;z\xdd\xe5\xf3\x18\xdbt\xc4\xd2\xe9\x9d\xaa\ -\xdc\xeb\xf30;\xf3^\xff\xe3|\x1e#\xff\xb4\xd6<\ -\x8b+g\x02a\x9a\xa1&\x88\xdc@\xf9\x9c\xa3M\x07\ -\xb1|\xf70%%?\xfe\xf2o\xda\xe23\xff\xdc@\ -I\xc9\x98\x9c\xdf\x5c7W4Uu\x90\x98R\x85`\ -\xd7\xc5\x06\xd3\xa3\x14t\x9e@\xe2\xfc\x90M]\xb2,\ -+\x13R\xc09\xf8\xbf\xc9\xda\xbd'\xbf>\xfd3\x9f\ -\xc7\xd8.\x8e\xa3\xbe\x17\xc1]]o-\xaf\xb5V/\ -@\xf8\x91\xe9\x18\x1e\xeb\x8d\xe8<\xa2\x0d\xfb\x9a\x0eb\ -\xf9\xe69zq\x0aMU\xeb\xbf\xfc\x07\xb6\xf8\xcc\x1b\ -\x92D\xe4\x22\x12\xb1\x0bh\xaa\xb2\x1d\xe9\xbaE\x94\xe6\ -X-\xc2\xf7\x01\xfb\xef.0$Ni\xc9I\xcc\x9f\ -\xbe\xdat\x12\xcb\xb2r\x8b\x03g\xfa=F.L\xb9\ -\xdd\xa4hm\xaa\x05\x9f\xa7o*\xf2\x1d\x0d_3\x99\ -\xdc\xd0\x1c\xbb\x1a\xd1\x99\xa6cxKw\x06\x9d\xc7\x98\ -\xba\x1dM'\xb1<\xb7\x9c\xc2d\xf9\xd6\xf6H\xb5\xc5\ -g~x\x1e\xd5ch\xae\xbe\xd6t\x90@k\x8e\xfd\ -\x15G\x8eA\xb0\xeb\x06s\xdb\x06\x84KIT\xd7\xd8\ -\x1b-\x96e}\xd9C\xc3g\x1e\xaa0\xdc\xcf1\x04\ -\xd6\xf4nK&\xfc\x1c\xa3;\x8ey\xfb\x87\xebA[\ -\xfc\x1dE\xf7xp\xbfk\xfc\x9e\xca\x9c\xbf\x8e|\xf3\ -2\xc2\xd7\x87b8\x85E\xb7c\xd7\x0b\x87\x88,!\ -\x159\x81y\xe7o\xb5\xd1\x9a->\xc3m\x03\xc2/\ -(-\x19IK\xeci\xd3aBaA\xf5RR\xa9\ -#\xb0\x1bj\xe7\xaa\x97\x10\x8e\xa69v\xb5\xe9 \x96\ -e\xe5\xa6\x94\xfa\xdfh\xc8E\xefH\x17|9D\xc5\ -\xf7\xa9\xb7\x8e\xedz\xeb\x9f\xdaZ\x97\xd2\x92\xb3\x11\x9e\ -2\x1d\xc5c\xe3(\x1e\xf4{\xd3!,/\xe8\xf5|\ -\xb0\xf1$\x16N\xfe\xe0\xeb^e\x8b\xcf\xb0\x12\x16\x10\ -\xe1`\x9ac\xb5\xf6\xe9\x8f\xc7Z\xa7}L\x22v\x06\ -J\x15\x88\x9d\xd2\x99\x1b\x14\xf4z\x0a:\x8e\xa49\xf6\ -\x9c\xe90\x96e\xe5&EE\x94*\xbf\xc7q$\x92\ -3Sn7)d\xfd<`\x83\xbf\xa3\xc8w\xfc=\ -\x7f\x9ek\xaaZO\xaa\xe0T\xe0M\xd3Q\ -\xd0]V/\xf2s\x8c\x9e8n\xf9\xe5kE\xe5A\ -\x9f\x87\x19\xba\xe8\x80\xab\x0f\xf4y\x8c\xfc\xd6z\xde\xbb\ -\xa0Q`\x8b\xeb\xe9\x02K\x98\xc9\xb8\x86\xf1\xa6cX\ -\xdd\xf6\x04\xe8a\xb4\xc4\xae\xdb\xde\x03l\xf1\x19\x1eO\ -\x22\x8c\xa5%v,\x89\x9a\x87L\x87\xc9\x1b\xf3\xce\x7f\ -\x9fD\xcd\x14\x1c=\x0eXj:N\x9e\xf9\x18\x91\x8b\ -(-9\x92D\xec\x09\xd3a,\xcb\xca}Y\xd9\xdb\ -S\xf4\xd6Q\x8bk\x93~\x8f\xd3\x13\xea\xb8\xfeo\xfd\ -\xa2\x8e}\xfa\xe9\xb7D\xcd\xcb8\xce\xe9\x84\xab\x09b\ -\x04Goa\xfc,{\xf3\x22\x18\xd6\x03WPZr\ -\x1c\x89\x9a\x97\xbbs\xa0->\x83\xad\x03\xa1\x09WO\ -&\x11\xfb6\xcd\xb1\xfbL\x07\xca[\x0bj\x1e#Q\ -=2=\x15\x97\x15\xa6\xe3\x84\xdc:\xe07\xf4b_\ -\x9a\xab\xaf\xa5\xa9*L\xfb\x9fY\x96\xe5\x93g\x0e\xaf\ -+\x04\xce\xf0{\x1c\x15\xcd\xb9)\xb7\x9fKu\xde\x0d\ -\xf8\x5c\x18\xdbu\x9fY\xb1`\xca\x22T\xc26Uu\ -\x0742\x8f\xb1s\x07\x9a\x0ebm\x95\x22\xdc\x89\x9b\ -:\x98D\xec7=\xb9\x06\xb3\xc5g0\xbd\x07\xfc\x06\ -\x92\xfb\xd2\x1c\xab\xa2\xb5\xe6~\xd3\x81,\x00QZb\ -M\xac_5\x02d\x0a\xc8\x8b\xa6\x13\x85\x8b\xb4!\xf2\ -G\x92\xeeP\x12\xb1+\xb6\xd6\xc2\xdb\xb2,kK\xd6\ -\xb6w\x8e\x03\xfa\xfb9\x86 \xaf\x8fz\xf9\x92\x9cm\ -\x083j\xc5\x8fW\x03K|\x1e\xe6[\x0f\x0c\x99i\ -\xf7o\xcc\x86\x96\xea\xd9\xa0\xbf5\x1d\xc3S\xca\xdeD\ -\xdc;\x88\xce\xece:\x8a\xf5e\xfa8.'\xd2\x1c\ -\xfb\x0e\xad\xd3V\xf6\xf4,\xb6\xb5qp|\x82r/\ -B\x13\x1ft\xb4l\xcf\x82^\xcb\x90\xf4t\xab\xb9\xa0\ -72nN\x05\x11.Eu4\xfeoh\x1eV\xef\ -\x00\x7f\xc5M^K\xeb\xb4\x8fM\x87\xb1,+\x98\x84\ -\xd4Y\xea\xf3\x8fa\x15\xbdE\x90\x9c\xee\x01\xa0\xca]\ -\x22\x94\xf99F$\xc2\xe9\xc0\xef\xfc\x1c\xc3\xea\x92\x88\ -]A\xb4\xe1\x9b\xc0\xd9\xa6\xa3xG\x8f\x83\xd2\xeb\x81\ -)\xa6\x93X\x00\xfaw\x5c\xe7\xbfh\x8d-\xf0\xe2l\ -\xb6\xf8\xccm\x1f\x01\xf7\x82s;\x1fl\xb8\xdf\x16\x9c\ -A#J+\x0b\x80\x05\x8c\x9b5\x04\xc79?\xfdD\ -\x94]L'\x0b\x00\x17X\x84\xc8_X\xf7\xc6=\xe4\ -\xe8\xfa)\xcb\xb2\x82a\xfe\xe1u}\xb4}\xe3$\xbf\ -\xc7Qqn\xf3{\x8cL\xa9&\xef\x10)\xf8\x13>\ -\xde\x10U\xe14l\xf1\x99%\xa2\xac\x8fO\xa57\x83\ -\x11\x8e1\x9d\xc6;2\x99h|9\x89\xd8\xafL'\ -\xc9S.\x22\x0f\xa2:\x93D\xec^<\xbc\xa9f\xa7\ -\xdd\xe6\x14I\x92nZ\xf3\x1b\x5c=\x99\xf5\xabv%\ -\x11\xab&1%a\x0b\xcf\x80k\x9d\xb6\x92D\xcd\xe5\ -\x94\x96\xec\x012\x0e\x98\x03|f:V\x0ez\x01\xe4\ -rR:\x98Dl\x0c\xcd\xd5w\xd8\xc2\xd3\xb2\xacL\ -\x95\xb4w\x9c\x0a\x94\xfa:\x88\xcas'\xbd|\xd1K\ -\xbe\x8e\xe1\x81\xd1+~\xf8/\x11\xfc\xde\xfb\xfb\xdb\x8b\ -\x86\xcc\xfc\xa6\xcfcX\x9b,\x8em\xa0\xd3\x99\x04\x84\ -m\x97\x83\xff\xa3<\xfe]\xd3!\xf2\xcc\x1a\xe0O8\ -:\x94\xe6\xea\x93I\xc4\xe6{Yx\x82}\xf2i\xda\ -\x87\xc0\xd3\x08O\xa3\xf2\x18\x05\x1b\x97\xd8\xad\x22B.\ -\xbd0{!\xb0\x90\xca\xc6\x22\xda\xdbOD\x99\x880\ -\x01eo\xd3\xf1\x0cH\x81<\x8e\xea<\x0a\xb8\xdbn\ -\x0fdY\x96\x1f\x04\xf7,\xbfW>\xa8C\xee6\x1a\ -\xfa\x12\x85;\x81#}\x1cB(\xd4S\x80k}\x1c\ -\xc3\xda\xdc\x03S>\x22\xda\x10%\xbd\xf5\xc5\xce\xa6\xe3\ -xD\x10\xea\xa9\xa8\xff'\xcd5O\x9a\x0e\x13b)\ -D\x16\xa1\xdcH\xca\xb9\x83\x85\x93\xdb\xfd\x1c\xcc\x16\x9f\ -\xd9\x91\x02V\xa1\xac\xc0\xe1\x05T\x9fB\xe5\x19Zb\ -o\x98\x0ef\x19\xd4T\xd5\x01\xdc\xd7\xf55\x83\x93g\ -\xefN\xc49\x16G\x8fC\xe5X\xe0PB7;A\ -\x92\xa0\xff@\xf4Q\x5cYB\xa4\xe8~\x16\x9c\xf3\x89\ -\xe9T\x96e\x85\xd7#\x07]\xd7/\xd5\x99\x1c\xeb\xf3\ -BL\x15M6\xfa;\x84\x87\x9c\xc8\x1d\xa4RW\xfa\ -9\x84\xa8\x9c\x8e->\xb3+Q\xfd\x1a\xe3\xe2\xa7\xe3\ -p\x1f\x10\x96\x86=\xc5\xa8\xdc\xc5\xd8\xfa\xa3XX\xf3\ -\x96\xe90!\x92Bx\x0c\xd5[\xe9\x884\xf2\xc0\x94\ -\x8f\xb25\xb0->\xbd\xf31\xc8;\xa0\xffBx\x1b\ -\xe5U\xd4Y\x81\xea\x0a\x9c5+I\xcc\xd8h:\xa0\ -\x95\xe3\xee\x9b\xfa\x0e\xd0\xd4\xf5\x05\xa7\xc6\xbf\xc1\x06\x8e\ -A\xf4\x18p\xbe\x05\x0c\x03\xdd\x1b\x88\x18L\xd9\x1d\x9d\ -\xa0+\x11y\x05\x95gq\xf5a6\xea\xd3,\x8em\ -0\x1d\xcc\xb2\xac\xfc\x91\xecLV\x01E\xbe\x0e\xa2,\ -\x19\xb5\xfc\xb27|\x1d\xc3C\xa3^\xbap\xe5\xa2a\ -3_\x00\x0e\xf2k\x0c\x85\x13\x1f\x1e2s\xc0\x09+\ -g|\xe8\xd7\x18\xd6\x16\xb4\xc6\x1e\xa1<^\x8dp\x0b\ -\xe1it\xb8\x1b\x11\x99G\xd9\xb5\xc7\xb3\xf8\xc2\xb6\x7f\ -\xfb\x93\x82\xe4\x9f\xe8(\xf8\x07\x22\xc7\x82{,\xc8\xc1\ -\x04\xe7:)\xdb\xdeChA\xb5\x05\xd7\xbd\xcfT\x13\ -\xc7\x02\xa0\x0d\xc8\xe7'\x0f\x9b\xff\xdd\xd7\x03]\x17\xc6\ -\xa2\x08\x9f\xa2\xeeZ\x905\xc0\x1aT>\x03]\x83\xc8\ -g\xe0~\x8c\xea{H\xe4]\xd6\xeb\xbf\xc2{A\xed\ -n\x04\xf9\xc0t\x8an\xe9\xb5>\xa7;\x0dn\xb7\xf4\ -V\x22\xcd]_i\x95\x8dE\xacY7\x94\x08\xc3P\ -\xdd\x1f\x91\x03Pw_\x90=\x80\x01@q\x96Sn\ -\x04> \xdd\x91v%\xf02\xca2\x5c^\xe6Qf\ -\xb8\x87\x00\x00\x0a,IDAT\xa3\x8e\xd7\xecZe\ -\xcb\xb2r\xc0Y~\x0f \x04g\xca\xed\x17\xf4N\xfe\ -\x7f{w\x1f[gY\xc6q\xfcw=\xe7\xb4\xdd\xd6\ -\xbd\xb0\x01\xc3\x81Qt\x84U\x99\x89\x91%F\x82a\ -g\xa0\x81\xc9P T\xffBA\xc9\x0c\xb0\xce\x11\xc9\ -\x12\x8d2\x8c1\x91?H\x18\x10\x82N\x11\x83dL\ -\x1802;\x84\xd1\x8e\xe8$\x84\x22\x09nk'\x1a\ -\xde\x06\xe3\xc5\xbdv\xdd\xba\xf6<\x97\x7f\xb4\x85mm\ -\xb7\xb3\xf6\xdc\xe7~z\xfa\xfd$\xa7\xcd\xd6\xd3\xe7\xfa\ -\x9d\x93\x93\x9c\xde\xe7\xbe\xee\xfb\x96\x05\x1b|J\xca\xf5\ -\xe6\xb4H\xd2\xef\x03\xd6\xc0P6\x5c\xb7Z\x97>0\ -G\xa6\x15\xb1\xa3\x94\xd1\x175q\xd2#\xbaf\xcd\x15\ -G\x9d-\xb9\xee\x86\xf7$\xad\xee\xbfIW\xfcn\x8a\ -\xba\xed\xcb2\x9b'\xb3\xf3%\x9f'\xe9\xec\x18\x813\ -\xe0]\xc9_\x90\xb4Y\xcamT\xf3\xb5\xaf\x94{\xfd\ -\xe6H\xe4\xd5|]\xf0\x03\x971\x865_\xbfI\xec\ -\xce\x9a\x1d}\xad\xba[\xfao\x83\xcd\xbfw\xb2&\xd6\ -\x9f!\xb7\x99\x92\x9f.\xf33d~\x9a\xdc\xa6I\x92\ -\x5c\x93e^#\xb3ZI\xf5\xfd\xffw\x8a\xccL\xee\ -\x07e:$WQ\xf2}\x92$\xb3\xbdr\xa5\x92u\ -I\xe9.I;\x95\xda{r}\xa0\xf4\xf0\xbbzv\ -\xf1\xf8\xda4\xc9\xf5\xa2\xcc\x7f\x13;\xc6\xc8%;b\ -'\xa8>\xc5\xb7\xe5\xc9\xd2\xd8)\xe0CnL\xf6\xd7\ -9w\x9f\xe9\xf2\xaf\x06\x9e\xfe\xe9\xed\xee\xady,l\ -\x89\xf2\xf3\xd4\x1e\xb7D\xb7\x85\xaca\xe6W\x8a\xc1g\ -\x1c\x1b\xbe\xf7\x0b]\xf6\x87\xcf\xa8\xba\x8e+Y\xa8\xce\ -\xce_IZ>\xec=\xd6}\x7f\xbf\xa4g\xfbo}\ -.~\xf0T\xd5\xa5_R\xeas\x95\xd8yr\xcd\x95\ -\xf4yIS\x02\xe7\xad\xa4]\x92m\x95\xbcM\xae\x17\ -T\x93\xdb\xac\xa7\xae}\xf3\xe8\xbbd\xe3\xa5P-\xd3\ -\xf1\x00\x00\x00Gi\x9d\xb3\xf2V7\xdd\x11\xb8\xcc\xfa\ -B{\xd3\xe5\x81k\x04\xd1\xda\xb0\xb2\xc3\xa5s\x03\x96\ -\xe8>\xd8\xab\x99\x0b_k\xda7\xe8'\x97=\xf0\xa8\ -\xfa\xf66\x18\xa3\xfc!5_\x1ft\xf0>j\xd7\xac\ -\xa9Ug\xe7\x9f%\x9b\x1b;Jy%7\xab\xf9\xbb\ -\xcd\xa3\xbe\xcc\xc2U\x9fV\x9a?G\xa6\xd9R:[\ -f\xe7H\x9a-\xb7\xb32\xbaiS\x97Lo\xca\xf5\ -\x96d\x1dR\xbaUI\xae]\xb9\xc3[\xfbg\x80\xc7\ -\x04\xd6|\x02\x00\x80\xaa\xe4\x16\xbe\xe5V>\x16[n\ -\xfb\xa4\xa6'\xcduk\xc0\x12u\x13k|\xa1\x06Z\ -\x22\x8fD\xe7]x}\xddR\xdf\x8c\x1d#\xb3\xfe\xf2\ -\x837$\xbd!i\xe3\xa0\x9f\xcd\x7f`\x82&\xdaY\ -J\xfdL\x99\x7fRf\xa7\xc9t\xaaR\xf5}7;\ -M\xee\xd3$\x9b(\xf9$I\xd3\xd5\xb7\xf4iB\x09\ -\x95?^\xf2g\xda#\xf7=r\xed\x91i\x8f\x94\xf4\ -}w\xed\x91\xb4S\xa9v(\xe7o\xaaX\xdc\x11k\ -\x8df\xb9\xe5[\x1a\xee\x9e/\xf3\x92\x07\xa1\xe6\xc9\x81\ -T\xc5\xc3!C\x9d\x0c3u\x17\x8bif\x8e'\xb1\ -\x5cM\xb16\x9f\x0c\xfe\x84/\xa2\x8d\xaf\xbe\xbfw\x85\ -V\xa4\xb1s\x00\x00\x10\xca\x9a\xf3V\xd4\xceJf\xd6\ -\x0f\xfc\xbb\xa7\xa7\xe7\xb3\x0a<\xb3fR\x97\xe7sO\ -\x86\xac\x11\x92\xf5\x1d\xb9\x12r\xf0)\xc9\xae\xd2P\x83\ -O \xcb\xfa\xf6r\xf9O\xffmd\xbe\xfe\xc7z\xd5\ -\xe4ju`O\xcf\xa0\x8d\x92\xc61kmX\xb9\xd7\ -\xa5\xa9\xb1\x83`\x5c9 )3\x1f`H\xea6Y\ -f>\xc0py*)Sk)M\xda\xebRf>\ -\xc00\xa9\xcbM\xec \x8d\x91s\x9f(Y)\x9fP\ -\x8f-\xae:\x99&\x8d\xf62&M\x91lT\xddQ\ -.\xcf\xa9<\x7f_\x94:\x9bPy\xaeG\x0a\x1dM\ -\xdf\x89\x1dc\xa4\x5cn\xad\x0d\xf7\xbc%\xf9Y\x01\xcb\ -t\xd6u\xf6\xce\xbc\xe0\xed[\x0e\x06\xac\x01`\x8c\xc8\ -;\xeb>Qy\xf5\x1a\xd8\xec&#\x5c\xd17\xff\xca\ -\xb4\xac=;\xfe\xd1\x17`\xa4\xaa\xf4\xad\xafL\x0f\xcb\ -\x8f\xf8\x8a\xe1\xb9|\xcc\xb6\xdcJ\x92\xc9\xbcE+\x9f\ -\x90tS\xc02\x93\x0f\xd5\xd7|M\xd2\xbaR\xee\xfc\ -\xb79\xbf\x9era\xc7\xf2\xfd\x01\xf3\x00\x88(\xaf\xaa\ -}\x07\x06\x00\x00\x08f\xf7\x87\xf9\xdd\xa3\xdf\xf4$\xb2\ -\xd4mmb\x1er\xf0)3]\xa5!\x06\x9f-\xf3\ -W\xe4\xed\xbd\xe9\x8b\xdc\xedj\x93}\xc5\xe5\x9f\xea\x91\ -\xf2-\x0d+C\xc69\xd2^U\xa0\xab\xc7\xa4\xfd\x92\ -\x0d\xb9#syy\xa7K\xe1\x8f8su\xa9\x22\xdd\ -G~Hf\xe1g\xcc]\x87Mv |\x19\xef\x91\ -+x\xfb\xad\x99\x8an\x0a\xbe\x04\xd0R\xa5\xa9yI\ -\x9dz\x85\xf6\xa6\xdfZ\xff1/l8\x04\x00\x00p\ -\x92\x5cz\xb4q\xcb\x8a,-!\x19\x91d\xd6\xff\x9e\ -\xd7\xce\x19\x1fJ\x0a\xb6\xbb\xa7\xc9\x17\xbdt\xfe\xfd5\ -\xf3\x8e8\xfb\xb9\xb5a\xe5\x22\xdf\xa9;]:G\x8a\ -\xd6\x814\xad\x12E\x5c\x9a^U\x9d\x04\x15\x9b\xb6\xb2\ -\x8a=m\x15{\xfdU\xe0\xb9\xabTw\x98\x9bd%\ ->\xa0\xdbu\xfb*\xf5\xa7J\xc4\xcc'\x00\x00\xc0I\ -I|l\xb7\xdc\x0e(\xb4\xae\xe85+\xad%v\xa4\ -\x5c\x9a\xd1\xd9\xd53_\xea\xdb\x18\xaau\xce]\xf7\xba\ -\xf4\xa4\xfa\x07\x9e\x00\xaa\xdbm\xba\xed\xa3\xe1pb\x0c\ ->\x01\x00\x00Jf\xd2;\xefw\xccz>v\x8er\ -qO\x1e\x0f]#Uz\xe5\x86\xf3\xee\x9cqzq\ -\xc6\xd3nv\xa3\xf8\xfb\x13\x187\x06Zn\xa5\xbe\x99\ -O\x00\x00\x00\x94nu\xa3\x1a\x8b\xb1C\x94\xcb\xc1\xde\ -\xf4\x19S\xd85b\x89\xeb\xea\x09\xc5\xfc?$\xcd\x0f\ -Y\x07@\xb6%\xecv\x0b\x00\x00P\xbaT\xfep\xec\ -\x0c\xe5\xb4\xf0\xb5\xa6\xeeTZ\x1f\xb2\x86K3]:\ -7d\x0d\x00\x99t\xd4\x0aTf>\x01\x00\x00Jd\ -\xd2\xf6\x05\xedK\xdbb\xe7(\xb7\xc4-x\xeb-\x00\ -\xb0\xe1\x10\x00\x00@\x89\x5cVU\xb3\x9e\x03\xf6O\xae\ -]oRW\xec\x1c\x00\xaa\x8b\x0f1\xf3\xc9\xe0\x13\x00\ -\x00\xa0\x04EYU\xecr{\xacEm\x8b\xbb\xdc\xf4\ -L\xec\x1c\x00\xaa\x1bm\xb7\x00\x00\x00\xa5y\xe9\x92\xf6\ -\x9b\xb7\xc7\x0e\x11\x8c;\xad\xb7\x00\xca\xea\xd8\xd3Z\x19\ -|\x02\x00\x00\x94fu\xec\x00!u\xe7\x8aOI\xea\ -\x89\x9d\x03@\xf5\xe2\x9cO\x00\x00\x80\x13p)M\xd3\ -\xde\xaa\x1e|^\xba\xe5\x96]\x92\xb7\xc6\xce\x01\xa0\xaa\ -\x1c=\xf3\xc9Q+\x00\x00\x00\xc7\x97H\xad\x17o\xbf\ -eG\xec\x1c\xc1y\xb26v\x04\x00U\xe5\xe8\xc1\xe7\ -\xb1}\xb8\x00\x00\x00\x18\xa4\xaag=?\x92O\x9ep\ -)\x8d\x1d\x03@uJ\x9cu\x9f\x00\x00\x00\xc32\xe9\ -\xf0\xa1\x5c\xefc\xb1sTBa\xcbM;\xcd\xb49\ -v\x0e\x00\xd5a\xa8\x0d\x87\x98\xf9\x04\x00\x00\x18\x86\x9b\ -\x9a\xfb\xd6C\x8e\x0f\xce\xae\xb7\x00\x02I\xc4\xcc'\x00\ -\x00\xc0\xf0\x0c>\ -\x01\x00\x00\x86\xe6.\xfbq\xa3\x1a\x8b\xb1\x83T\x9a\xbb\ -\xb3\xeb-\x80r\xe0\xa8\x15\x00\x00\x80\x131\xf7\xfb\x16\ -\xb4/y:v\x8e\x18\x924e\xf0\x09\xa0\xec\x98\xf9\ -\x04\x00\x00\x18l\xc3\xe4\xc9\x13~\x14;D,\x17\xfd\ -{\xd96\x99m\x8b\x9d\x03\xc0\x987\xe8\x9cOf>\ -\x01\x00\x00>\xf6\xa7\x83\xbd\xfa\xd6\xbc\xb6\xc5=\xb1\x83\ -\xc4d\xecz\x0b\xa0\xcc\xf2\xb1\x03\x00\x00\x00d\x81I\ -\xef\xb8\xdb\xf2B\xc7\x92\x87bg\xc9\x82T\xbe\xd6d\ -?\x89\x9d\x03\xc0\xd85\xd49\x9f\xcc|\x02\x00\x80\xf1\ -j\xb7\xa4\xf5\x92\xae\xf3CSf3\xf0\xfcX\xa1\xbd\ -\xe9eI\xaf\xc7\xce\x01\xa0z\xe4\x0b\xedM'5\xf8\ -\x5c\xa35\xb9Y_\xf8pj\xa8@#\xd1\xdb\xd3=\ -\xad\x98fg\xfdjM\x92\xaf/*\xad\x8d\x9dc@\ -\xa2\x5c\xad[Z\x1f;\xc7\x80\xd4\x95\xe4\xdc\xa6\xc5\xce\ -q$O4\xd5=\xcd\xc5\xce1\xc0\xcc&I\xaa\x8b\ -\x9dc\x80\xc9j=\xf5\xcc\xbc\x86Ln\xb2\xe4\x94\xd8\ -9\x90}\xa9y\xd1R\xed\x8b\x9d\xa3R,\xb1\x03.\ -?<\xda\xeb\xb8\xfb\xee\xd1^#\xf1\xa4GR\xe7h\ -\xaf\x93&\xe9\x01\x93\x9d\xf01\x99\xa9;M\xbdk\xc8\ -k\xa4\xe9\xa0\xc7c\xb9\x9abwO\xba{\xe1kM\ -\xe3\xe6\xf5q\xb2L\xe6\xcf\xe9\xae\xc7M\xb6,v\x16\ -\x00c\xd3\xb1\xe7|\x9et\xdbm\xa3\x1a\x8bzU\xa3\ -~S*\xb3\xac\xe5\x01\x00\x00\x18\xf3,\xd5Z%b\ -\xf0\x09\xa0,X\xf3\x09\x00\x00\x80!m\xda\xbe{\xf3\ -E\x0d3vJ\xfaD\x84\xf2m.\xfb\xa9\x99\x079\ -g\xd5\x5c\x13\xe56!\xc4\xb5\x8f\xe4R\x9d[:)\ -t\x1d\x93\xd5\xca\x14\xbeK)\xf5\xbcdSB\x97\xf1\ -DIR\xa1N97\x9f^\x89:r?E\xb2\xe0\ -K\x1e\xcd4\xd5]\x95\xe8\xe8\x9bj\xb2\xe3\xd6q\x1d\ -\xdd\x91\xc2zO\x00\x00\x00\x0c\xab\xe5sw\xdd'\xb7\ -\x1fV\xb4\xa8\xa9\xb9&=\xf8\xed\x0b;\x96\xef\xafh\ -]\x00Aef\x9d$\x00\x00\x00\xb2'u\xab\xe8\x91\ -+&\xad\xdc\xb4m\xd7\xe5\x0c<\x81\xeaC\xdb-\x00\ -\x00\x00\x865\xad\xbe\xae\xa5\xf3@\xf7.\x97f\x04.\ -U\x94\xf9\xd2\xf9\xdb\x96\xde\x1b\xb8\x0e\x80H\x98\xf9\x04\ -\x00\x00\xc0\xb0\xe6\xb5-\xeeqi]\xc8\x1a&\xeds\ -\xd97\x0a\x0c<\x81\xaa\xc6\xe0\x13\x00\x00\x00\xc7\xe5n\ -\xab\xc2]\xddvx\xaex\xd1\x82\xf6%O\x87\xab\x01\ - \x0b\x18|\x02\x00\x00\xe0\xb8\x16t,\xf9\xbb\xa4\x96\ -r_\xd7L/*\x97\xcc+lY\xf6J\xb9\xaf\x0d\ - {\x18|\x02\x00\x00\xe0\x84\x5c~\xab\xa4\xde2^\ -\xf1\xb1\xfd\x93\xea\x0a\x85-7\xed,\xdf5\x01d\x19\ -\x83O\x00\x00\x00\x9c\xd0\x82\xf6\xa5m&\xffy9\xae\ -e\xeewlj\xdf\xdd\xb8\xa8mq\xd7\x89\xef\x0d\xa0\ -Zp\xce'\x00\x00\x00J\xe2rki\xb8\xfb~\x93\ -n\x18\xe1%zd~ca\xdb\xd2\x80kH\x01d\ -\x153\x9f\x00\x00\x00(\x89\xc9\xbc\xd0\xbed\xb1L\xbf\ -t)=\xb9\xdf\xd5\xfb\x9e\xf8e\x0c<\x81\xf1\x8b\xc1\ -'\x00\x00\x00Jf2/lk\xfa\x99[Z0\xe9\ -\xe5\x12~\xa5(\xd9\x83I\xaf\xe6.\xd8\xbatc\xf0\ -\x80\x002\x8b\xb6[\x00\x00\x00\x8c\x88\xcb\xed\xb99\xf7\ -\x14\x92$\xbd\xc6\xdc.\x90t\xb6K\xf5\x92vI\xda\ -\x22\xd7\xc6\xa2\x17\x1f\xbed\xfb\xb2\xffF\x8e\x0a \x03\ -\xfe\x0fq\x19\x1f\xfd\xa4\xbe\x05\xf8\x00\x00\x00\x00IE\ -ND\xaeB`\x82\ +\x00\x00(\x00\x00\x00(\x08\x06\x00\x00\x00\x8c\xfe\xb8m\ +\x00\x00\x00\x01sRGB\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00\ +\x09pHYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\ +\xa8d\x00\x00\x03\x8dIDATXG\xed\x98\xcdO\ +\x13A\x18\xc6kTb\xd4\xc4\x8b\x07O&\x9e\xfc\x03\ +\xf4\xe2E/\xb4\x0b\xed\xb6\x02\x0aF\x8cp3\xea\xc1\ +\xb3'\x89\x89\xb2]v\x0b\xad\x10\xd3\x92\xa8\x07\xf1 \ +\x89\x22\x891\x0a&F\x81\xfdb\xa3P\x08\xf6`$\ +\x98P\xeaGB!m\xc2\x87\xed8\xef2%\xa5\x19\ +\x92\xeen\xf9H\xec\x93<\x97\xd9\xdd\xf7\xf9ef\xe7\ +\xddv\x1c\xc5\xea\x5c\xcb\x87\x03\x8c_j`8\xf9\x99\ +\x8b\x93c\x0c'\xa5\x5c\x9c\xf4\x9a\x5c\xde9a\xb0}\ +U\xbc|\xd3\xe5\x97\x7f1~\x19\xe5{\xc7\x01YQ\ +?\x8a!\x86\x0c N\xca\x02\x10v#\xc3\x0f\x9ft\ +\x0ac\x87\xc8m;\xa3J\xbf~\x04\x83}]\x83\x93\ +g\x98\xd6\xe13\xe4\xd2.\x10B{\xf0L\xf5\xaf-\ +\xa54\xe6nS\x8f\x91+\xeb\xaao\x99<\xccp#\ +\x1d\xac\xa8%<\xa2\x96\xa9\xe6\x95\x0d\xcb\x0f\xaen\x93\ +\x97\xcf\x07\xb4xM\xfb(\xae%5\xe0g*\xc8\xe3\ +\xf6\xc4p\x8a\x07\x02\xe0\xbd\xab\xf4\x0f\x1d'\xc3\xebb\ +\xda\xd4v\xb7\xa0f\xae\x84\xa3\xd9\xdb\xfd\x09$~J\ +\xa1.u\x15\x85\xf5\x8c\xe1\xe9d\xd6p\xec\xcf_$\ +O\xa7\xd1S9\x81n<\x99X\xf0\x05\xd48\xde`\ +\x17H\x19\xeb\xc2K\xaa\x1a\x80\xad\xf2%2d\xa8\xbe\ +\xb7\xb7\x82\x15\xd4\xb1\x9a\x0e\x1d\xdd\x1b\x9c_\x07*t\ +\x0e\xb0\xd0\xef\xa6\x92\xa8\xa1SOyE%\x84k\xed\ +%e\xcd\xa9\x92WO\x1bp\x9c4\x09KM\x86\x0d\ +yDu\xfc\xf2\xc3(\xea\x94\x97\xa9`\xb7\x9e\xcf\xe0\ +eU\x90[PPh\xe0\x07\x15r\xf2\xe7\x0a\xba\xf6\ +(\x8a!\xb5\x10)kN\x18\xec\x0ey\x87\xee\x92!\ +C\xb0\xac0s\x9d\xca\x0a\x15\x0e\x0cp\xe4Y\x03\x92\ +\x06\x08\x06H\x98I\x17?RC\xca\x17/\x5c\xfc#\ +\x048\xb9\x91\xb3d\xc8\xd8\x10\x1eA\xc9\xdc\x7f\x9f\xa4\ +\x82\xe5\x9c\x83\xcb\x99\x06\x97\xf3[\xbc\xdc>Q\x9b;\ +\x15\xd1\xf7\x93\x98\xe2\x84\xfb\xddw\x03PPN\x90!\ +\x87\xabU\x096\x86'\xb24\xa8|\x9b\x01\x04_\x7f\ +<\xbeX\xc5I\xf5$\xa68\xb9\xfcR\x1a\x8a\xb3-\ +\xfaA2\x84\x1b\xf6h\x02v+\x0d*\xdff\x01a\ +w\xd7\x06\xb4>\x12S\x9c\xc8\xd7b\xc3'\x0co\x8e\ +\x0c\xb4\x12\x1aT\xbe\xcd\x02J\xb8\x05A\xeb!1\xd6\ +\x05M\xb8K\xdd|s\xe4l\x160\xf6{\x15\xb9y\ +e\x89\xc4X\x17\x84\xd1\x80\x0am\x16\x10\x0c\xf7\x91\x18\ +\xeb\x82\x224\xa0B\x17\xdbf\xf2\xbd\xad\x80\xc54\xea\ +Bo+`\xce4\x90\xcd\xfc\xff\x00n\xa5I\x8cu\ +A\x917\xdf\xb2[\xe22\xa0]\x97\x01\xed\xba\x0ch\ +\xd7e@\xbb.\x09 \xfe=\xb8\xd4\x1f[\xa5\x06\xd8\ +1\xd4\x84\xda$\xc6\xba\xbc\xa2\x1a\xef\xf9\x9c\xa6\x86\xd8\ +q\xcf\x974bEm\x96\xc4X\x97/\xa0\xbd\x12\x06\ +\x13\xd4\x10;\xe6\x07\x12\xc8+\x98\xfcOB\x13\x9c\xab\ +4uO,\xd2B\xec\xb8\xb9;\xba\xe0\xf4+\x17I\ +\x8cu\xc1\xa1\x0f^\x8a\xb9n9I\x0d\xb2\xe2\x884\ +\x8fXA\x8d\x9b\xfe_\xbc\x99\xf0n\xab\xab\x0b\xe9\xa9\ +\xbe\xa9\x15j\xa0\x19\xbf\xc05\xea\x82z\x8a\xe1e\x1f\ +)_\x1a\xb1\x82\xf2\xa0)\x12M\xbd\xb4\x01\x09pW\ +\xc3\xd1\x14\x9e\xbd )[:\xc1\x89\x14\x1bP\x830\ +\x93V\x96\x1b\x9e\xa9\xc53\xe7\x16\xd4\x0e\xcb\xa7[\xc5\ +\x08\x0e}X\xdcz\xe0%\x87\x9d\x08-\x88\xd6'a\ +\x0c\xae\xc1=\xcd\x91\xe8\x02\xbcsNN\xf2\x922[\ ++\xd88p\xae\x82[P\x9f7\xa0\xcdV\xf1\xf22\ +|\x15\xf2\x0dcp\x0dZ\x09\xdc[\xb2\x13\xd6\xdd#\ +\x87\xe3\x1f?I\x97\x1f>\xc9\xe5\xcd\x00\x00\x00\x00I\ +END\xaeB`\x82\ " qt_resource_name = b"\ -\x00\x0e\ -\x07\xf2!\xa7\ -\x00m\ -\x00o\x00u\x00s\x00e\x00_\x00i\x00c\x00o\x00n\x00.\x00p\x00n\x00g\ +\x00\x0d\ +\x04d=\x07\ +\x00f\ +\x00i\x00l\x00e\x00_\x00i\x00c\x00o\x00n\x00.\x00p\x00n\x00g\ \x00\x0f\ \x0f\xcc\xbc\x87\ \x00p\ \x00y\x00t\x00h\x00o\x00n\x00_\x00i\x00c\x00o\x00n\x00.\x00p\x00n\x00g\ -\x00\x08\ -\x05\xe2Y'\ -\x00l\ -\x00o\x00g\x00o\x00.\x00p\x00n\x00g\ +\x00\x0e\ +\x07\xf2!\xa7\ +\x00m\ +\x00o\x00u\x00s\x00e\x00_\x00i\x00c\x00o\x00n\x00.\x00p\x00n\x00g\ " qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x03\x00\x00\x00\x01\ -\x00\x00\x00F\x00\x00\x00\x00\x00\x01\x00\x00\x07\xef\ \x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x00\x22\x00\x00\x00\x00\x00\x01\x00\x00\x03\xfc\ +\x00\x00\x00D\x00\x00\x00\x00\x00\x01\x00\x00\x05@\ +\x00\x00\x00 \x00\x00\x00\x00\x00\x01\x00\x00\x01M\ " def qInitResources(): diff --git a/biopeaks/view.py b/biopeaks/view.py index 9c1ceac..6657e9c 100644 --- a/biopeaks/view.py +++ b/biopeaks/view.py @@ -255,6 +255,9 @@ def __init__(self, model, controller): self.continuecustomfile.clicked.connect(self.set_customheader) self.customfiledialog = QDialog() + self.customfiledialog.setWindowTitle("custom file info") + self.customfiledialog.setWindowIcon(QIcon(":/file_icon.png")) + self.customfiledialog.setWindowFlags(Qt.WindowCloseButtonHint) # remove help button by only setting close button self.customfilelayout = QFormLayout() self.customfilelayout.addRow(self.signallabel, self.signaledit) self.customfilelayout.addRow(self.markerlabel, self.markeredit) From 6420657cd90fffecdbc36294deed29b501728dde Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 12:14:54 +0200 Subject: [PATCH 20/46] Infer file suffix for custom files during saving. --- biopeaks/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index a165edd..335cdb5 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -94,8 +94,8 @@ def get_wpathsignal(self): filefilter = "OpenSignals (*.txt)" elif self._model.filetype == "EDF": filefilter = "EDF (*.edf)" - else: - return + elif self._model.filetype == "Custom": + filefilter = f"Plain text (*{Path(self._model.rpathsignal).suffix})" self._model.wpathsignal = getSaveFileName(None, 'Save signal', "untitled", filefilter)[0] From 0b573115c014dc88fa3eefbcc11a3aaaf5afae03 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 12:16:12 +0200 Subject: [PATCH 21/46] Update docs with infos about custom files. --- README.md | 14 +++--- docs/images/screenshot_customfile.png | Bin 0 -> 26036 bytes docs/index.md | 15 ++++--- docs/user_guide.md | 61 +++++++++++++++----------- 4 files changed, 51 insertions(+), 39 deletions(-) create mode 100644 docs/images/screenshot_customfile.png diff --git a/README.md b/README.md index 5903c1b..3642ba6 100644 --- a/README.md +++ b/README.md @@ -13,17 +13,17 @@ Click on the badge below to cite `biopeaks` in a format of your choice. `biopeaks` is a straightforward graphical user interface for feature extraction from electrocardiogram (ECG), photoplethysmogram (PPG) and breathing biosignals. It processes these biosignals semi-automatically with sensible defaults and offers the following functionality: -* processes files in the open biosignal formats [EDF](https://en.wikipedia.org/wiki/European_Data_Format) -as well as [OpenSignals (Bitalino)](https://bitalino.com/en/software) +* processes files in the open biosignal formats [EDF](https://en.wikipedia.org/wiki/European_Data_Format), [OpenSignals (Bitalino)](https://bitalino.com/en/software) +as well as plain text files (.txt, .csv, .tsv) * interactive biosignal visualization * biosignal segmentation * benchmarked, automatic extrema detection (R-peaks in ECG, systolic peaks in PPG, exhalation troughs and inhalation -peaks in breathing signals) +peaks in breathing signals) with signal-specific, sensible defaults * automatic state-of-the-art [artifact correction](https://www.tandfonline.com/doi/full/10.1080/03091902.2019.1640306) for ECG and PPG extrema -* manual editing of extrema (useful in case of poor biosignal quality) -* calculation of instantaneous (heart- or breathing-) rate and period, as well as -breathing amplitude -* batch processing +* manual editing of extrema ++ extraction of instantaneous features: (heart- or breathing-) rate and period, as well as breathing amplitude ++ .csv export of extrema and instantaneous features for further analysis (e.g., heart rate variability) ++ automatic analysis of multiple files (batch processing) Visit the [documentation](https://jancbrammer.github.io/biopeaks/) for additional information. \ No newline at end of file diff --git a/docs/images/screenshot_customfile.png b/docs/images/screenshot_customfile.png new file mode 100644 index 0000000000000000000000000000000000000000..432f5e2272f450fd2a0ccd8091aabe81a1c19c28 GIT binary patch literal 26036 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 **_processing options_** -> _modality_ ("ECG" for -electrocardiogram, "PPG" for photoplethysmogram, and "RESP" for breathing). -Next, under **configurations** -> **_channels_** you need to -specify which channel contains the _biosignal_ corresponding to your -modality. Optionally, in addition to the _biosignal_, you can select a -_marker_. This is useful if you recorded a channel +#### OpenSignals and EDF +Under **configurations** -> **_channels_**, specify which channel contains the +_biosignal_ corresponding to your modality. Optionally, in addition to the +_biosignal_, you can select a _marker_. This is useful if you recorded a channel that marks interesting events such as the onset of an experimental condition, button presses etc.. You can use the _marker_ to display any other channel alongside your _biosignal_. Once these options are selected, -you can load the biosignal: **menubar** -> **_biosignal_** -> _load_. A -dialog will let you select the file containing the biosignal. The file format -(EDF or OpenSignals) is detected automatically. If the biosignal -has been loaded successfully it is displayed in the upper **datadisplay**. If -you selected a _marker_, it will be displayed in the middle -**datadisplay**. The current file name is always displayed in the lower right +you can load the biosignal: **menubar** -> **_biosignal_** -> _load_ -> _Opensignals_ or _EDF_. A +dialog will let you select a file. +#### Plain text files +If you have a .txt, .csv, or .tsv file that contains biosignal channels as columns, +you can load a biosignal channel and optionally a marker channel using **menubar** -> **_biosignal_** -> _load_ -> _Custom_. +This will open a dialog that prompts you for some information. + +![customfile](images/screenshot_customfile.png) + +Note that the fields _biosignal_column_, _number of header rows_, and _sampling rate_ +are required, whereas _marker column_ is optional. In case your file doesn't have a header, +simply put "0" into the _number of header rows_ field. Also make sure to select the +correct _column separator_ ("comma" for .csv, "tab" for "tsv", "colon", +or "space" in case your columns are separated with these characters). Once you're done, +pressing _continue loading file_ opens another dialog that will let you select the file. + + +Once the biosignal has been loaded successfully it is displayed in the upper **datadisplay**. If +you selected a marker, it will be displayed in the middle **datadisplay**. The current file name is displayed in the lower right corner of the interface. You can load a new biosignal from either the same file (i.e., another channel) or a different file at any time. Note however that this will remove all data that is currently in the interface. @@ -89,8 +97,8 @@ on the right side of the **datadisplay**. ![segmentdialog](images/screenshot_segmentdialog.png) -Specify the start and end of the segment in seconds either by entering values in -the respective fields, or with the mouse. For the latter option, first click on +Specify the start and end of the segment in seconds either by entering values, +or with the mouse. For the latter option, first click on the mouse icon in the respective field and then left-click anywhere on the upper **datadisplay** to select a time point. To see which time point is currently under the mouse cursor have a look at the x-coordinate @@ -112,14 +120,16 @@ the segmentation, the signal starts at second 0 again. That is, relative timing is not preserved during segmentation. ### save biosignal -**menubar** -> **_biosignal_** -> _save_ opens a file dialog that lets you +**menubar** -> **_biosignal_** -> _save_ opens a dialog that lets you select a directory and file name for saving the biosignal. Note that saving the biosignal is only possible after segmentation. The file is saved in its original format containing all channels. ### find peaks -First make sure that the correct modality is selected in **configurations** -> **_processing options_** -> _modality_, -since `biopeaks` uses a modality-specific peak detector. +Before identifying peaks, you need to select the modality of your biosignal +in **configurations** -> **_processing options_** -> _modality_ ("ECG" for +electrocardiogram, "PPG" for photoplethysmogram, and "RESP" for breathing). +This is important since `biopeaks` uses modality-specific peak detectors. Then, **menubar** -> **_peaks_** -> _find_ automatically identifies the peaks in the biosignal. The peaks appear as dots displayed on top of the biosignal. @@ -196,11 +206,12 @@ all errors in peak placement will be caught. It is always good to check for erro > peak placement is the only way to guarantee sensible statistics. Only use > batch processing if you are sure that the biosignal's quality is sufficient! -You can configure your batch processing in the **configurations**. +You can configure the batch processing in the **configurations**. To enable batch processing, select **_processing options_** -> _mode_ -> "multiple files". Make sure to select the correct _modality_ in the **_processing options_** as well. Also select -the desired _biosignal channel_ in **_channels_**. Further, indicate if you'd +the desired _biosignal channel_ in **_channels_** (for custom files you can +specify the _biosignal_column_ in the file dialog while loading the files). Further, indicate if you'd like to save the peaks during batch processing: **_peak options_** -> _save during batch processing_. You can also choose to apply the auto-correction to the peaks by selecting **_peak options_** -> From 158096602db7459c295490600be3c22b26ef913d Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 12:26:06 +0200 Subject: [PATCH 22/46] Smaller logo. --- docs/images/logo.png | Bin 19143 -> 41415 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/logo.png b/docs/images/logo.png index a5c2928a7f47415199119027bad4a0c5df7a86f4..8fd1f42da326625bb0d58bcd8ffdfa3467293fc7 100644 GIT binary patch literal 41415 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 literal 19143 zcmXtg1yEc~*EFudJ-EA*;O@aCxJ!WG?(Vh(cXxuj1a}C!L4pQnad-Ld^ZxZyP{0<& z%$+%NPM_|cjaF5bMML?B0tE$yCNC$Y4h0395Byw=gz(=h;#IuBH#k=bc}*nX!w<e0ETYx;VumT|7<@Ndr@&EN zsj67N@$@~n_c^z><<)Okk)gzKQaM6Ml6Qi}^K4ke!>p-rA4bDYv)K0w2vk(9qM&h| z_B+UV6d_Oe1LHbjNEsV(({|%hQ{q%3%Oq;+ zLo>@H@B^3K+}n~L#+)d0cc-&XcEz6**53B5MHV%NLMZa&<>?bOS9@Rc zq&^qHA444~y4O3obVXaIrG8C~m$rMbKwKNRx%X6mR>x$dpp~|+f(t^TEs+Zp<(rS7 z&aEiJG=vSB5J*aFx$_-qY&A18Bp2`@w-%7uV3w=-Caz?X4<*89w;zb~(nvqb)J`Sf z|A#sK_mx08@E}WJ*1zBhtpuW=^P!1@JWSB)p%Xv-H3<|tNIsy8O;r9jIB?Z4Af5cfayhG`ssqQ zik#v~q_#(4NLQSuCB$1}$%pUT8Dl~|$U5Nlm*Tt736$8d^i31{TilG(EjP^-<3zdC zu13-oqu|X`N?Fxy8I{#nx9P%vBDMk%!>lxB2N~#dII)paLJC{oxU%EZq@qARmLso5 zd%#Nn7(7P#+tQpO5>7nupqn)_Y(fI|}csJkv_%<&Fi$l=OyT4+ zn6sydWg}btj;7SN=p=f~DC!3GG*Uq{%n|Cfv*$|}=xA`Ue2sbb)}QsXfK&{7S<|*! zW_R!=lv_k<&;Oq!mBZ6L>3z3Bl>&+)A`%0%hxA`OKCaiiA2*^rFl?CiI`N(C6w?ZQ zsC;L!?b7T?e4g>+|JDz1gYJOI=zqpX^klsrZ}OzyCW$Q`Ep%a#M~Bauc{dP!&o^ih zr0Ob;gHfXQB#9c?l-7X}iaBO0b=)kst~%l%UA+3r$$?84695rDMxu!|VA}qvY6R^~ zbb}{lTvT%N$gu%wsb-zCIC{S*xPVh#&b>`!4r2brm;c_(E+-0R^ihQIu!qC6=fiiy zU)R^pDZasI{*%Ef#QY6yAU;lPSIkK^hy>~c{zL*rtS4ip9^zVWrClM>sS58lxyhX! zIQ?bm-Lnfpfgdy8k;Wg^i+0R5s&*(fHL5G=_DUeuoY*I&8qw|&LcC6ts8#?0xz|GF zV#e3Octd-^Y+-$Q60&D+zFJ=HpyjJXA;}Coitfu3>r60hKZ#IB)}%sjZmFaF7>KeL z;B;POdE^3yuBmoay+gP|sDYY4e{OY`FS^8IP{7g+nJjKr_I`|2(U?SutS!&m5UIgq zvm@iW*2ZJDf;*9E+v~b|FqsuZmfgxMXy)l^`a!pNs%TYbj(M-+1cDYOw2XA){CkVZ z)OHJVCm(2|y<59B%rN(`T2U)yK|8=@kSC3jf4ftd$tu$1h zp>Q}%y+(>Ul0x*;if-|iQkUmrDYml;?3dmv3x<~`m|`rM&}-YG%O$LC0_|+;O7B1T zA0HJ_gSL8Oy`NlSMgl=Nq~9=#n+WsmStMo7+`?Laq9ZJq?un z^@muHam>2mV*HY_I?(0$TIxqi>jOr&KHwu6fgVk=2$bp~vy3>ymJSle`inz&E6uuI zDYe5KY^9bKBBW4qp^Kk$El(HvcN_eRD$HCA1RXFTqb{|hLcvowE-07t-UJxJSpK;#T*)fpsshJ{zo*0SK#g}M9MPn^{;4%Uh2-G5xn+ss#7sb$ z2+5YAazUCX-S~F~r)DBw7Pk9fF%bmaeI})E-&Or55c6yxFYOO2A22hJ)uve9=rUo{w^#DO zIQL*)u@Qy4{_)qogO-RhDgWds^BPRfz-1rFdqh1<;V)B!B7!W?7PNH67uOTy1ZDcT z*o2WpLZ^?`&UvC+znQF7*&AHKLXQZ;WL6egi>cluNB(%vQ_ft?EmUP&4{ic2ZBV0#NN>@@$#81LNSEtn*{eYv`i4RGY zshUH?j0uQd?BdLJpv~SuqKZ1+S=v@CbsVw^;=1ewB@8%zqf5rqB^;gj z1QaQR%t+bi(Ja~-Oi0Cdwq>H+j~7HB*8E3SvK$=i*)ut2AfNRss+HB$U?5qJ6WHn+i@ z^i2TMc6?kOc+)LoHfH)nTKc7pVFoHP17r?8(dM*^%(UH-vVahnmAIFF(`;`+H1{z4 zXK`gljQJ-*`ztf9$HcU?fa#oxAT?89#jLCY6#t4FHb%8{uKaJo_g4pUqHY_+BD7m1 z6_`;U@~MfG*MDf#rAbux@JvU8vwDJUn!i87`4beszzGh2UcxPU$=Mc9;Oq$?2b)&s zwBm3i(<2C*Z8B?Vbv&)JfV94x-*M`a%y3l}Z5>qP0pJ2ry^p_H(Yt3=R_hNr{CH^r z=SdnP|0J-QEyPUMQ6qKDp7+A)WTK?;LKuFlZn(#`@k6OrCsPEr*opFMQ4LV$+34GH4=)?k;aBEMXuZ=?jt?Ak!)4Bq z;-BoRe_i){&NneJU?yhzh9;5gJAS2vmcKUI`E~Pn5o^csEb^us1Ize+s^ym@d)f6U zCExVx-9QyFPk2!T&!oc>-o6szvAK+9-ZH~ODXTZL^}c`Wa}&5e_3VY- zwf>~&n)>NCAHAdL;&`%fDk>Fj_Klik$CzbO1=tP+u1xFK%rcGLlExL7m6i91F$S<2 zi*b(BYNt#WK}8FxOuhJdvr<&G;yUm4QFq@-c%yCCMue>Qpr+=x4A530sxF76S{tY$ z${0H5RE30@ctF*!7A7}x?6uF##Aa#f7{acrK&# zm`A=Ldm#WbRzqPDN$WqKD~;BN)Nl;*6P`&iO%TzIwh5_Yi4twqm6>}<>@Crl@wy#n z#PB~CVuM&MuXHZX-fGMM?AS$@xxvs;P29C-A=I$We_>_sWpAN=P>PAT53NO~ zys@jA3&>4JEgHiaSPMj?!WV&~M)K*gx<=emSbTBzMhpBg%L%N>3y$;WW8FrFp!?2{ zVAbxRo<1tsZAeQIpV6vy&$?kUXW+VOIPf;zhdxieEXp%~dl_`Y{24hi5?RO-g7Cg# z*q&Ux@t~sD>G}g@<$|D~O{KD$1^JEGt1Bw$pX9&6nx5T@P6-6u6Il2`=QV0)erQvg zLN>Wu1G?!N%eP%#C3?WbFq@vm`p#uFh~ql)z7MKjjXpWKh5CPvH_TTT%Y z#Y=7|{!xTUibdjp?%r0Z3IYpRL_&re(14@Ue>FQRO7?F+yv!Af54{|=_2EMi&?4c? zJr+>vMAin<>vCkS=(Xube*Il3#XnxIs)fHNf7wBrrhE>U=IOr<-&c9*I4cfcWfc-f z@0Y!VpQQLoh%~tpoJSPsD!K-uqd2kc5}^_%ef>21DatP*>20enWqX!5T^REY0RguA zxAYVOYhP+MQQ);H3*P|_*v3UmW5Gwnc%+ZO)|H8v+HX+=S5{>xv4MdeXQRC2c~+Jk zHo#sWN<~@HYsxJuj>i32(DGZN4Anu_bba&yq;E#)puQ6VtX#}mFSMkh>}6lqkF24A ztUCvC$Pw)~`nqSOq=K2^E0zKMiY}BbRw)q~H?*U&AKRbudxaJnsIlDTuF%4u`R2*B zJ!ICpG5?}kvd=HM@VDV5u!{65cb;Ce_OU`*pW#K}dt}@5BD6H{-))RG=TzJc;yK3L zKAew#cRH8arQ~_!h`!$3IFn>1rmi*=g<0DXQ<1U|!W^;xg0SOZ0K5Ae-pmAmpFx&} z#_o#M$C6ctnklG3I8zbV!4nA#U3VV=@byg~=D@%^b@3=&aTJMI+LCi=cOYd|3Ej`> zUGy|?{z@S|8{N#F9S?!$H^PJ>{biqz!GWe;mosbQ_(Q~?^C!P``|!XbAJp1r^=4;A zplLiFnOpS)zx;XNT^{3*!Vn`uaADxU=D5RM$IoA7_TLf-y>xvK$?lAII;nx1O&1YB zXkT)}q#pScCwn6V<`lZaWYWIe8j)JUc|B7gz@PE-Hyj1p4%`xD@ms1N#oZsWVW&+E z25XwN*f~gPX6MPXAldGCx3YyVoEGP)Z@Bl-(2uRk%SVNXKsUT1e)l=o+?6U39K;Ol zTC0Zli&a$E)<|8jIER6lUHp(>k>u#gqkB1Hz%+(+War64X%R^7E5vaS@irzQ#+~wU)nq4R}t(N zJzQOi=Vp7c)VlV*sA}@~Ato9F`3!kaRPN8(#x)L~sqS$HNg|H?5MAlB(u0AQLn_1l zh*4?jzE;-HKsnbFxGxGGL-M3y)@Z62zqXbRTOUrM0iqx!EeGMr6Rh^DP&Px^M9OMT zB}iClbS#G^_ucNnmm4_semJWd6_?==FLsZb!&$)J3ukaqF9{B*}*4UOl+ zv0*-faNhn=q!QxaqOvMn)Us|lSEU>&JErL64Vraf)he+%M97;Pqy}j)dnFE-t9411 z)7rx3l3P%9Hh_J@FmZ|JBMzmo8{Y43qk0=%)Vh$yVo}oi#~@Czdg69YNt@4Zd7@FWeCd z?`)$uTD7KILtd9Te~nFG=2Au91fm&zDM{j-7&*vwnra5yM)319tuwELAn#D(>9OQL z-oA9CHV9~M@v2+-|MHa0Mn--s@51#`mK@b}EgB(YFibTN01v4| z^*Q?X=aa_DL1JFcIDS46cvX@9LJTW%DZDcDeCcP@If^1TDYay7yWV)nQE6G|EQxPI z$aF(V8zV~RN4n`2jd>yZn4>zy@{6#;t^XFre(s-_Odo% zt*-$LZZ>)%)=WrAEIMq=9S7;agdtOh;GPE#p54-_0e1WnMcB?r`4KJ8=-Ks&A60*V4wUN0 zi2byUhGDC+G@~DI$Ufjg%Q9Gt_&Z@#q${`LmUj3Be%~evtS;ihez&*}x24=k*8~Z{ zC8m$MJ0~Ur!Kv9i>+x+Hfo+T#S9uk|f4- z-)hJOY-VN_k1d6DFoci@>s}X()hLr;CLkdb_S0IJy{()LpQa9KF$JWSJOagIEd1JFW<=GaadH2;wDEL#5qidobV+Nw}{+eB&K(ZS}mRi zIbq-&hinRs1BLx}O48pq>HTg6`EkiINek(T8k*(vM_rof)61SOf4_Qt5)XW0oc2_b zMIz@N)7a=$J*?&??Vk{il`-gI=9q8;ozluR-~QVSH(?)#68Ke%Nk0$w;S(vXYvV@? z?0c2>-}&H4a8C~JGdi8_gVPcS)thyUV0&Nsct(nbgPcd7F0iz+YS1l)CDTMe3?T)n zNhxyKA=-7R;5QJ4)MmV9_j#@<5V$;R{q$9hO;+x;Kc2x+5W-c3LKRclI_EOi-O}Lj zKm=tjAbf~bGo^~2K5fca=e;mp@+0+62lJAXop4=KDO5o0=TeZ9VOS*U2pR%+Ga$?3jamOCWfp`H8bPir!}f+8Lv8n- zBaikwmF-}QvNsBnX&^Q~t}|I#(H4FYuOHYg%MQ7pR}5cs2!l$`II{jjA`{q|i;hJy z=E{~Cd5;i`#dYzq2CS8ZK1c09S|(B>!6-gJl#nHNDi1L`z#e~X5Y%5*>}A1v>^019#Dhp{b$lKcK9;b-7uiE zDU0ulY4(OYAwjetU8VH17WX5dnGJ~?PMWe`rUG(W!f#XlP9c76YN7gmY_&?*@uFyO z?oU;!st>Fti4cll?A*Vo9uJ&VqOdVaqnB~BW82FJV9x5bB=u>OZNF6|T!cH(f4Sz~ z;gL#K$mDn{AGiAw%aSdWSDZ9<_<|rG&}o|eKVwFp1RtNkoHb zhCCb85M9)$Q-}QGLW0Oo3h>w+)`NAyeahn-aj`DwH5%`xJkmw`l0~-!p=;lGWIgyQ zK+q!qS4yS#ZnYMYt;5%`?pCh|-}`_&qB`e9Uga~yLXME z0gde5(m~W6dwWcGaC!&DG^i*$7Nt<%s`H5A&Hs-ZDIy#F2NyVLVFfcl zyQmA&G~e@hNC)XHgCEJhQ{*Q|3crx_ptkudW@(`DB2*clEM5-%os>)4uvR=f6j^+_Eq$BX`KZ5*v z;;ODp^u*%N6b9n1cHdMt3-U1UP$t|L@cm?fCkIn<6*DW+k424Pg0Wm8QFgwa$L^58 zkifXW*`Ly5mQ}XsmqlG0mr3e$jEXllv>n*@@dkrmKZ=>wMc#EiA!%JS8=LQ@I$!Q! zy?qymkUg>i8CnVq_42zc%y_xfy^3Ge=q{oo9Z8BraY9^}kvW=)m># zJ;;xY6wQuYqB5t=(k6_8JAS@@t;KVM-U`sfC0rkm+8U6?z@B!D`Rthu%@OEiD70kP63P`k-Se zWmtW`Z+0hZ-OlQsm>{}T3=O?2e*h>HwJw&j1biM|gVqF?w+IrbC*qtx?9n4Q6~D~} ziKkdzfJ572wwxRxJzW0tP{b_(Q_G&g!0u|<`5--&0)J3a(-K8rg!)inxeG8J}0@vV2O z!5T`VoU*7v(NW4JUbDVZJFZR|O)lQCB3%n#ys9N1;E(^#;O80B^{+EYTDCxtHfAw^ zofhNV`F77uIzX(4LZaYvT=*L7AM|EtkYp6&F)fp zVA(~%%pjV-(?dS&FUvU7$R#|9gIqYD)!Z5XI6gceC9N(|D19XbdWvX{Rksw924*LI zWefPZ2iTOQd}&>w^2PB`R*}Jf+)`HC_#X=SwQSpcVEa28ol{yN3v z#ugCL3iv;xSpd|D3jY)~w6er5evEf=jF&yzsjfrp%s(I4Ep_!oVe8rs8td5jrn2f* ziaxL~v{gJ%^f6tY&mgUG6ZrX$G98KgM?iOFx>we7N~}UD?7l9_G?ROCCC%>trqH&- z*0&UE>;7u{9rycO1UdBPe>!LtL2||AGh^31_QDdi9`y3@Rv<6b-T@!{&kF1Im}uAd z`-b{qt9ph3Y6h!@fCbeobL~jeUUK8x^{KX7&cB7kO7s_F0$^yaBy2&}Px(lmjiz6f z_!b}a0#q*oCu#<3vfq9F6E_fE?EPqc#{MhS$H#=GIJr3Rukiq{Kf77}u)X&Gy#Vv- z8dd_*37I^0*;r+M{ajx1?30M?vW9bi2*`Ss+;SISKIaJ>oN%azv zc64pOW#gY~VbwS>Q`-c^pdiEAbrN6q67c1`@8Aub6zEHT7srJsn+^kH)$QN|o!&FW ztSa@gP_HuD#h~nuC_ciE8%_R`aBeUYO@LYfU|L(>tkrF9D;q}Dh|UAm4JQPgV?X-? zGCfkPCAYA+_KzU)j4$O7B%;I=AZ+c?4t04(zbhd&!1UiUw!!Gsq%KX?@iWZ^JPNkCcbw5#kP4Y#ZKct1wGj1a9PiAShZfBt z&s$$HK=f|)7h@B%rqR6-nR^hesOZhIe$k|xn(mF?@GVM*>pBzI`M{SxC{vyzv55^IBNYquh<>gp%X8!OK}!EqkMh zzxc~iAgWI9yWV4gIov>LUwPTiZiPe&dBHCko&ehN5&vU%Q{aF&1%;|*La#}eUyJ+H zdicj(@~$f9=r@fZdt4IT6xrh}9WZ_maFuAhTU=Q~_=nQdedN_u(!XL9zMaSVW-QnO z@;!alU;m~M*83}f3nGb z@fjtJS#M$M=XO!@>S~-JU_%bm%GVrRwwcp^q5L{@jcJsAe%)ZZN9Nv3X#TTnti3EX z6-i^ZDXW24v?lkX4(oM+e2-nIV~HEUqAQa)Kpc?x1S^YK11a>U)K2%2lwTW)?=BGhi^lR4-!S1+HXMaIU>kupS z0)4qK zq7MeqtCt9KPw&^R(;h*7W&&a}9kgXQUJ|9t|Lr~TzxyBXEV8?b*bB0A*!p$?ad?W` z74N>jRM#@y&8H*mALSo=_F`gkeTZE`4_SVtN4xtgB65E3Zziw-eGMi-bYXAhK1)Ly z@Rm}qQ#BG523NKN6|Ig`EMP>&i2h@5&cuj`sP}|o5Go?_D2`oZE&yNT)J%gmCB0A8 zQkcFd$jln7Bbl)?RV(@}okP131MD)3$ z1MZmEQYvQ5b;=l1HF;jv+~RWPxc0oU8(dXMV-6Kfav^dZvI%|8c{Cq;8|g9HfaX^; zVtY)2>8NhHx4hu06#^$*M#(5bITp_>@@;F^158<6mnyLv@PiSzvnw&mKOdm>I7uf^ zFl1jgS2eaA0Y`$}+Qw2>dyh)@aEpfHb;#AG6!A1CDRp`C&_+SWJ~m47GlhZtkLSn- z3DlrT8{5E~%O>=b=|mv&v4>22SXoKuFr6kV8?>0~RYV*UtIsEdAu-ZMg7Z8|!#sPP zfl)~6gN@2d$o?qjdOJ>2b-?9#qz4|acB(906fc=ScP3eIa3>SC`5QM19q3q#vO|Cd zy1#S(z)XO2$Wqsp6*2JZdb6DW?)~tDFzo6*2o%8?cF!};>`eJ`-a`K@6p>2oLoJZ-U!xSdTk>2UD5G%l z<%hivnVT_u?S(YKu-^JL_UiuOjYSxBvEbD-v{TcCX-@s_gl zV=#H`H7zOU{@>`aZWL3Rnfp)xaZthM7N`NCF(E5)HW(kNC0B*AzAqcrvMcC?{j z-yLk~*EFZA%*kky?P>0!Zq=VB7IaUO60|A(waa5RkT@8RVTMvDUSMXn?SVh5>g25| zO)Z}Kdn^rY`QxjXBxD014YhiIt!_gPZs^A+93R2cu`>N+B~0z9ZTyVYfm{mj#kCO4d5 z*8_I&HrrzKyyqJg((ZQqn5xt6hF!&}QM?o`f2y1}h1H0#{EU|{#pSiq{LQfuFTJy>_v0d&^**dlq$he-| z!{AeUe4HMa?@sZt1Idz0)$Vf+J$hlrCJeVC;${35F81uU#-2&Rup6MlQ~!sI>ETId z0RA#m1a=Y1w(G3}VT(IOMSQ8?1yyzf(L1{&Ky0+LP1nzKRci+SpQk6^*eFZROE3g? z)zZ`>Qe)_zt&M`$(xo|ct@NM%B{Bu1wypJHEop!Gs}%7aLN{QeNih84{qj}MY$+2R zo201u0_p<`!0m?(AmJtxaezczV4XJ7-}~_Mg&Ry(V*Bm^Nj6Zf|Gh|&at!V3 ze1Mpna?VgnZ#Mci>XuFG1sdjuCsr`BPjw@w#A>HxXcwLF8`j}%t8oj7EOYDgg7U$~ zwfUQ7W^D~aPfu26c9y^Iy#C1(UYq4>-;Y`qLB{D8rui_76$ysJUw3cxzO&a3Ppd8$ z&5NWrw)(4tX#gBz3P$WIl;lB0Q$5|KmwL^EV~^w1Qa=Yiq;MLF;liVAP8Vi z+JIf7sR|Uc8YM$Y$3F2tY}2N!HP~!95HyKZIj8w_ad}`xJR=vmai{Vb zLxr6;|9Cple(^E%^J4G~b)^SLa79_<@c(kI>&f>;Za52p`Uy9h{CE?wx^u^;a8{A- zl!^e{1p$DUDpI6sCTGjy$SoKR`)7Y8T;?GA5AJ__)%wA2z}He0#C_>)lLQ+pTlY{% zuOcGT?Pi+&&zJmPKL$oGT~1T+uRnUP*aM!D7aZFPH++Z0C$?(_O?<7vQn!G(bP_ox z-6vQ^{V3C%)@ZBHkDtF0d-xStQ)^zPk0Go7_K)r2exqJQ_IoiAhqkELh}Urk4cPav zX;&UU^L(CcbV>dZ&e*m+m9jcBgXrn)-NPzh`bW8YoouQBf9UnC)tHb^gCW7#R6y#G z=d1gyfj?_0t)$p{O(uO)JLVrdKTvAGh*jkBNW+Z{s=MIFVq|OvE3S;u(^48*9ua*ZlW{f zOjDnl!~z@zA`Zfg)yUp!A%um0G+Xkdre@Uc_4@|PcDz)2FtyA;labq*p2q^2S{pjU_#M*C@^EI68ysZr(7!PD&7nb;KnA3c@Wvde_@o< z9J;S;+iv$bI`zOQEt%1r9aW;g)ZqmA@&Z}uzmf2l=;Z$xfWEhvIm+^F;OF=JSO@at z4@cs!#7mkjWMkHnj-gc6M+9Y(b{gJ(T>TC_H;m!A9^K`Zdxg)kze&(ZcCR^3NyCZH zZg+;Z`8=8019E(Er2iw0mA)P`9_GL`&I4KTR3^-qP>@&j?k7)aX&L$h(Aef-@dI0E zq!5UM;5$_k;V00_97tCICQRWb)IPU;evbqEOJQT@fZHU>3HJYak{hc}T-9A_H_aNOFl;1AGRm!!wMRsW18flL zz&x_HyH|Vc2b`Q;A~8qcEbLB3noV2R1R!7R)w<#%f!2Nl9$#M4-T(ADY3ggFyXMn) zx6VK=(g9#>4NbV)?!=hlo;v6o-=iA7<;u48R*u#Wi5kfI^ct2z_NTo(q||@1sun{> zltwS)0`>iTDo<9_rM?4fc}Cdo*~F6CeH(B@Oeo-|*W-FS@*x!Y#2RhX!~XlQh}obd zEYr{TN@(xTn~l|VUQWxui-WuU$-cSUY!*Ggl2W-Nip-p0wdmL00HATOVg%{vC$j7fex`Wb=jfnx5e_*lm}lu3cZ4 z;gt9ZsLuc16-O`f-T=Ffe3?d=&V(CO0xn=#MFnz`m&@!_ffdlBqumpMksBp;UHlw= z|6}J*nGQK%bR@KAk??+TWAI}8w_0U)h1k^{W4Tu@ZP@}ZR=T-Uk(6rBJ9JRn1_LTf zN`4`s{sS1nWjp5FJ|y6U@NiO!{Bir?lw>l)@+^@JRG_aS&wM54u3|c$*9-@r#{)Ca z6Y&7Nh#4Xh89*U{iif7nS>`ZW9k0h}C0carNwo%jGqckE<{c@)yBwdyGI`YD`5X`J zmn%ozh$v2|3x8$Fmnz@lfAg! znja8)dfsO1GW_B2F8Pjnj)-Orxduy*kxg0uZdsMqw${eLfas8LnY2L%IJ^AHYjbipW2ZD$Pk z>C84TV8-{NvPkL3ak+@1(g*$+X4!}yp@$K7NP0XEE4RDs_yiD;jjmhbd@eAD$Q%b= zKXM0kojkDK;WX|4y%L)jwZ=_fAtw)a0~W0&ss~ zVo}e;z_{=VouW^RlIDfy-;gn8^EAV!Uv=hw-g0}&aH6p)n98Gol&j14n4dCAPr-ul zlmXf){5WxJTvEE95HYfemJz}gE;j+V;?Zvce$cw{C0zEUBz)Lg5dk6{7vqicLSgpe zk1dD|ea5(qe&o-ooZ7izA8FX6YFJ@kK%eTop)D$y9d}>}&llC;kyh!eDWq&cSe*y$ zlo0vEMj&PsB{&g@S~L*DG3yo&%Sn&|2J#L$>MqGPB3KD=#Tibu3GOETu4|-rU=tG> zFqOR`_B1ZrR{8H;k7;S@sk%sQl0D$tTz%&I(9)@S^DjVlO^x$?qu<&fUvp$_C}&L2 zrd~q`vy#slJ7;wCicdT8j!5R~Go^@x7A+n7T68-pMkecq>|nu2{PxHdYnvS5S{5Hm zo-mj%J~(e-o$N{UxR2A%E)p)DGRo}SjO&h~=0mObK^~~5``{?&m83#-bj%ZFMVQSbJL1;2ktq?-Vr9pl-|AxM zQ0QW%2+oCGvpz+affPchPoA1aB?Oc0f_FAr*ztMbe5!l9uu96m;{Sez@dMTuVDejV zVK}q3<;nR3iP@r+DN_NNh$SPA?F7%)P8wM?^#KaP^>9akQRg zvU*#hQmUnGW>84vs1#NENoWMW1LT`t49Xv-56O!F(Ak^;aL)kEHzZ)w4foud5!4if z>{#sgd~Ie%oxKR^foUXh!<%4o(n|bNyG`uBc|I4{f2br#ksGQ?itb|9 z33A4O$-zu#B&E3(2;#ua*V+#l|IeWpIffja)vJmnin)jQ-`ud?#0&LP+cbW&PCkYF zw0ac2mLGE{E}Rm+nkZLLCTRux_SK4@a~C>G*{dxmw2+E`5=BLpy^vvJ@8CxOgL9#< zT*o~}!x`MZim{b|BJXM1+kA3RZRekG7{rwo%8&0UH?_#*)Q$LV)b;4vc%$-#xKw_y z3lat77fOd~jD{8-uNTOU{T)OddEpIh6pGgqHDvQ}rL?^qxm4vO9YX zMYBp!Pf6>d^a>p;s*-HOFB-?>m>fmd6JfAC%j5*K!%>%8i@|klAFR32uN!~S(PexB z;MTwg*}2s*JgF{~P3@??>iy71#WM>1P^2pY^?K!d17Sc}Q;6+VM4q;I+~F zjvpEId-I}DEVC7iX;vX!)SB31PBFlN``vTU(u%%aIj(GTzpnxQu#B8d172+ah5yJ< z4>wsFMgSM%y5)&&zx+@#Y-C}^0gU1gz2ht0^t~dl-1(!+Y@p&Ef__6oN7lL)Eq1Yo zMBuWB(kY#?ufsZU5B+@3 z#zCWOGcNx-zXx!=1MNaAtksjS44Y!ddWp4t6?}fVBhwWrnlX3g+AK-e$GJ?vnXBN5 zkUN^uNvcHOAk>yKVDY$kC)YA;72rj-U~XO8v*`9i%og;DCkqm-n*IZgA zW(D%gU&%o$vMP=>U?&!D92DqYNQwnP-p`T8;`%S7()V6Yy|yoT6FI#gziY06rARZr zaOpPlCDh0$7D_z+VDAIRmbt9|gap_Db_P}=d4&@ju&IQp_^rBnC3QXV_k*f*Oi&rW zM+=1FzXF=_v}rvieJ)4f$1gPu0Cdo)&vE{WUiy4e6ILD<>9F|@1KEu)n39G4@Yb*X zk_KP-?ccGHW$a8nHa_N-NtSub9VB5CsIiJv9tdCXqH^amiE(dyPy|3F{C7DJ4qyyA zY3UTE-ce_oByONWGg`a~nS}Nhk9ocb2j8Vux)BL9dpa~D;y-V^8bko~qnG{s{ zKZ;h6n`!&*W&C{8Vj%=Q8CN?Pz-#!4l>QIE(BgWKAqfzuf}O9HoBB9uzL#+aJ_&$L zfkR^$8)4`Q!!ZUdpUAAC7Ws)|FN=DL&`xrtKyFCqBbXU-**XO?5D~W;|6mC)dd2t>MlD%Dzawm^o|39oPmLesDnM>paNLeld*$&;V}e^B0oAqogQ9@#d}UzQ2IYMK&RH{y3Pi zELjKY$rb)*DW4^;-@XD zPEaZ34aEX+=8gygw+H5 z`bo!0Yl=D(WSiTXBsH}MP#Mr%=ozqn-vqSKeV}(;)PWtd`!KcOxjX=FtX52I_RK*^ zc@l`4aHa3|n^WikZ3WLU2RyK2y=*)`uf|6FeklXAiEI{-eE~QVF3dhX^u1W<%@9m^ z_50&L6+VHK{i9_0p?zOnuMqz$o0Z5uFp^5~6(Kxv`f?4G1Y0+UFJ=7?-~!6Zs)|%* z6bH1WBlz-zT(A)6NC>5U)d9moH;o+bo~2h?q(buS^T#LbaKT_|e-a6XKN>-{{v?Ad zYp@z3R4bUY-M7Qj0N9qoJgTgCr=R+yl7)_A#wdkfxF$Ez5ej&cUrB!B=l@NF(JHL& z+k)53m)IlHGjq&9|3>r&Ai>v`Za;cHIcB5vXD^YS7 z0NhWEhG?c)hA&Yg8ZNOfWRe_TODN(geSQ)Qi_94#Em7Bg;t#9H0RfjVnblrD?rfLu zIFVzfMFw;ur{enaTg1OEYboj8h6@h@p^0W+32e5#f6 zt?B0@Ft7<6q`w=hUlLCgwVHxh9I~5#$nbzjH5%f}GS&}w zAi%h~A!#k%0>9Kfqlq&LQ-C&hK#!6AzP);sTn(gT~r?d{u|?$!t>}7yDE{8?@=Ai*U%6;WZSe%n^KY zo~Q2oHiajf%i3`kaZgio)+?|5;t;=v1#~Bi&*?Wm{%aKHD|7ML^bCnS7E# zvZWDqlb-zT>$_*nZ^bi2O92{fwJZN5nx<}-(j<3z2%EfM>WZ(6f5FW~mygHg!wS#% zZPD*Q9Kn$>T0t}V3X$Qqz47^F#($Cyig!!=p81fgI)uD8$mv;)H;Y~L)%72;HmSDo zYSVq~n0_k~;+}g{!@J0ticS!-ZpHMpvAmwhiSVL-U~h@lv!g#-z=h^Qsj>QzYwEJM z%T(%(x4Z_-K~0Hal6~qgrCI{b{uO`5uh|oed3GPcM5p$ae2NYoDdSLG?9gq%bQeGiNzI zMwnIOT29cJx|wp%i((;oi8*Jk@q~BY`yN*{94V9B4dhINDIPdXtPjo)@iED z)=i_w>6P3UF}FNuEO55QukZ3j9v?fMuzhmhpO=YWJ2`y$>KrITBXLvR;4{UznBBwv(;s;;SuujA}# zPpnl_IPs*eYltp3slpyqNqBay43TT229I5t@KqBEqGR_+sFmO_vL4= zUCOa6I8S6cNGrofQr$k$HAAYs7VNB?~fA>=f-#c$1b5<9KD@O+e zTI#4S^X+#vPoa>lE(Kp2eVI||BG;C=o;uEnYp)%4BDXf!>rwAUyEd6*%k}oh!Mp2` z^WWJPFA5_s^OiOxM8MxVmGt;M;3Sg&VApPMj1-S9YgB#2;rxz+&6}c2)OJ%1M1M{Q z8)wpng~8eJ&3x)?){=p&o?VMYw~u>K1&_8#mgT5N5s9VFOpFEvb>EA;_cQci?XNVis>jCySw8-<>c0)TKbic}NPma1={rj&*0 z+b-d?$LXewp@dPa!}b!qp3l$HV<(tRw)y3EK@>h-oKT5i%kp5Xa@v>h2_dvS|Kc z1Sz7WCyY?ME|ym`-|ZDv!p%c~`pXH!7avVy+2ypo{JgB>I$-9PGB>-o`Mb47hn(jg zn_5i)VIKgTuXo!D7s&8y7N0$tck@!IN_I9yTbg5JfxPIC(r(xvCu6!-e-Szl`AzAt z-xyq-GXVY>{}ljiQcxc5)rFF)2qyXp@=w7rfSx92ENUH-2Y@0edrx;kWb9LWT-x*D zWtSb}Z{W{W04O2OkXH-Xy!{96Kp1S`-RwY*mlCK}AP0*V0BZQDR0M1`g(7LQ^6x{i zjYRoyU3kxD`tdlQdj5ShncdPUQA9rm6}4W_1GDjR9T^H&UIaJ@j=dySohC>D?)>SO zQ>^$p(5OhG2STmMh{AB)bY)2!ywgMc&X}9mCNqnVPz(O7C^y^7ZE#3OjRj#p(0jQ| z_V$AfkbTAEf_!nGU4~pIlMU>G{iJO1tmk2ft!7^LGr!1PHcDq47+|Ix!6lWN_n{fe zkIB-)HXrjDDk$SJs~>bFPa29Nb_hsv%YR-yXOI{6xq3AO&7mk zeAGA-0ITGIK$0f*3E|pi>!@Ngj_0NI@Zd5V^{$~FDB}S(a5ETAt~5HyNT>MQJVr6R zy;LB)#ER)LN$f>_7v$bX#V!d zfN{0W{dx|DeDsHfIn^h{GSb?Tk9{e{ZE(7=BKuoMl@qe>fJF`cES#Tc%^_48%UUZ1raG646J%@|x} ziO6QnFc&Lj=$^CDy)ktE^KW(g6CHoh-}KMqGlThvAoSa#TEeJL=F(s&y=6yA0FNJ5 z@d`acxXQfRbz@Z%al1l7b)VvnWX9@_MC)%nr`?ad)bC<+GPhkX)N|E|Qz@$&`_q0C zl~6(ZY~u5kwV6FDeapaHs6T2Zfl*4@Y3AXF8>=Nat!<(OGhjX@G Date: Sat, 25 Jul 2020 12:26:49 +0200 Subject: [PATCH 23/46] Consistent bullet points. --- README.md | 12 ++++++------ docs/index.md | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3642ba6..dbda816 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ Click on the badge below to cite `biopeaks` in a format of your choice. `biopeaks` is a straightforward graphical user interface for feature extraction from electrocardiogram (ECG), photoplethysmogram (PPG) and breathing biosignals. It processes these biosignals semi-automatically with sensible defaults and offers the following functionality: -* processes files in the open biosignal formats [EDF](https://en.wikipedia.org/wiki/European_Data_Format), [OpenSignals (Bitalino)](https://bitalino.com/en/software) ++ processes files in the open biosignal formats [EDF](https://en.wikipedia.org/wiki/European_Data_Format), [OpenSignals (Bitalino)](https://bitalino.com/en/software) as well as plain text files (.txt, .csv, .tsv) -* interactive biosignal visualization -* biosignal segmentation -* benchmarked, automatic extrema detection (R-peaks in ECG, systolic peaks in PPG, exhalation troughs and inhalation ++ interactive biosignal visualization ++ biosignal segmentation ++ benchmarked, automatic extrema detection (R-peaks in ECG, systolic peaks in PPG, exhalation troughs and inhalation peaks in breathing signals) with signal-specific, sensible defaults -* automatic state-of-the-art [artifact correction](https://www.tandfonline.com/doi/full/10.1080/03091902.2019.1640306) ++ automatic state-of-the-art [artifact correction](https://www.tandfonline.com/doi/full/10.1080/03091902.2019.1640306) for ECG and PPG extrema -* manual editing of extrema ++ manual editing of extrema + extraction of instantaneous features: (heart- or breathing-) rate and period, as well as breathing amplitude + .csv export of extrema and instantaneous features for further analysis (e.g., heart rate variability) + automatic analysis of multiple files (batch processing) diff --git a/docs/index.md b/docs/index.md index aeca208..6a7e915 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,15 +2,15 @@ Welcome to `biopeaks`, a straightforward graphical user interface for feature ex It processes these biosignals semi-automatically with sensible defaults and offers the following functionality: -* processes files in the open biosignal formats [EDF](https://en.wikipedia.org/wiki/European_Data_Format), [OpenSignals (Bitalino)](https://bitalino.com/en/software) ++ processes files in the open biosignal formats [EDF](https://en.wikipedia.org/wiki/European_Data_Format), [OpenSignals (Bitalino)](https://bitalino.com/en/software) as well as plain text files (.txt, .csv, .tsv) -* interactive biosignal visualization -* biosignal segmentation -* benchmarked, automatic extrema detection (R-peaks in ECG, systolic peaks in PPG, exhalation troughs and inhalation ++ interactive biosignal visualization ++ biosignal segmentation ++ benchmarked, automatic extrema detection (R-peaks in ECG, systolic peaks in PPG, exhalation troughs and inhalation peaks in breathing signals) with signal-specific, sensible defaults -* automatic state-of-the-art [artifact correction](https://www.tandfonline.com/doi/full/10.1080/03091902.2019.1640306) ++ automatic state-of-the-art [artifact correction](https://www.tandfonline.com/doi/full/10.1080/03091902.2019.1640306) for ECG and PPG extrema -* manual editing of extrema ++ manual editing of extrema + extraction of instantaneous features: (heart- or breathing-) rate and period, as well as breathing amplitude + .csv export of extrema and instantaneous features for further analysis (e.g., heart rate variability) + automatic analysis of multiple files (batch processing) From 2ef70148226a8062df06eaa22192bf57098d9399 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 14:07:00 +0200 Subject: [PATCH 24/46] Test batch processing with custom files. --- biopeaks/tests/test_gui.py | 52 ++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 8fb5ef4..3a197dd 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -357,19 +357,34 @@ def test_singlefile(qtbot, tmpdir, cfg_single): 4) == cfg_single["avgtidalamp"] -ecg_batch = {"modality": "ECG", - "sigchan": "A3", - "mode": "multiple files", - "filetype": "OpenSignals", - "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", - "OSmontage2A.txt", "OSmontage2J.txt", - "OSmontage3A.txt", "OSmontage3J.txt"], - "peaksums": [3808244, 3412308, 2645824, 3523449, 3611836, - 3457936], - "stats": [(0.7950, 76.1123), (0.7288, 83.1468), - (0.7894, 76.8911), (0.7402, 81.7864), - (0.7856, 76.9153), (0.7235, 83.6060)], - "correctpeaks": False} +ecg_batch_os = {"modality": "ECG", + "sigchan": "A3", + "mode": "multiple files", + "filetype": "OpenSignals", + "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", + "OSmontage2A.txt", "OSmontage2J.txt", + "OSmontage3A.txt", "OSmontage3J.txt"], + "peaksums": [3808244, 3412308, 2645824, 3523449, 3611836, + 3457936], + "stats": [(0.7950, 76.1123), (0.7288, 83.1468), + (0.7894, 76.8911), (0.7402, 81.7864), + (0.7856, 76.9153), (0.7235, 83.6060)], + "correctpeaks": False} + +ecg_batch_custom = {"modality": "ECG", + "header": {"signalidx": 7, "markeridx": None, "skiprows": 3, + "sfreq": 100, "separator": "\t"}, + "mode": "multiple files", + "filetype": "Custom", + "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", + "OSmontage2A.txt", "OSmontage2J.txt", + "OSmontage3A.txt", "OSmontage3J.txt"], + "peaksums": [3808244, 3412308, 2645824, 3523449, 3611836, + 3457936], + "stats": [(0.7950, 76.1123), (0.7288, 83.1468), + (0.7894, 76.8911), (0.7402, 81.7864), + (0.7856, 76.9153), (0.7235, 83.6060)], + "correctpeaks": False} ecg_batch_autocorrect = {"modality": "ECG", "sigchan": 'A3', @@ -389,14 +404,16 @@ def test_singlefile(qtbot, tmpdir, cfg_single): def idcfg_batch(cfg): """Generate a test ID.""" modality = cfg["modality"] + filetype = cfg["filetype"] if cfg["correctpeaks"]: correction = "autocorrection" else: correction = "uncorrected" - return f"{modality}_{correction}" + return f"{modality}:{correction}:{filetype}" -@pytest.fixture(params=[ecg_batch, ecg_batch_autocorrect], ids=idcfg_batch) +@pytest.fixture(params=[ecg_batch_os, ecg_batch_custom, ecg_batch_autocorrect], + ids=idcfg_batch) def cfg_batch(request): return request.param @@ -412,7 +429,10 @@ def test_batchfile(qtbot, tmpdir, cfg_batch): # Configure options. qtbot.keyClicks(view.modmenu, cfg_batch["modality"]) - qtbot.keyClicks(view.sigchanmenu, cfg_batch["sigchan"]) + if cfg_batch["filetype"] == "Custom": + model.customheader = cfg_batch["header"] + else: + qtbot.keyClicks(view.sigchanmenu, cfg_batch["sigchan"]) qtbot.keyClicks(view.batchmenu, cfg_batch["mode"]) view.savecheckbox.setCheckState(Qt.Checked) if cfg_batch["correctpeaks"]: From a1082ea2de1f2c2245578bbf4740816fa9e269b5 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 14:23:46 +0200 Subject: [PATCH 25/46] Gitignore vscode stuff. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c2a8fa8..2cb8339 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ build/ dist/ __pycache__/ biopeaks/__pycache__/ -biopeaks.egg-info/ \ No newline at end of file +biopeaks.egg-info/ +.vscode/ \ No newline at end of file From f19f37397c502320fcdacd91fafa88871d80b0cf Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sat, 25 Jul 2020 15:33:36 +0200 Subject: [PATCH 26/46] Version bump. --- docs/changelog.md | 28 ++++++++++++++++------------ setup.py | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index 0bc6548..62721d5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,9 @@ # Changelog +### Version 1.4.0 (July 25, 2020) ++ enhancement: added support for plain text files (.txt, .csv, .tsv). ++ enhancement: stream [Glasgow University Database (GUDB)](http://researchdata.gla.ac.uk/716/) for ECG benchmarking (download is no longer required). + ### Version 1.3.2 (June 07, 2020) + enhancement: visibility of configuration panel can now be toggled (more screen space for signals). + bugfix: fixed index-out-of-range error in `heart._correct_misaligned()`. @@ -33,10 +37,10 @@ accordance with [Elgendi et al., (2013)](https://journals.plos.org/plosone/artic |metric |summary|version 1.2.0 |:---------:|:-----:|:-----------: -|precision |mean |.996 -| |std |.004 -|sensitivity|mean |.999 -| |std |.001 +|precision |mean |.996 +| |std |.004 +|sensitivity|mean |.999 +| |std |.001 + bugfix: the PATCH version has been reset to 0 after incrementing MINOR version (https://semver.org/) @@ -80,14 +84,14 @@ sample. |condition|metric |summary|version 1.0.2|version 1.0.3 |:-------:|:---------:|:-----:|:-----------:|:-----------: -|sitting |precision |mean |.999 |.998 -| | |std |.002 |.005 -| |sensitivity|mean |.996 |.996 -| | |std |.008 |.004 -|handbike |precision |mean |.904 |.930 -| | |std |.135 |.127 -| |sensitivity|mean |.789 |.857 -| | |std |.281 |.247 +|sitting |precision |mean |.999 |.998 +| | |std |.002 |.005 +| |sensitivity|mean |.996 |.996 +| | |std |.008 |.004 +|handbike |precision |mean |.904 |.930 +| | |std |.135 |.127 +| |sensitivity|mean |.789 |.857 +| | |std |.281 |.247 ### Version 1.0.2 (December 1, 2019) + enhancement: `resp.resp_extrema()` is now based on zerocrossings and makes diff --git a/setup.py b/setup.py index 48e163d..8c2e6c5 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup( name="biopeaks", - version="1.3.2", + version="1.4.0", description="A graphical user interface for feature extraction from heart- and breathing biosignals.", url="https://github.com/JanCBrammer/biopeaks", author="Jan C. Brammer", From 377b373f548031b8799806859cde59237793328f Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Sun, 26 Jul 2020 19:48:40 +0200 Subject: [PATCH 27/46] Replace branching with iterator in batch processing. --- biopeaks/controller.py | 87 ++++++++++++++++++-------------------- biopeaks/tests/test_gui.py | 26 +++++++----- 2 files changed, 58 insertions(+), 55 deletions(-) diff --git a/biopeaks/controller.py b/biopeaks/controller.py index 335cdb5..e6c6ea7 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from functools import wraps from .heart import ecg_peaks, ppg_peaks, correct_peaks, heart_period from .resp import resp_extrema, resp_stats from .io_utils import (read_custom, read_opensignals, read_edf, @@ -51,6 +52,7 @@ def run(self): # decorator that runs Controller methods in Worker thread def threaded(fn): + @wraps(fn) def threader(controller, **kwargs): worker = Worker(fn, controller, **kwargs) worker.signals.progress.connect(controller._model.progress) @@ -82,7 +84,7 @@ def get_fpaths(self): elif (self._model.batchmode == 'single file' and len(self._model.fpaths) == 1): self._model.reset() - self.read_channels(path=self._model.fpaths[0]) + self.read_channels() def get_wpathsignal(self): @@ -175,7 +177,7 @@ def get_wpathstats(self): def batch_processor(self): - ''' + """ Initiates batch processing. After initiation, the dispatcher method handles the sequential execution of methods that are called on each file of the batch. The dispatcher only listenes to the threader's @@ -193,75 +195,70 @@ def batch_processor(self): in rapid succession (i.e., when small files are processed). The user can still get an impression of the progress since the current file path is displayed. - TODO: make method calls and their order more explicit! - ''' + """ self.get_wpathpeaks() self.get_wpathstats() - self.methodnb = -1 - self.nmethods = 5 if self._model.wdirstats is None: self._model.status = "No statistics selected for saving." return - self.filenb = 0 - self.nfiles = len(self._model.fpaths) self._model.status = "Processing files." self._model.plotting = False + + self.batchmethods = [self.read_channels, self.find_peaks, + self.autocorrect_peaks, self.calculate_stats, + self.save_stats] + if self._model.wdirpeaks: # optional + self.batchmethods.append(self.save_peaks) + + self.iterbatchmethods = iter(self.batchmethods) + self._model.progress_changed.connect(self.dispatcher) - # initiate processing - self.dispatcher(1) + self.dispatcher(1) # initiate processing def dispatcher(self, progress): - ''' + """ Start with first method on first file. As soon as one method has finished, (indicated by emission of progress_changed), execute next method. Once all methods are executed, go to the next file and start cycling through methods again. - ''' + """ if not progress: return - if self.filenb == self.nfiles: # works because filenb starts at 0 - self._model.plotting = True - self._model.progress_changed.disconnect(self.dispatcher) - self._model.wdirpeaks = None - self._model.wdirstats = None - return - fpath = self._model.fpaths[self.filenb] - fname = Path(fpath).stem - self.methodnb += 1 - if self.methodnb == 0: - self._model.reset() - self.read_channels(path=fpath) - elif self.methodnb == 1: - self.find_peaks() - elif self.methodnb == 2: - self.autocorrect_peaks() - elif self.methodnb == 3: - self.calculate_stats() - elif self.methodnb == 4: - p = Path(self._model.wdirstats).joinpath(f"{fname}_stats.csv") - self._model.wpathstats = p - self.save_stats() - elif self.methodnb == self.nmethods: - # once all methods are executed, move to next file and start with - # first method again - self.methodnb = -1 - self.filenb += 1 + + try: + batchmethod = next(self.iterbatchmethods) + + except StopIteration: # all methods finished on current file + self._model.reset() # reset for new file + self.iterbatchmethods = iter(self.batchmethods) # restart cycling through batch methods + batchmethod = next(self.iterbatchmethods) + self._model.fpaths.pop(0) # go to next file + + if not self._model.fpaths: # all files have been processed + self._model.plotting = True + self._model.progress_changed.disconnect(self.dispatcher) + self._model.wdirpeaks = None + self._model.wdirstats = None + return + + if batchmethod.__name__ == "read_channels": # set paths prior to calling first method + fname = Path(self._model.fpaths[0]).stem + self._model.wpathstats = Path(self._model.wdirstats).joinpath(f"{fname}_stats.csv") if self._model.wdirpeaks: # optional - p = Path(self._model.wdirpeaks).joinpath(f"{fname}_peaks.csv") - self._model.wpathpeaks = p - self.save_peaks() - else: - self.dispatcher(1) + self._model.wpathpeaks = Path(self._model.wdirpeaks).joinpath(f"{fname}_peaks.csv") + + batchmethod() @threaded - def read_channels(self, path): + def read_channels(self): self._model.status = "Loading file." + path = self._model.fpaths[0] filetype = self._model.filetype readfunc = readfuncs[filetype] diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 3a197dd..8ebbf76 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -249,9 +249,10 @@ def test_singlefile(qtbot, tmpdir, cfg_single): model.set_filetype(cfg_single["filetype"]) # 1. load signal ######################################################### + model.fpaths = [cfg_single["sigpathorig"]] with qtbot.waitSignals([model.signal_changed, model.marker_changed], timeout=10000): - controller.read_channels(path=cfg_single["sigpathorig"]) + controller.read_channels() assert np.size(model.signal) == cfg_single["siglen"] assert np.size(model.sec) == cfg_single["siglen"] assert np.size(model.marker) == cfg_single["markerlen"] @@ -306,9 +307,10 @@ def test_singlefile(qtbot, tmpdir, cfg_single): controller.save_peaks() # 7. re-load signal ####################################################### + model.fpaths = [tmpdir.join(cfg_single["sigfnameseg"])] with qtbot.waitSignals([model.signal_changed, model.marker_changed], timeout=10000): - controller.read_channels(path=tmpdir.join(cfg_single["sigfnameseg"])) + controller.read_channels() sfreq = cfg_single["header"]["sfreq"] if cfg_single["filetype"] == "Custom" else cfg_single["sfreq"] assert model.sfreq == sfreq assert model.loaded @@ -444,31 +446,35 @@ def test_batchfile(qtbot, tmpdir, cfg_batch): # Mock the controller's batch_processor in order to avoid # calls to the controller's get_wpathpeaks and get_wpathstats methods. - controller.methodnb = -1 - controller.nmethods = 5 - controller.filenb = 0 - controller.nfiles = len(model.fpaths) - model.wdirpeaks = tmpdir model.wdirstats = tmpdir model.status = 'processing files' model.plotting = False + + controller.batchmethods = [controller.read_channels, controller.find_peaks, + controller.autocorrect_peaks, + controller.calculate_stats, + controller.save_stats, + controller.save_peaks] + controller.iterbatchmethods = iter(controller.batchmethods) + model.progress_changed.connect(controller.dispatcher) # Initiate batch processing. controller.dispatcher(1) # Wait for all files to be processed. - while controller.filenb < controller.nfiles: - qtbot.wait(500) + while not model.plotting: # dispatcher enables plotting once all files are processed + qtbot.wait(1000) # Load each peak file saved during batch processing and assess if # peaks have been identified correctly. for sigfname, peaksum in zip(cfg_batch["sigfnames"], cfg_batch["peaksums"]): with qtbot.waitSignal(model.signal_changed, timeout=5000): - controller.read_channels(path=datadir.joinpath(sigfname)) + model.fpaths = [datadir.joinpath(sigfname)] + controller.read_channels() fname = Path(sigfname).stem model.rpathpeaks = tmpdir.join(f"{fname}_peaks.csv") with qtbot.waitSignal(model.peaks_changed, timeout=5000): From 3ee3cf2de1b352d9967eed2b72910ee9f993e1d8 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Wed, 29 Jul 2020 18:42:57 +0200 Subject: [PATCH 28/46] Clean up breathing extrema detection. --- biopeaks/resp.py | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/biopeaks/resp.py b/biopeaks/resp.py index fb0387c..8524b16 100644 --- a/biopeaks/resp.py +++ b/biopeaks/resp.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import numpy as np +from itertools import cycle from .filters import butter_bandpass_filter from .analysis_utils import interp_stats @@ -25,34 +26,18 @@ def resp_extrema(signal, sfreq): risex = np.where(np.bitwise_and(smaller[:-1], greater[1:]))[0] fallx = np.where(np.bitwise_and(greater[:-1], smaller[1:]))[0] - if risex[0] < fallx[0]: - startx = "rise" - elif fallx[0] < risex[0]: - startx = "fall" - allx = np.concatenate((risex, fallx)) allx.sort(kind="mergesort") + argextreme = cycle([np.argmax, np.argmin]) + if fallx[0] < risex[0]: + next(argextreme) # cycle once to switch order + # find extrema extrema = [] - for i in range(len(allx) - 1): - - # determine whether to search for min or max - if startx == "rise": - if (i + 1) % 2 != 0: - argextreme = np.argmax - else: - argextreme = np.argmin - elif startx == "fall": - if (i + 1) % 2 != 0: - argextreme = np.argmin - else: - argextreme = np.argmax - - beg = allx[i] - end = allx[i + 1] - - extreme = argextreme(signal[beg:end]) + for beg, end in zip(allx[0:], allx[1:]): + + extreme = next(argextreme)(signal[beg:end]) extrema.append(beg + extreme) extrema = np.asarray(extrema) From 24fe729a4ca1272b4042b85731f5a08580996fa2 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Thu, 30 Jul 2020 13:23:01 +0200 Subject: [PATCH 29/46] Smore zip instead of indexing. --- biopeaks/heart.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/biopeaks/heart.py b/biopeaks/heart.py index ddc7a66..073edc7 100644 --- a/biopeaks/heart.py +++ b/biopeaks/heart.py @@ -50,12 +50,9 @@ def ecg_peaks(signal, sfreq, smoothwindow=.1, avgwindow=.75, min_len = np.mean(end_qrs[:num_qrs] - beg_qrs[:num_qrs]) * minlenweight peaks = [0] - for i in range(num_qrs): + for beg, end in zip(beg_qrs, end_qrs): - beg = beg_qrs[i] - end = end_qrs[i] len_qrs = end - beg - if len_qrs < min_len: continue @@ -123,17 +120,13 @@ def ppg_peaks(signal, sfreq, peakwindow=.111, beatwindow=.667, beatoffset=.02, end_waves = end_waves[end_waves > beg_waves[0]] # Identify systolic peaks within waves (ignore waves that are too short). - num_waves = min(beg_waves.size, end_waves.size) min_len = int(np.rint(peakwindow * sfreq)) min_delay = int(np.rint(mindelay * sfreq)) peaks = [0] - for i in range(num_waves): + for beg, end in zip(beg_waves, end_waves): - beg = beg_waves[i] - end = end_waves[i] len_wave = end - beg - if len_wave < min_len: continue From d0442a47adc0b9ec05c71bda6865f2396d00e528 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 16:24:59 +0200 Subject: [PATCH 30/46] Trying out new test workflow (0). --- .github/workflows/build.yml | 52 ------------- .github/workflows/test.yml | 148 ++++++++++++++++++++++++++++++++++++ environment.yml | 7 +- 3 files changed, 153 insertions(+), 54 deletions(-) delete mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index ebabb0d..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,52 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: build -on: - push: - branches: dev - -jobs: - build: - - runs-on: ${{ matrix.os }} - continue-on-error: true - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.7, 3.8] - - steps: - - uses: actions/checkout@v2 - - name: Set-up miniconda - uses: goanpeca/setup-miniconda@v1 - with: - auto-update-conda: true - channels: conda-forge - channel-priority: strict - activate-environment: biopeaks-build - environment-file: environment.yml - python-version: ${{ matrix.python-version }} - auto-activate-base: false - - name: Install editable biopeaks - shell: bash -l {0} - run: | - pip install -e . - - name: Lint with flake8 - shell: bash -l {0} - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest on Ubuntu - if: matrix.os == 'ubuntu-latest' - shell: bash -l {0} - run: | - sudo apt-get install -y xvfb x11-utils libxkbcommon-x11-0 - xvfb-run pytest -v - - name: Test with pytest on Windows and MacOS - if: matrix.os == 'windows-latest' || matrix.os == 'macos-latest' - shell: bash -l {0} - run: | - pytest -v diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d764738 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,148 @@ +name: test +on: + push: + branches: + - master + - dev + pull_request: + branches: + - master + - dev + +jobs: + + lint: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade setuptools pip wheel + python -m pip install flake8 + - name: Linting + run: | + flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata --max-line-length=90 --max-complexity=10 --ignore E303,C901,E203,W503 + + + build: + + needs: lint # only run if lint finishes + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + - run: | + python -m pip install --upgrade setuptools pip wheel + - name: Build source and wheel + - run: | + python setup.py sdist bdist_wheels + - name: Upload source + uses: actions/upload-artifact@v2 + with: + name: source + path: dist/*.tar.gz + - name: Upload wheel + uses: actions/upload-artifact@v2 + with: + name: wheel + path: dist/*.whl + + + test_source: + + needs: build # only run if build finishes + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up miniconda + uses: goanpeca/setup-miniconda@v1 + with: + auto-update-conda: true + channels: conda-forge + channel-priority: strict + activate-environment: ci_env # inclused pytest, setuptools + environment-file: condaenv.yml + python-version: ${{ matrix.python-version }} + auto-activate-base: false + - name: Download source + uses: actions/download-artifact@v2 + with: + name: source + path: source + - name: Install source + run: | + pip install --find-links=source biopeaks + - name: Test with pytest on Ubuntu + if: matrix.os == 'ubuntu-latest' + shell: bash -l {0} + run: | + sudo apt-get install -y xvfb x11-utils libxkbcommon-x11-0 + xvfb-run pytest -v + - name: Test with pytest on Windows and MacOS + if: matrix.os == 'windows-latest' || matrix.os == 'macos-latest' + shell: bash -l {0} + run: | + pytest -v + + + test_wheel: + + needs: build # only run if build finishes + runs-on: ${{ matrix.os }} + continue-on-error: false + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up miniconda + uses: goanpeca/setup-miniconda@v1 + with: + auto-update-conda: true + channels: conda-forge + channel-priority: strict + activate-environment: ci_env # inclused pytest, setuptools + environment-file: condaenv.yml + python-version: ${{ matrix.python-version }} + auto-activate-base: false + - name: Download wheel + uses: actions/download-artifact@v2 + with: + name: wheel + path: wheel + - name: Install wheel + run: | + pip install --find-links=wheel biopeaks + - name: Test with pytest on Ubuntu + if: matrix.os == 'ubuntu-latest' + shell: bash -l {0} + run: | + sudo apt-get install -y xvfb x11-utils libxkbcommon-x11-0 + xvfb-run pytest -v + - name: Test with pytest on Windows and MacOS + if: matrix.os == 'windows-latest' || matrix.os == 'macos-latest' + shell: bash -l {0} + run: | + pytest -v diff --git a/environment.yml b/environment.yml index c767f73..c4a9061 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: biopeaks-build +name: ci_env channels: - conda-forge - defaults @@ -9,4 +9,7 @@ dependencies: - pyside2 - pandas - pytest-qt - - flake8 \ No newline at end of file + - flake8 + - setuptools + - wheel + - twine \ No newline at end of file From ee33ebd22067fe8b750361968d9927fae2ae8711 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 16:43:57 +0200 Subject: [PATCH 31/46] Trying out new test workflow (1). --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d764738..026ad5f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: python -m pip install flake8 - name: Linting run: | - flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata --max-line-length=90 --max-complexity=10 --ignore E303,C901,E203,W503 + flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata --max-line-length=90 --max-complexity=10 --ignore E303,C901,E203,W503 build: @@ -81,7 +81,7 @@ jobs: channels: conda-forge channel-priority: strict activate-environment: ci_env # inclused pytest, setuptools - environment-file: condaenv.yml + environment-file: environment.yml python-version: ${{ matrix.python-version }} auto-activate-base: false - name: Download source @@ -124,7 +124,7 @@ jobs: channels: conda-forge channel-priority: strict activate-environment: ci_env # inclused pytest, setuptools - environment-file: condaenv.yml + environment-file: environment.yml python-version: ${{ matrix.python-version }} auto-activate-base: false - name: Download wheel From 4f0af6d2ee6568c6d55e7f9fbb2005685e7df334 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 16:53:19 +0200 Subject: [PATCH 32/46] Trying out new test workflow (2). --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 026ad5f..c716d7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: matrix: python-version: [3.7, 3.8] - steps: + steps: # every step is marked with a leading hyphen (-) - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 @@ -45,10 +45,10 @@ jobs: with: python-version: 3.7 - name: Install dependencies - - run: | + run: | python -m pip install --upgrade setuptools pip wheel - name: Build source and wheel - - run: | + run: | python setup.py sdist bdist_wheels - name: Upload source uses: actions/upload-artifact@v2 From fa4672e683e58f7d9d86c4340168033f7a418845 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 17:18:07 +0200 Subject: [PATCH 33/46] Fix flake8 complaints. --- biopeaks/__main__.py | 1 - biopeaks/benchmarks/benchmark_ECG_local.py | 1 - biopeaks/controller.py | 1 + biopeaks/model.py | 2 +- biopeaks/tests/test_gui.py | 22 +++++++++++----------- biopeaks/view.py | 14 +++++++------- 6 files changed, 20 insertions(+), 21 deletions(-) diff --git a/biopeaks/__main__.py b/biopeaks/__main__.py index 64c1034..8c83792 100644 --- a/biopeaks/__main__.py +++ b/biopeaks/__main__.py @@ -23,4 +23,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/biopeaks/benchmarks/benchmark_ECG_local.py b/biopeaks/benchmarks/benchmark_ECG_local.py index 6eaa31a..c37d567 100644 --- a/biopeaks/benchmarks/benchmark_ECG_local.py +++ b/biopeaks/benchmarks/benchmark_ECG_local.py @@ -2,7 +2,6 @@ import glob import os -import matplotlib.pyplot as plt import numpy as np import pandas as pd from biopeaks.heart import ecg_peaks diff --git a/biopeaks/controller.py b/biopeaks/controller.py index e6c6ea7..3ee9421 100644 --- a/biopeaks/controller.py +++ b/biopeaks/controller.py @@ -28,6 +28,7 @@ "OpenSignals": write_opensignals, "EDF": write_edf} + # threading is implemented according to https://pythonguis.com/courses/ # multithreading-pyqt-applications-qthreadpool/complete-example/ class WorkerSignals(QObject): diff --git a/biopeaks/model.py b/biopeaks/model.py index 3f45d66..68cc024 100644 --- a/biopeaks/model.py +++ b/biopeaks/model.py @@ -360,7 +360,7 @@ def __init__(self): self._wdirstats = None self._savebatchpeaks = False self._correctbatchpeaks = False - self._savestats = {"period":False, "rate":False, "tidalamp":False} + self._savestats = {"period": False, "rate": False, "tidalamp": False} self._filetype = None self._customheader = {"signalidx": None, "markeridx": None, "skiprows": None, "sfreq": None, "separator": None} diff --git a/biopeaks/tests/test_gui.py b/biopeaks/tests/test_gui.py index 8ebbf76..bfc47d3 100644 --- a/biopeaks/tests/test_gui.py +++ b/biopeaks/tests/test_gui.py @@ -309,7 +309,7 @@ def test_singlefile(qtbot, tmpdir, cfg_single): # 7. re-load signal ####################################################### model.fpaths = [tmpdir.join(cfg_single["sigfnameseg"])] with qtbot.waitSignals([model.signal_changed, model.marker_changed], - timeout=10000): + timeout=10000): controller.read_channels() sfreq = cfg_single["header"]["sfreq"] if cfg_single["filetype"] == "Custom" else cfg_single["sfreq"] assert model.sfreq == sfreq @@ -364,13 +364,13 @@ def test_singlefile(qtbot, tmpdir, cfg_single): "mode": "multiple files", "filetype": "OpenSignals", "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", - "OSmontage2A.txt", "OSmontage2J.txt", - "OSmontage3A.txt", "OSmontage3J.txt"], + "OSmontage2A.txt", "OSmontage2J.txt", + "OSmontage3A.txt", "OSmontage3J.txt"], "peaksums": [3808244, 3412308, 2645824, 3523449, 3611836, - 3457936], + 3457936], "stats": [(0.7950, 76.1123), (0.7288, 83.1468), - (0.7894, 76.8911), (0.7402, 81.7864), - (0.7856, 76.9153), (0.7235, 83.6060)], + (0.7894, 76.8911), (0.7402, 81.7864), + (0.7856, 76.9153), (0.7235, 83.6060)], "correctpeaks": False} ecg_batch_custom = {"modality": "ECG", @@ -379,13 +379,13 @@ def test_singlefile(qtbot, tmpdir, cfg_single): "mode": "multiple files", "filetype": "Custom", "sigfnames": ["OSmontage1A.txt", "OSmontage1J.txt", - "OSmontage2A.txt", "OSmontage2J.txt", - "OSmontage3A.txt", "OSmontage3J.txt"], + "OSmontage2A.txt", "OSmontage2J.txt", + "OSmontage3A.txt", "OSmontage3J.txt"], "peaksums": [3808244, 3412308, 2645824, 3523449, 3611836, - 3457936], + 3457936], "stats": [(0.7950, 76.1123), (0.7288, 83.1468), - (0.7894, 76.8911), (0.7402, 81.7864), - (0.7856, 76.9153), (0.7235, 83.6060)], + (0.7894, 76.8911), (0.7402, 81.7864), + (0.7856, 76.9153), (0.7235, 83.6060)], "correctpeaks": False} ecg_batch_autocorrect = {"modality": "ECG", diff --git a/biopeaks/view.py b/biopeaks/view.py index 6657e9c..8abf95d 100644 --- a/biopeaks/view.py +++ b/biopeaks/view.py @@ -4,7 +4,7 @@ QVBoxLayout, QHBoxLayout, QCheckBox, QLabel, QStatusBar, QGroupBox, QDockWidget, QLineEdit, QFormLayout, QPushButton, - QProgressBar, QSplitter, QMenu, QDialog) + QProgressBar, QSplitter, QDialog) from PySide2.QtCore import Qt, QSignalMapper, QRegExp from PySide2.QtGui import QIcon, QRegExpValidator from matplotlib.figure import Figure @@ -29,7 +29,7 @@ def __init__(self, model, controller): self._model = model self._controller = controller self.segmentcursor = False - self.togglecolors = {"#1f77b4":"m", "m":"#1f77b4"} + self.togglecolors = {"#1f77b4": "m", "m": "#1f77b4"} ################################################################# @@ -47,7 +47,7 @@ def __init__(self, model, controller): # mpl to throw an error because figure is resized to height 0. The # widget can still be fully collapsed with self.splitter- self.canvas0.setMinimumHeight(1) # in pixels - self.ax00 = self.figure0.add_subplot(1,1,1) + self.ax00 = self.figure0.add_subplot(1, 1, 1) self.ax00.set_frame_on(False) self.figure0.subplots_adjust(left=0.04, right=0.98, bottom=0.25) self.line00 = None @@ -59,7 +59,7 @@ def __init__(self, model, controller): self.figure1 = Figure() self.canvas1 = FigureCanvas(self.figure1) self.canvas1.setMinimumHeight(1) - self.ax10 = self.figure1.add_subplot(1,1,1, sharex=self.ax00) + self.ax10 = self.figure1.add_subplot(1, 1, 1, sharex=self.ax00) self.ax10.get_xaxis().set_visible(False) self.ax10.set_frame_on(False) self.figure1.subplots_adjust(left=0.04, right=0.98) @@ -70,15 +70,15 @@ def __init__(self, model, controller): self.figure2 = Figure() self.canvas2 = FigureCanvas(self.figure2) self.canvas2.setMinimumHeight(1) - self.ax20 = self.figure2.add_subplot(3,1,1, sharex=self.ax00) + self.ax20 = self.figure2.add_subplot(3, 1, 1, sharex=self.ax00) self.ax20.get_xaxis().set_visible(False) self.ax20.set_frame_on(False) self.line20 = None - self.ax21 = self.figure2.add_subplot(3,1,2, sharex=self.ax00) + self.ax21 = self.figure2.add_subplot(3, 1, 2, sharex=self.ax00) self.ax21.get_xaxis().set_visible(False) self.ax21.set_frame_on(False) self.line21 = None - self.ax22 = self.figure2.add_subplot(3,1,3, sharex=self.ax00) + self.ax22 = self.figure2.add_subplot(3, 1, 3, sharex=self.ax00) self.ax22.get_xaxis().set_visible(False) self.ax22.set_frame_on(False) self.line22 = None From a9689562bf9190f101fdb0a5f79c0521ca3676a7 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 17:18:52 +0200 Subject: [PATCH 34/46] Trying out new test workflow (3). --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c716d7d..c6c2465 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: python -m pip install flake8 - name: Linting run: | - flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata --max-line-length=90 --max-complexity=10 --ignore E303,C901,E203,W503 + flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata,biopeaks/resources.py --max-complexity=10 --ignore E303,C901,E203,W503,E501,W504,E129,W605,E371 build: From 69eb1faa58912a3b2057b04f444e142fce866235 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 17:20:38 +0200 Subject: [PATCH 35/46] Trying out new test workflow (4). --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c6c2465..17ce898 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -30,7 +30,7 @@ jobs: python -m pip install flake8 - name: Linting run: | - flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata,biopeaks/resources.py --max-complexity=10 --ignore E303,C901,E203,W503,E501,W504,E129,W605,E371 + flake8 biopeaks --exclude biopeaks/images,biopeaks/tests/testdata,biopeaks/resources.py --max-complexity=10 --ignore E303,C901,E203,W503,E501,W504,E129,W605,E371,E731 build: From 14143fcf6f4d9436e71c3218156357f5fb566b1a Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 17:22:38 +0200 Subject: [PATCH 36/46] Trying out new test workflow (5). --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17ce898..d9396a6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -49,7 +49,7 @@ jobs: python -m pip install --upgrade setuptools pip wheel - name: Build source and wheel run: | - python setup.py sdist bdist_wheels + python setup.py sdist bdist_wheel - name: Upload source uses: actions/upload-artifact@v2 with: From 2c08c2b11c1115a438a4e96fdfe1e0d8ace7acce Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 17:55:11 +0200 Subject: [PATCH 37/46] Trying out new test workflow (6). --- .github/workflows/test.yml | 8 ++++---- environment.yml | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d9396a6..152e995 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,10 +88,10 @@ jobs: uses: actions/download-artifact@v2 with: name: source - path: source + path: source_download - name: Install source run: | - pip install --find-links=source biopeaks + pip install --find-links=biopeaks\source_download biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} @@ -131,10 +131,10 @@ jobs: uses: actions/download-artifact@v2 with: name: wheel - path: wheel + path: wheel_download - name: Install wheel run: | - pip install --find-links=wheel biopeaks + pip install --find-links=biopeaks\wheel_download biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} diff --git a/environment.yml b/environment.yml index c4a9061..2f43ff7 100644 --- a/environment.yml +++ b/environment.yml @@ -1,7 +1,6 @@ name: ci_env channels: - conda-forge - - defaults dependencies: - scipy - numpy From 7bb30c5e781a3b3e2768432562af581f3068b518 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 18:01:24 +0200 Subject: [PATCH 38/46] Trying out new test workflow (7). --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 152e995..0c3bad3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,7 +91,7 @@ jobs: path: source_download - name: Install source run: | - pip install --find-links=biopeaks\source_download biopeaks + pip install --find-links=biopeaks/source_download biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} @@ -134,7 +134,7 @@ jobs: path: wheel_download - name: Install wheel run: | - pip install --find-links=biopeaks\wheel_download biopeaks + pip install --find-links=biopeaks/wheel_download biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} From 84b7ef87b4316503a5e0037c110f8a4704d87bf3 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 18:31:37 +0200 Subject: [PATCH 39/46] Trying out new test workflow (8). --- .github/workflows/test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c3bad3..7e6e625 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -88,10 +88,11 @@ jobs: uses: actions/download-artifact@v2 with: name: source - path: source_download - name: Install source + shell: bash -l {0} run: | - pip install --find-links=biopeaks/source_download biopeaks + ls + pip install --find-links=source biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} @@ -131,10 +132,11 @@ jobs: uses: actions/download-artifact@v2 with: name: wheel - path: wheel_download - name: Install wheel + shell: bash -l {0} run: | - pip install --find-links=biopeaks/wheel_download biopeaks + ls + pip install --find-links=wheel biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} From 57fd61fa290f86f127a9b7a70b7495c63d96e11b Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Mon, 3 Aug 2020 18:55:25 +0200 Subject: [PATCH 40/46] Trying out new test workflow (9). --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e6e625..47c0b05 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,7 +92,7 @@ jobs: shell: bash -l {0} run: | ls - pip install --find-links=source biopeaks + pip install --no-index --find-links=. biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} @@ -136,7 +136,7 @@ jobs: shell: bash -l {0} run: | ls - pip install --find-links=wheel biopeaks + pip install --no-index --find-links=. biopeaks - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} From 7331eecb7180cbf6b26d3682791419470ca52832 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 4 Aug 2020 08:21:53 +0200 Subject: [PATCH 41/46] Only lint Python 3.7. --- .github/workflows/test.yml | 9 +++------ environment.yml | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47c0b05..392d8fa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,16 +14,13 @@ jobs: lint: runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.7, 3.8] steps: # every step is marked with a leading hyphen (-) - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: - python-version: ${{ matrix.python-version }} + python-version: 3.7 - name: Install dependencies run: | python -m pip install --upgrade setuptools pip wheel @@ -68,7 +65,7 @@ jobs: runs-on: ${{ matrix.os }} continue-on-error: false strategy: - matrix: + matrix: # all steps of this job will run using this matrix (matrix not accessible to other jobs) os: [ubuntu-latest, windows-latest, macos-latest] python-version: [3.7, 3.8] @@ -92,7 +89,7 @@ jobs: shell: bash -l {0} run: | ls - pip install --no-index --find-links=. biopeaks + pip install --no-index --find-links=. biopeaks # --no-index: do not install from PyPI, --find-links=.:search local dir for wheels or source - name: Test with pytest on Ubuntu if: matrix.os == 'ubuntu-latest' shell: bash -l {0} diff --git a/environment.yml b/environment.yml index 2f43ff7..1b26a58 100644 --- a/environment.yml +++ b/environment.yml @@ -8,7 +8,6 @@ dependencies: - pyside2 - pandas - pytest-qt - - flake8 - setuptools - wheel - twine \ No newline at end of file From 13e8e8127552e40384ed3026265826d12ad39089 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 4 Aug 2020 09:33:45 +0200 Subject: [PATCH 42/46] Set up publish workflow (0). --- .github/workflows/publish.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..157adee --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +name: publish +on: + # release: + # types: [published] + push: + branches: + - dev + +jobs: + + build_publish: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Install dependencies + run: | + python -m pip install --upgrade setuptools pip wheel + - name: Build source and wheel + run: | + python setup.py sdist bdist_wheel + - name: Publish + env: + TWINE_USERNAME: ${{ secrets.TESTPYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TESTPYPI_PASSWORD }} + run: | + twine upload --repository testpypi dist/* \ No newline at end of file From 799c30b209edd45223be8c3e7f244c8766d2c879 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 4 Aug 2020 09:43:54 +0200 Subject: [PATCH 43/46] Set up publish workflow (1). --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 157adee..2aa216d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,4 +29,4 @@ jobs: TWINE_USERNAME: ${{ secrets.TESTPYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.TESTPYPI_PASSWORD }} run: | - twine upload --repository testpypi dist/* \ No newline at end of file + twine upload --repository-url https://test.pypi.org/legacy/ dist/* \ No newline at end of file From 7b9935417842438e04cdfde138a808f14440ffb8 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 4 Aug 2020 09:51:23 +0200 Subject: [PATCH 44/46] Set up publish workflow (2). --- .github/workflows/publish.yml | 2 +- .github/workflows/test.yml | 4 ++-- environment.yml | 5 +---- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2aa216d..98b7892 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -20,7 +20,7 @@ jobs: python-version: 3.7 - name: Install dependencies run: | - python -m pip install --upgrade setuptools pip wheel + python -m pip install --upgrade setuptools pip wheel twine - name: Build source and wheel run: | python setup.py sdist bdist_wheel diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 392d8fa..ff44f00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,7 +77,7 @@ jobs: auto-update-conda: true channels: conda-forge channel-priority: strict - activate-environment: ci_env # inclused pytest, setuptools + activate-environment: ci_env # includes pytest and pip environment-file: environment.yml python-version: ${{ matrix.python-version }} auto-activate-base: false @@ -121,7 +121,7 @@ jobs: auto-update-conda: true channels: conda-forge channel-priority: strict - activate-environment: ci_env # inclused pytest, setuptools + activate-environment: ci_env environment-file: environment.yml python-version: ${{ matrix.python-version }} auto-activate-base: false diff --git a/environment.yml b/environment.yml index 1b26a58..6bcf6b0 100644 --- a/environment.yml +++ b/environment.yml @@ -7,7 +7,4 @@ dependencies: - matplotlib - pyside2 - pandas - - pytest-qt - - setuptools - - wheel - - twine \ No newline at end of file + - pytest-qt \ No newline at end of file From d42b6dfb8e63a2f72cf8d2f66e844ccd859e95a5 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 4 Aug 2020 10:02:02 +0200 Subject: [PATCH 45/46] Set up publish workflow (3). --- .github/workflows/publish.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 98b7892..51d6038 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,10 +1,8 @@ name: publish on: - # release: - # types: [published] - push: - branches: - - dev + release: + types: [published] + jobs: @@ -25,8 +23,13 @@ jobs: run: | python setup.py sdist bdist_wheel - name: Publish + # env: + # TWINE_USERNAME: ${{ secrets.TESTPYPI_USERNAME }} + # TWINE_PASSWORD: ${{ secrets.TESTPYPI_PASSWORD }} + # run: | + # twine upload --repository-url https://test.pypi.org/legacy/ dist/* env: - TWINE_USERNAME: ${{ secrets.TESTPYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.TESTPYPI_PASSWORD }} + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | - twine upload --repository-url https://test.pypi.org/legacy/ dist/* \ No newline at end of file + twine upload dist/* From 7305f745dc8c9fc594b04b9f455d50f783e4f244 Mon Sep 17 00:00:00 2001 From: JohnDoenut Date: Tue, 4 Aug 2020 10:20:08 +0200 Subject: [PATCH 46/46] Version bump. --- README.md | 2 +- docs/changelog.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dbda816..a374b77 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![logo](docs/images/logo.png) -![](https://github.com/JanCBrammer/biopeaks/workflows/build/badge.svg?branch=dev) +![](https://github.com/JanCBrammer/biopeaks/workflows/test/badge.svg?branch=dev) # Citation Click on the badge below to cite `biopeaks` in a format of your choice. diff --git a/docs/changelog.md b/docs/changelog.md index 62721d5..282c457 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,6 @@ # Changelog -### Version 1.4.0 (July 25, 2020) +### Version 1.4.0 (August 04, 2020) + enhancement: added support for plain text files (.txt, .csv, .tsv). + enhancement: stream [Glasgow University Database (GUDB)](http://researchdata.gla.ac.uk/716/) for ECG benchmarking (download is no longer required).