From ae09b688b289f85dc8604e83f04cbbddf53faa77 Mon Sep 17 00:00:00 2001 From: jim Date: Wed, 3 Jun 2015 12:56:11 -0500 Subject: [PATCH 1/6] #42 refactored and tidied up a bit, created test files --- nd2reader/__init__.py | 4 +- nd2reader/parser.py | 189 +++++++++++++++++++++--------------------- tests.py | 5 ++ tests/__init__.py | 0 4 files changed, 103 insertions(+), 95 deletions(-) create mode 100644 tests.py create mode 100644 tests/__init__.py diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index 7f9cf1e..aa0d14f 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -16,7 +16,7 @@ class Nd2(Nd2Parser): def __repr__(self): return "\n".join(["" % self._filename, - "Created: %s" % self._absolute_start.strftime("%Y-%m-%d %H:%M:%S"), + "Created: %s" % self.absolute_start.strftime("%Y-%m-%d %H:%M:%S"), "Image size: %sx%s (HxW)" % (self.height, self.width), "Image cycles: %s" % len(self.time_indexes), "Channels: %s" % ", ".join(["'%s'" % str(channel) for channel in self.channels]), @@ -32,7 +32,7 @@ class Nd2(Nd2Parser): :rtype: int """ - return self._image_count * self._channel_count + return self._total_images_per_channel * self._channel_count def __getitem__(self, item): """ diff --git a/nd2reader/parser.py b/nd2reader/parser.py index 5aabcaa..34acc24 100644 --- a/nd2reader/parser.py +++ b/nd2reader/parser.py @@ -19,6 +19,7 @@ class Nd2Parser(object): CHUNK_MAP_END = six.b("ND2 CHUNK MAP SIGNATURE 0000001!") def __init__(self, filename): + self._absolute_start = None self._filename = filename self._fh = None self._channels = None @@ -34,6 +35,99 @@ class Nd2Parser(object): self._parse_metadata() self._z_levels = None + @property + def absolute_start(self): + """ + The date and time when acquisition began. + + :rtype: datetime.datetime() + + """ + if self._absolute_start is None: + for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values(): + line = line.decode("utf8") + absolute_start_12 = None + absolute_start_24 = None + # ND2s seem to randomly switch between 12- and 24-hour representations. + try: + absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S") + except (TypeError, ValueError): + pass + try: + absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p") + except (TypeError, ValueError): + pass + if not absolute_start_12 and not absolute_start_24: + continue + return absolute_start_12 if absolute_start_12 else absolute_start_24 + raise ValueError("This ND2 has no recorded start time. This is probably a bug.") + return self._absolute_start + + @property + def channels(self): + """ + These are labels created by the NIS Elements user. Typically they may a short description of the filter cube + used (e.g. "bright field", "GFP", etc.) + + :rtype: str + + """ + if not self._channels: + self._channels = [] + metadata = self.metadata[six.b('ImageMetadataSeq')][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')] + try: + validity = self.metadata[six.b('ImageMetadata')][six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')] + except KeyError: + # If none of the channels have been deleted, there is no validity list, so we just make one + validity = [True for _ in metadata] + # Channel information is contained in dictionaries with the keys a0, a1...an where the number + # indicates the order in which the channel is stored. So by sorting the dicts alphabetically + # we get the correct order. + for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity): + if not valid: + continue + self._channels.append(chan[six.b('sDescription')].decode("utf8")) + return self._channels + + @property + def fields_of_view(self): + """ + The metadata contains information about fields of view, but it contains it even if some fields + of view were cropped. We can't find anything that states which fields of view are actually + in the image data, so we have to calculate it. There probably is something somewhere, since + NIS Elements can figure it out, but we haven't found it yet. + + :rtype: int + + """ + if self._fields_of_view is None: + self._fields_of_view = self._parse_dimension_text(r""".*?XY\((\d+)\).*?""") + return self._fields_of_view + + @property + def time_indexes(self): + """ + The number of cycles. + + :rtype: int + + """ + if self._time_indexes is None: + self._time_indexes = self._parse_dimension_text(r""".*?T'\((\d+)\).*?""") + return self._time_indexes + + @property + def z_levels(self): + """ + The different levels in the Z-plane. Just a sequence from 0 to n. + + :rtype: int + + """ + if self._z_levels is None: + self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""") + return self._z_levels + @property def _file_handle(self): if self._fh is None: @@ -97,32 +191,6 @@ class Nd2Parser(object): raise ValueError("Could not parse metadata dimensions!") return self._dimension_text - @property - def channels(self): - """ - These are labels created by the NIS Elements user. Typically they may a short description of the filter cube - used (e.g. "bright field", "GFP", etc.) - - :rtype: str - - """ - if not self._channels: - self._channels = [] - metadata = self.metadata[six.b('ImageMetadataSeq')][six.b('SLxPictureMetadata')][six.b('sPicturePlanes')] - try: - validity = self.metadata[six.b('ImageMetadata')][six.b('SLxExperiment')][six.b('ppNextLevelEx')][six.b('')][0][six.b('ppNextLevelEx')][six.b('')][0][six.b('pItemValid')] - except KeyError: - # If none of the channels have been deleted, there is no validity list, so we just make one - validity = [True for _ in metadata] - # Channel information is contained in dictionaries with the keys a0, a1...an where the number - # indicates the order in which the channel is stored. So by sorting the dicts alphabetically - # we get the correct order. - for (label, chan), valid in zip(sorted(metadata[six.b('sPlaneNew')].items()), validity): - if not valid: - continue - self._channels.append(chan[six.b('sDescription')].decode("utf8")) - return self._channels - def _calculate_image_group_number(self, time_index, fov, z_level): """ Images are grouped together if they share the same time index, field of view, and z-level. @@ -150,32 +218,6 @@ class Nd2Parser(object): channel_offset[channel] = n return channel_offset - @property - def _absolute_start(self): - """ - The date and time when acquisition began. - - :rtype: datetime.datetime() - - """ - for line in self.metadata[six.b('ImageTextInfo')][six.b('SLxImageTextInfo')].values(): - line = line.decode("utf8") - absolute_start_12 = None - absolute_start_24 = None - # ND2s seem to randomly switch between 12- and 24-hour representations. - try: - absolute_start_24 = datetime.strptime(line, "%m/%d/%Y %H:%M:%S") - except (TypeError, ValueError): - pass - try: - absolute_start_12 = datetime.strptime(line, "%m/%d/%Y %I:%M:%S %p") - except (TypeError, ValueError): - pass - if not absolute_start_12 and not absolute_start_24: - continue - return absolute_start_12 if absolute_start_12 else absolute_start_24 - raise ValueError("This ND2 has no recorded start time. This is probably a bug.") - def _parse_dimension_text(self, pattern): try: count = int(re.match(pattern, self._dimensions).group(1)) @@ -188,48 +230,9 @@ class Nd2Parser(object): return list(range(count)) @property - def fields_of_view(self): - """ - The metadata contains information about fields of view, but it contains it even if some fields - of view were cropped. We can't find anything that states which fields of view are actually - in the image data, so we have to calculate it. There probably is something somewhere, since - NIS Elements can figure it out, but we haven't found it yet. - - :rtype: int - - """ - if self._fields_of_view is None: - self._fields_of_view = self._parse_dimension_text(r""".*?XY\((\d+)\).*?""") - return self._fields_of_view - - @property - def time_indexes(self): - """ - The number of cycles. - - :rtype: int - - """ - if self._time_indexes is None: - self._time_indexes = self._parse_dimension_text(r""".*?T'\((\d+)\).*?""") - return self._time_indexes - - @property - def z_levels(self): - """ - The different levels in the Z-plane. Just a sequence from 0 to n. - - :rtype: int - - """ - if self._z_levels is None: - self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""") - return self._z_levels - - @property - def _image_count(self): + def _total_images_per_channel(self): """ - The total number of images in the ND2. Warning: this may be inaccurate as it includes "gap" images. + The total number of images per channel. Warning: this may be inaccurate as it includes "gap" images. :rtype: int diff --git a/tests.py b/tests.py new file mode 100644 index 0000000..49b4387 --- /dev/null +++ b/tests.py @@ -0,0 +1,5 @@ +import unittest +loader = unittest.TestLoader() +tests = loader.discover('tests', pattern='*.py', top_level_dir='.') +testRunner = unittest.TextTestRunner() +testRunner.run(tests) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 From 07e2297aaf0a70b4556755703a64aaf0a83f1525 Mon Sep 17 00:00:00 2001 From: jim Date: Wed, 3 Jun 2015 13:32:01 -0500 Subject: [PATCH 2/6] #42 fixed dockerfile and makefile, got one passing test --- Dockerfile | 5 +---- Makefile | 4 ++++ nd2reader/__init__.py | 10 ---------- nd2reader/parser.py | 10 ++++++++++ tests/__init__.py | 19 +++++++++++++++++++ 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/Dockerfile b/Dockerfile index 0209f2a..8f656a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu +FROM ubuntu:15.04 MAINTAINER Jim Rybarski RUN mkdir -p /var/nds2 @@ -15,6 +15,3 @@ COPY . /opt/nd2reader WORKDIR /opt/nd2reader RUN python setup.py install RUN python3 setup.py install -WORKDIR /var/nd2s - -CMD /usr/bin/python3.4 \ No newline at end of file diff --git a/Makefile b/Makefile index 89b185e..87b2d38 100644 --- a/Makefile +++ b/Makefile @@ -8,3 +8,7 @@ py2shell: py3shell: docker run --rm -v ~/Documents/nd2s:/var/nd2s -it jimrybarski/nd2reader python3.4 + +test: build + docker run --rm -it jimrybarski/nd2reader python3.4 /opt/nd2reader/tests.py + diff --git a/nd2reader/__init__.py b/nd2reader/__init__.py index aa0d14f..f3f5d7b 100644 --- a/nd2reader/__init__.py +++ b/nd2reader/__init__.py @@ -123,16 +123,6 @@ class Nd2(Nd2Parser): """ return self.metadata[six.b('ImageAttributes')][six.b('SLxImageAttributes')][six.b('uiWidth')] - def _calculate_field_of_view(self, frame_number): - images_per_cycle = len(self.z_levels) * len(self.channels) - return int((frame_number - (frame_number % images_per_cycle)) / images_per_cycle) % len(self.fields_of_view) - - def _calculate_channel(self, frame_number): - return self._channels[frame_number % len(self.channels)] - - def _calculate_z_level(self, frame_number): - return self.z_levels[int(((frame_number - (frame_number % len(self.channels))) / len(self.channels)) % len(self.z_levels))] - def get_image(self, time_index, field_of_view, channel_name, z_level): """ Returns an Image if data exists for the given parameters, otherwise returns None. In general, you should avoid diff --git a/nd2reader/parser.py b/nd2reader/parser.py index 34acc24..f5bbda5 100644 --- a/nd2reader/parser.py +++ b/nd2reader/parser.py @@ -128,6 +128,16 @@ class Nd2Parser(object): self._z_levels = self._parse_dimension_text(r""".*?Z\((\d+)\).*?""") return self._z_levels + def _calculate_field_of_view(self, frame_number): + images_per_cycle = len(self.z_levels) * len(self.channels) + return int((frame_number - (frame_number % images_per_cycle)) / images_per_cycle) % len(self.fields_of_view) + + def _calculate_channel(self, frame_number): + return self._channels[frame_number % len(self.channels)] + + def _calculate_z_level(self, frame_number): + return self.z_levels[int(((frame_number - (frame_number % len(self.channels))) / len(self.channels)) % len(self.z_levels))] + @property def _file_handle(self): if self._fh is None: diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..531c513 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,19 @@ +from nd2reader.parser import Nd2Parser +import unittest + + +class MockNd2Parser(object): + def __init__(self, channels, fields_of_view, z_levels): + self.channels = channels + self.fields_of_view = fields_of_view + self.z_levels = z_levels + + +class TestNd2Parser(unittest.TestCase): + def test_calculate_field_of_view_simple(self): + """ With a single field of view, the field of view should always be the same number (0). """ + nd2 = MockNd2Parser([''], [0], [0]) + for frame_number in range(1000): + result = Nd2Parser._calculate_field_of_view(nd2, frame_number) + self.assertEqual(result, 0) + From 2b62ed4d2df554348119918da218d292abf006aa Mon Sep 17 00:00:00 2001 From: jim Date: Wed, 3 Jun 2015 13:48:26 -0500 Subject: [PATCH 3/6] #42 many passing tests for calculate_field_of_view --- tests/__init__.py | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index 531c513..a20d35a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -17,3 +17,67 @@ class TestNd2Parser(unittest.TestCase): result = Nd2Parser._calculate_field_of_view(nd2, frame_number) self.assertEqual(result, 0) + def test_calculate_field_of_view_two_channels(self): + nd2 = MockNd2Parser(['', 'GFP'], [0], [0]) + for frame_number in range(1000): + result = Nd2Parser._calculate_field_of_view(nd2, frame_number) + self.assertEqual(result, 0) + + def test_calculate_field_of_view_three_channels(self): + nd2 = MockNd2Parser(['', 'GFP', 'dsRed'], [0], [0]) + for frame_number in range(1000): + result = Nd2Parser._calculate_field_of_view(nd2, frame_number) + self.assertEqual(result, 0) + + def test_calculate_field_of_view_two_fovs(self): + nd2 = MockNd2Parser([''], [0, 1], [0]) + for frame_number in range(1000): + result = Nd2Parser._calculate_field_of_view(nd2, frame_number) + self.assertEqual(result, frame_number % 2) + + def test_calculate_field_of_view_two_fovs_two_zlevels(self): + nd2 = MockNd2Parser([''], [0, 1], [0, 1]) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 0), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 1), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 2), 1) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 3), 1) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 4), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 5), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 6), 1) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 7), 1) + + def test_calculate_field_of_view_two_everything(self): + nd2 = MockNd2Parser(['', 'GFP'], [0, 1], [0, 1]) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 0), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 1), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 2), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 3), 0) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 4), 1) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 5), 1) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 6), 1) + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, 7), 1) + + def test_calculate_field_of_view_7c2f2z(self): + nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0, 1], [0, 1]) + for i in range(14): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) + for i in range(14, 28): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) + for i in range(28, 42): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) + for i in range(42, 56): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) + for i in range(56, 70): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) + for i in range(70, 84): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) + + def test_calculate_channel_simple(self): + + + + + + + + From 047af59a1a59f3a3f064d63ab3e36266350b1408 Mon Sep 17 00:00:00 2001 From: jim Date: Wed, 3 Jun 2015 14:01:29 -0500 Subject: [PATCH 4/6] #42 many passing tests for calculate_channel --- nd2reader/parser.py | 2 +- tests/__init__.py | 42 +++++++++++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/nd2reader/parser.py b/nd2reader/parser.py index f5bbda5..139e2fb 100644 --- a/nd2reader/parser.py +++ b/nd2reader/parser.py @@ -133,7 +133,7 @@ class Nd2Parser(object): return int((frame_number - (frame_number % images_per_cycle)) / images_per_cycle) % len(self.fields_of_view) def _calculate_channel(self, frame_number): - return self._channels[frame_number % len(self.channels)] + return self.channels[frame_number % len(self.channels)] def _calculate_z_level(self, frame_number): return self.z_levels[int(((frame_number - (frame_number % len(self.channels))) / len(self.channels)) % len(self.z_levels))] diff --git a/tests/__init__.py b/tests/__init__.py index a20d35a..6edf425 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -72,12 +72,36 @@ class TestNd2Parser(unittest.TestCase): for i in range(70, 84): self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) - def test_calculate_channel_simple(self): - - - - - - - - + def test_calculate_field_of_view_2c3f5z(self): + """ All prime numbers to elucidate any errors that won't show up when numbers are multiples of each other """ + nd2 = MockNd2Parser(['', 'GFP'], [0, 1, 2], [0, 1, 2, 3, 4]) + for i in range(10): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) + for i in range(10, 20): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) + for i in range(20, 30): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) + for i in range(30, 40): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 0) + for i in range(40, 50): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 1) + for i in range(50, 60): + self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) + + def test_calculate_channel(self): + nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0], [0]) + for i in range(1000): + for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): + self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) + + def test_calculate_channel_7c2fov1z(self): + nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0, 1], [0]) + for i in range(1000): + for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): + self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) + + def test_calculate_channel_ludicrous_values(self): + nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], list(range(31)), list(range(17))) + for i in range(1000): + for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): + self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) \ No newline at end of file From 8ae2bf6d0688172c935159a60105e5060a8533cc Mon Sep 17 00:00:00 2001 From: jim Date: Wed, 3 Jun 2015 14:02:55 -0500 Subject: [PATCH 5/6] #42 added simple calculate_channel test --- tests/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index 6edf425..f24514a 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -88,6 +88,11 @@ class TestNd2Parser(unittest.TestCase): for i in range(50, 60): self.assertEqual(Nd2Parser._calculate_field_of_view(nd2, i), 2) + def test_calculate_channel_simple(self): + nd2 = MockNd2Parser(['GFP'], [0], [0]) + for i in range(1000): + self.assertEqual(Nd2Parser._calculate_channel(nd2, i), 'GFP') + def test_calculate_channel(self): nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], [0], [0]) for i in range(1000): @@ -104,4 +109,5 @@ class TestNd2Parser(unittest.TestCase): nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], list(range(31)), list(range(17))) for i in range(1000): for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): - self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) \ No newline at end of file + self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) + From c16dccd13abcd3f101803d48541f838c85584099 Mon Sep 17 00:00:00 2001 From: jim Date: Wed, 3 Jun 2015 14:15:29 -0500 Subject: [PATCH 6/6] #42 tests for calculate_z_level --- tests/__init__.py | 72 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/tests/__init__.py b/tests/__init__.py index f24514a..0a3def9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -107,7 +107,77 @@ class TestNd2Parser(unittest.TestCase): def test_calculate_channel_ludicrous_values(self): nd2 = MockNd2Parser(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], list(range(31)), list(range(17))) - for i in range(1000): + for i in range(10000): for n, channel in enumerate(['', 'GFP', 'dsRed', 'dTomato', 'lulzBlue', 'jimbotronPurple', 'orange'], start=i*7): self.assertEqual(Nd2Parser._calculate_channel(nd2, n), channel) + def test_calculate_z_level(self): + nd2 = MockNd2Parser([''], [0], [0]) + for frame_number in range(1000): + result = Nd2Parser._calculate_z_level(nd2, frame_number) + self.assertEqual(result, 0) + + def test_calculate_z_level_1c1f2z(self): + nd2 = MockNd2Parser([''], [0], [0, 1]) + for frame_number in range(1000): + result = Nd2Parser._calculate_z_level(nd2, frame_number) + self.assertEqual(result, frame_number % 2) + + def test_calculate_z_level_31c17f1z(self): + nd2 = MockNd2Parser(list(range(31)), list(range(17)), [0]) + for frame_number in range(1000): + result = Nd2Parser._calculate_z_level(nd2, frame_number) + self.assertEqual(result, 0) + + def test_calculate_z_level_2c1f2z(self): + nd2 = MockNd2Parser(['', 'GFP'], [0], [0, 1]) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 0), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 1), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 2), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 3), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 4), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 5), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 6), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 7), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 8), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 9), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 10), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 11), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 12), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 13), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 14), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 15), 1) + + def test_calculate_z_level_2c3f5z(self): + nd2 = MockNd2Parser(['', 'GFP'], [0, 1, 2], [0, 1, 2, 3, 4]) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 0), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 1), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 2), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 3), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 4), 2) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 5), 2) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 6), 3) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 7), 3) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 8), 4) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 9), 4) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 10), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 11), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 12), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 13), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 14), 2) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 15), 2) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 16), 3) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 17), 3) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 18), 4) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 19), 4) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 20), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 21), 0) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 22), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 23), 1) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 24), 2) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 25), 2) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 26), 3) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 27), 3) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 28), 4) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 29), 4) + self.assertEqual(Nd2Parser._calculate_z_level(nd2, 30), 0) \ No newline at end of file