diff --git a/transcoder/colours_test.py b/transcoder/colours_test.py index 0be88b2..1399a9c 100644 --- a/transcoder/colours_test.py +++ b/transcoder/colours_test.py @@ -7,7 +7,7 @@ HGRColours = colours.HGRColours class TestColours(unittest.TestCase): - def test_int28_to_pixels(self): + def test_int34_to_pixels(self): self.assertEqual( ( HGRColours.BLACK, @@ -38,9 +38,12 @@ class TestColours(unittest.TestCase): HGRColours.BLACK, HGRColours.BLACK, HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK ), colours.int34_to_nominal_colour_pixels( - 0b00000000000000000000111000000000, HGRColours + 0b00000000000000000000111000000000, HGRColours, init_phase=0 ) ) @@ -73,10 +76,13 @@ class TestColours(unittest.TestCase): HGRColours.BLACK, HGRColours.BLACK, HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, + HGRColours.BLACK, HGRColours.BLACK ), colours.int34_to_nominal_colour_pixels( - 0b0000111100001111000011110000, HGRColours + 0b0000111100001111000011110000, HGRColours, init_phase=0 ) ) diff --git a/transcoder/make_data_tables_test.py b/transcoder/make_data_tables_test.py index ab951e3..b8d18b1 100644 --- a/transcoder/make_data_tables_test.py +++ b/transcoder/make_data_tables_test.py @@ -9,42 +9,6 @@ class TestMakeDataTables(unittest.TestCase): pixels = (HGRColours.BLACK, HGRColours.WHITE, HGRColours.ORANGE) self.assertEqual("0FC", make_data_tables.pixel_string(pixels)) - def test_pixels_influenced_by_byte_index(self): - pixels = "CB00000" - self.assertEqual( - "CB", - make_data_tables.pixels_influenced_by_byte_index(pixels, 0) - ) - - pixels = "CBA9000" - self.assertEqual( - "BA9", - make_data_tables.pixels_influenced_by_byte_index(pixels, 1) - ) - - def test_map_to_mask32(self): - byte_mask32 = [ - # 33222222222211111111110000000000 <- bit pos in uint32 - # 10987654321098765432109876543210 - # 0000GGGGFFFFEEEEDDDDCCCCBBBBAAAA <- pixel A..G - # 3210321032103210321032103210 <- bit pos in A..G pixel - 0b00000000000000000000000011111111, # byte 0 influences A,B - 0b00000000000000001111111111110000, # byte 1 influences B,C,D - 0b00000000111111111111000000000000, # byte 2 influences D,E,F - 0b00001111111100000000000000000000, # byte 3 influences F,G - ] - int8_max = 2 ** 8 - 1 - int12_max = 2 ** 12 - 1 - - self.assertEqual( - make_data_tables.map_int8_to_mask32_0(int8_max), byte_mask32[0]) - self.assertEqual( - make_data_tables.map_int12_to_mask32_1(int12_max), byte_mask32[1]) - self.assertEqual( - make_data_tables.map_int12_to_mask32_2(int12_max), byte_mask32[2]) - self.assertEqual( - make_data_tables.map_int8_to_mask32_3(int8_max), byte_mask32[3]) - if __name__ == '__main__': unittest.main() diff --git a/transcoder/screen.py b/transcoder/screen.py index 63b6260..51de10e 100644 --- a/transcoder/screen.py +++ b/transcoder/screen.py @@ -3,14 +3,14 @@ import bz2 import functools import pickle -from typing import Union, List, Optional +from typing import Union, List, Optional, Tuple import numpy as np import palette as pal # Type annotation for cases where we may process either an int or a numpy array. -IntOrArray = Union[int, np.ndarray] +IntOrArray = Union[np.uint64, np.ndarray] def y_to_base_addr(y: int, page: int = 0) -> int: @@ -142,6 +142,8 @@ def _edit_distances(name: str, palette_id: pal.Palette) -> List[np.ndarray]: class Bitmap: """Packed 28-bit bitmap representation of (D)HGR screen memory. + XXX comments + The memory layout is still page-oriented, not linear y-x buffer but the bit map is such that 20 consecutive entries linearly encode the 28*20 = 560-bit monochrome dot positions that underlie both Mono and Colour ( @@ -150,6 +152,20 @@ class Bitmap: For Colour display the (nominal) colours are encoded as 4-bit pixels. """ + NAME = None # type: str + + # Size of packed representation + HEADER_BITS = None # type: np.uint64 + BODY_BITS = None # type: np.uint64 + FOOTER_BITS = None # type: np.uint64 + + BYTE_MASKS = None # type: List[np.uint64] + BYTE_SHIFTS = None # type: List[np.uint64] + + # How many bits of packed representation are influenced when storing a + # memory byte + MASKED_BITS = None # type: np.uint64 + def __init__( self, palette: pal.Palette, @@ -160,105 +176,367 @@ class Bitmap: self.main_memory = main_memory # type: MemoryMap self.aux_memory = aux_memory # type: Optional[MemoryMap] + self.PACKED_BITS = ( + self.HEADER_BITS + self.BODY_BITS + self.FOOTER_BITS + ) # type: np.uint64 + + # How many screen bytes we pack into a single scalar + self.SCREEN_BYTES = np.uint64(len(self.BYTE_MASKS)) # type: np.uint64 + self.packed = np.empty( shape=(32, 128), dtype=np.uint64) # type: np.ndarray self._pack() - def _pack(self) -> None: - """Pack MemoryMap into 34-bit representation.""" + def _body(self) -> np.ndarray: raise NotImplementedError - NAME = None + # TODO: don't leak headers/footers across screen rows. We should be using + # x-y representation rather than page-offset + + @staticmethod + def _make_header(prev_col: IntOrArray) -> IntOrArray: + raise NotImplementedError + + @staticmethod + def _make_footer(next_col: IntOrArray) -> IntOrArray: + raise NotImplementedError + + def _pack(self) -> None: + """Pack MemoryMap into efficient representation for diffing.""" + + body = self._body() + + # XXX comments + # Prepend last 3 bits of previous main odd byte so we can correctly + # decode the effective colours at the beginning of the 28-bit + # tuple + prev_col = np.roll(body, 1, axis=1).astype(np.uint64) + header = self._make_header(prev_col) + # Don't leak header across page boundaries + header[:, 0] = 0 + + # Append first 3 bits of next aux even byte so we can correctly + # decode the effective colours at the end of the 28-bit tuple + next_col = np.roll(body, -1, axis=1).astype(np.uint64) + footer = self._make_footer(next_col) + # Don't leak footer across page boundaries + footer[:, -1] = 0 + + self.packed = header ^ body ^ footer + + @staticmethod + def masked_update( + byte_offset: int, + old_value: IntOrArray, + new_value: np.uint8) -> IntOrArray: + raise NotImplementedError + + @staticmethod + @functools.lru_cache(None) + def byte_offset(x_byte: int, is_aux: bool) -> int: + raise NotImplementedError + + @staticmethod + @functools.lru_cache(None) + def _byte_offsets(is_aux: bool) -> Tuple[int, int]: + raise NotImplementedError + + def apply( + self, + page: int, + offset: int, + is_aux: bool, + value: np.uint8) -> None: + """Update packed representation of changing main/aux memory.""" + + byte_offset = self.byte_offset(offset, is_aux) + packed_offset = offset // 2 + + self.packed[page, packed_offset] = self.masked_update( + byte_offset, self.packed[page, packed_offset], np.uint64(value)) + self._fix_scalar_neighbours(page, packed_offset, byte_offset) + + def _fix_scalar_neighbours( + self, + page: int, + offset: int, + byte_offset: int) -> None: + + if byte_offset == 0 and offset > 0: + self.packed[page, offset - 1] = self._fix_column_left( + self.packed[page, offset - 1], + self.packed[page, offset] + ) + + # # Need to also update the 3-bit footer of the preceding column + # self.packed[page, packed_offset - 1] &= np.uint64(2 ** 31 - 1) + # + # self.packed[page, packed_offset - 1] ^= ( + # (self.packed[page, packed_offset] & np.uint64(0b111 << 3)) + # << np.uint64(28) + # ) + elif byte_offset == (self.SCREEN_BYTES - 1) and offset < 127: + # Need to also update the 3-bit header of the next column + self.packed[page, offset + 1] = self._fix_column_right( + self.packed[page, offset + 1], + self.packed[page, offset] + ) + + # self.packed[page, offset + 1] &= np.uint64( + # (2 ** 31 - 1) << 3) + # + # self.packed[page, offset + 1] ^= ( + # (self.packed[page, offset] & np.uint64(0b111 << 28)) + # >> np.uint64(28) + # ) + + def _fix_column_left( + self, + column_left: IntOrArray, + column: IntOrArray + ) -> IntOrArray: + # Mask out footer(s) + column_left &= np.uint64(2 ** (self.HEADER_BITS + self.BODY_BITS) - 1) + column_left ^= self._make_footer(column) + + return column_left + + def _fix_column_right( + self, + column_right: IntOrArray, + column: IntOrArray + ) -> IntOrArray: + # Mask out header(s) + column_right &= np.uint64( + (2 ** (self.BODY_BITS + self.FOOTER_BITS) - 1)) << self.HEADER_BITS + column_right ^= self._make_header(column) + + return column_right + + def _fix_array_neighbours( + self, + ary: np.ndarray, + byte_offset: int + ) -> None: + # Propagate new value into neighbouring byte headers/footers if + # necessary + if byte_offset == 0: + # Need to also update the 3-bit footer of the preceding column + shifted_left = np.roll(ary, -1, axis=1) + self._fix_column_left(ary, shifted_left) + # + # # Mask out all footers + # ary &= np.uint64(2 ** 31 - 1) + # + # shifted_left = np.roll(ary, -1, axis=1) + # ary ^= self._make_footer(shifted_left) + # new ^= (shifted & np.uint64(0b111 << 3)) << np.uint64(28) + + elif byte_offset == 3: + # Need to also update the 3-bit header of the next column + shifted_right = np.roll(ary, 1, axis=1) + self._fix_column_right(ary, shifted_right) + # + # # Mask out all headers + # ary &= np.uint64((2 ** 31 - 1) << 3) + # + # shifted_right = np.roll(ary, 1, axis=1) + # ary ^= self._make_header(shifted_right) + # # new ^= (shifted & np.uint64(0b111 << 28)) >> np.uint64(28) @functools.lru_cache(None) def edit_distances(self, palette_id: pal.Palette) -> List[np.ndarray]: """Load edit distance matrices for masked, shifted byte values.""" return _edit_distances(self.NAME, palette_id) - def apply( + def mask_and_shift_data( self, - page: int, - offset: np.uint8, - is_aux: bool, - value: np.uint8) -> None: - raise NotImplementedError + data: IntOrArray, + byte_offset: int) -> IntOrArray: + """Masks and shifts data into the MASKED_BITS range.""" + res = (data & self.BYTE_MASKS[byte_offset]) >> ( + self.BYTE_SHIFTS[byte_offset]) + assert np.all(res <= 2 ** self.MASKED_BITS) + return res + # TODO: unit tests @functools.lru_cache(None) def byte_pair_difference( self, byte_offset: int, - old_packed: int, - content: int - ) -> int: - raise NotImplementedError + old_packed: np.uint64, + content: np.uint8 + ) -> np.uint16: + old_pixels = self.mask_and_shift_data( + old_packed, byte_offset) + new_pixels = self.mask_and_shift_data( + self.masked_update(byte_offset, old_packed, content), byte_offset) + + pair = (old_pixels << self.MASKED_BITS) + new_pixels + + return self.edit_distances(self.palette)[byte_offset][pair] def diff_weights( self, - other: "DHGRBitmap", + source: "Bitmap", is_aux: bool ) -> np.ndarray: - raise NotImplementedError + return self._diff_weights(source.packed, is_aux) + def _diff_weights( + self, + source_packed: np.ndarray, + is_aux: bool, + content: np.uint8 = None + ) -> np.ndarray: + """Computes diff from source_packed to self.packed""" + diff = np.ndarray((32, 256), dtype=np.int) + + offsets = self._byte_offsets(is_aux) + + dists = [] + for o in offsets: + if content is not None: + source_packed = self.masked_update(o, source_packed, content) + self._fix_array_neighbours(source_packed, o) + + # Pixels influenced by byte offset o + source_pixels = self.mask_and_shift_data(source_packed, o) + target_pixels = self.mask_and_shift_data(self.packed, o) + + # Concatenate 13-bit source and target into 26-bit values + pair = (source_pixels << self.MASKED_BITS) + target_pixels + dist = self.edit_distances(self.palette)[o][pair].reshape( + pair.shape) + dists.append(dist) + + diff[:, 0::2] = dists[0] + diff[:, 1::2] = dists[1] + + return diff + + # TODO: unit tests def compute_delta( self, content: int, old: np.ndarray, is_aux: bool ) -> np.ndarray: - raise NotImplementedError + # TODO: use error edit distance + + # XXX reuse code + # + # diff = np.ndarray((32, 256), dtype=np.int) + # + # if is_aux: + # offsets = [0, 2] + # else: + # offsets = [1, 3] + # + # # TODO: extract into parent class + # dists = [] + # for o in offsets: + # # Pixels influenced by byte offset o + # source_pixels = self.mask_and_shift_data( + # self.masked_update_array(0, self.packed, content), o) + # target_pixels = self.mask_and_shift_data(self.packed, o) + # + # # Concatenate 13-bit source and target into 26-bit values + # pair = (source_pixels << np.uint64(self.MASKED_BITS)) + target_pixels + # dist = self.edit_distances(self.palette)[o][pair].reshape( + # pair.shape) + # dists.append(dist) + # + # diff[:, 0::2] = dists[0] + # diff[:, 1::2] = dists[1] + + diff = self._diff_weights(self.packed, is_aux, content) + + # + # + # if is_aux: + # # Pixels influenced by byte offset 0 + # source_pixels0 = self.mask_and_shift_data( + # self.masked_update_array(0, self.packed, content), 0) + # target_pixels0 = self.mask_and_shift_data(self.packed, 0) + # + # # Concatenate 13-bit source and target into 26-bit values + # pair0 = (source_pixels0 << np.uint64(13)) + target_pixels0 + # dist0 = self.edit_distances(self.palette)[0][pair0].reshape( + # pair0.shape) + # + # # Pixels influenced by byte offset 2 + # source_pixels2 = self.mask_and_shift_data( + # self.masked_update_array(2, self.packed, content), 2) + # target_pixels2 = self.mask_and_shift_data(self.packed, 2) + # # Concatenate 13-bit source and target into 26-bit values + # pair2 = (source_pixels2 << np.uint64(13)) + target_pixels2 + # dist2 = self.edit_distances(self.palette)[2][pair2].reshape( + # pair2.shape) + # + # diff[:, 0::2] = dist0 + # diff[:, 1::2] = dist2 + # + # else: + # # Pixels influenced by byte offset 1 + # source_pixels1 = self.mask_and_shift_data( + # self.masked_update_array(1, self.packed, content), 1) + # target_pixels1 = self.mask_and_shift_data(self.packed, 1) + # pair1 = (source_pixels1 << np.uint64(13)) + target_pixels1 + # dist1 = self.edit_distances(self.palette)[1][pair1].reshape( + # pair1.shape) + # + # # Pixels influenced by byte offset 3 + # source_pixels3 = self.mask_and_shift_data( + # self.masked_update_array(3, self.packed, content), 3) + # target_pixels3 = self.mask_and_shift_data(self.packed, 3) + # pair3 = (source_pixels3 << np.uint64(13)) + target_pixels3 + # dist3 = self.edit_distances(self.palette)[3][pair3].reshape( + # pair3.shape) + # + # diff[:, 0::2] = dist1 + # diff[:, 1::2] = dist3 + + # TODO: try different weightings + return (diff * 5) - old class HGRBitmap(Bitmap): - BYTE_MASK16 = [ - # 11111110000000 <- byte 0, 1 - # 1111110000000000 - # 5432109876543210 - # 00GGFFEEDDCCBBAA <- pixel A..G - 0b0000000011111111, - 0b0011111111000000 - ] - - # Representation - # - # 1111110000000000 - # 5432109876543210 - # PGGFFEEDPDCCBBAA - # - # Where palette bit influences all of the pixels in the byte - # - # Map to 3-bit pixels, i.e. 21-bit quantity - # - # 222211111111110000000000 - # 321098765432109876543210 - # 000PGGPFFPEEPDDPCCPBBPAA - BYTE_MASK32 = [ - 0b000000000000111111111111, - 0b000111111111111000000000 - ] - - # XXX 3-bit pixel isn't quite correct, e.g. the case of conflicting - # palette bits across byte boundary - # Also hard to interleave the palette bit in multiple places - could use - # a mapping array but maybe don't need to, can just use 8-bit values as is? - # But need contiguous representation for edit distance tables - - # P - # (0)00 --> 0.0. - # (0)01 --> 0.1. - # - # (1)01 --> .0.1 - # (1)11 --> .1.1 - # etc - - # - - BYTE_SHIFTS = [0, 9] - NAME = 'HGR' + # hhhbbbbbbbpPBBBBBBBfff + # 0000000011111111111111 + # 1111111111111100000000 + + # Header: + # 0000000010000011 + # Footer: + # 1100000100000000 + + BYTE_MASKS = [ + np.uint64(0b0000000011111111111111), + np.uint64(0b1111111111111100000000) + ] + BYTE_SHIFTS = [np.uint64(0), np.uint64(8)] + MASKED_BITS = np.uint64(14) # 3 + 8 + 3 + + HEADER_BITS = np.uint64(3) + BODY_BITS = np.uint64(16) # 8 + 8 + FOOTER_BITS = np.uint64(3) + def __init__(self, palette: pal.Palette, main_memory: MemoryMap): super(HGRBitmap, self).__init__(palette, main_memory, None) + def _make_header(self, prev_col: IntOrArray) -> IntOrArray: + raise NotImplementedError + + def _body(self) -> np.ndarray: + raise NotImplementedError + + def _make_footer(self, next_col: IntOrArray) -> IntOrArray: + raise NotImplementedError + + # XXX move to make_data_tables def _pack(self) -> None: """Pack main memory into (28+3)-bit uint64 array""" @@ -356,40 +634,37 @@ class HGRBitmap(Bitmap): @staticmethod @functools.lru_cache(None) - def byte_offset(x_byte: int) -> int: - """Returns 0..1 offset in ByteTuple for a given x_byte,""" + def byte_offset(x_byte: int, is_aux: bool) -> int: + """Returns 0..1 offset in packed representation for a given x_byte.""" + assert not is_aux + is_odd = x_byte % 2 == 1 return 1 if is_odd else 0 + @staticmethod + @functools.lru_cache(None) + def _byte_offsets(is_aux: bool) -> Tuple[int, int]: + assert not is_aux + return 0, 1 + + # XXX test @staticmethod def masked_update( byte_offset: int, old_value: IntOrArray, - new_value: int) -> IntOrArray: - raise NotImplementedError + new_value: np.uint8) -> IntOrArray: + """Update int/array to store new value at byte_offset in every entry. - def apply(self, page: int, offset: int, is_aux: bool, value: int) -> None: - """Update packed representation of changing main/aux memory.""" + Does not patch up headers/footers of neighbouring columns. + """ - assert not is_aux + # Mask out 8-bit value where update will go + masked_value = old_value & ( + ~np.uint64(0xff << (8 * byte_offset + 3))) - # XXX fix - - byte_offset = self.byte_offset(offset) - packed_offset = offset // 2 - - self.packed[page, packed_offset] = self.masked_update( - byte_offset, self.packed[page, packed_offset], value) - - # XXXX Generic? - def mask_and_shift_data( - self, - data: IntOrArray, - byte_offset: int) -> IntOrArray: - """Masks and shifts data into the 8 or 12-bit range.""" - return (data & self.BYTE_MASK32[byte_offset]) >> ( - self.BYTE_SHIFTS[byte_offset]) + update = new_value << np.uint64(8 * byte_offset + 3) + return masked_value ^ update class DHGRBitmap(Bitmap): @@ -397,8 +672,10 @@ class DHGRBitmap(Bitmap): # for why we have to cast things explicitly to np.uint64 - type promotion # to uint64 is broken in numpy :( - # 3-bit header + 28-bit body + 3-bit trailer - BYTE_MASK34 = [ + NAME = 'DHGR' + + # 3-bit header + 28-bit body + 3-bit footer + BYTE_MASKS = [ # 3333333222222211111110000000 <- byte 0.3 # # 3333222222222211111111110000000000 <- bit pos in uint64 @@ -414,51 +691,46 @@ class DHGRBitmap(Bitmap): # How much to right-shift bits after masking to bring into int13 range BYTE_SHIFTS = [np.uint64(0), np.uint64(7), np.uint64(14), np.uint64(21)] - NAME = 'DHGR' + HEADER_BITS = np.uint64(3) + BODY_BITS = np.uint64(28) + FOOTER_BITS = np.uint64(3) - def _pack(self) -> None: - """Interleave and pack aux and main memory into 34-bit uint64 array""" + MASKED_BITS = np.uint64(13) + def _body(self) -> np.ndarray: # Palette bit is unused for DHGR so mask it out aux = (self.aux_memory.page_offset & 0x7f).astype(np.uint64) main = (self.main_memory.page_offset & 0x7f).astype(np.uint64) + # XXX update # Interleave aux and main memory columns and pack 7-bit masked values - # into a 28-bit value, with 3-bit header and trailer. This + # into a 28-bit value, with 3-bit header and footer. This # sequentially encodes 7 4-bit DHGR pixels, together with the # neighbouring 3 bits that are necessary to decode artifact colours. # # See make_data_tables.py for more discussion about this representation. - packed = ( + + return ( (aux[:, 0::2] << 3) + (main[:, 0::2] << 10) + (aux[:, 1::2] << 17) + (main[:, 1::2] << 24) ) - # Prepend last 3 bits of previous main odd byte so we can correctly - # decode the effective colours at the beginning of the 28-bit - # tuple - prevcol = np.roll(packed, 1, axis=1).astype(np.uint64) + @staticmethod + def _make_header(col: IntOrArray) -> IntOrArray: + """Extract upper 3 bits of body for header of next column.""" + return (col & np.uint64(0b111 << 28)) >> np.uint64(28) - # Append first 3 bits of next aux even byte so we can correctly - # decode the effective colours at the end of the 28-bit tuple - nextcol = np.roll(packed, -1, axis=1).astype(np.uint64) - - self.packed = np.bitwise_xor( - np.bitwise_xor( - packed, - # Prepend last 3 bits of 28-bit body from previous column - (prevcol & (0b111 << 28)) >> 28 - ), - # Append first 3 bits of 28-bit body from next column - (nextcol & (0b111 << 3)) << 28 - ) + @staticmethod + def _make_footer(col: IntOrArray) -> IntOrArray: + """Extract lower 3 bits of body for footer of previous column.""" + return (col & np.uint64(0b111 << 3)) << np.uint64(28) @staticmethod @functools.lru_cache(None) - def interleaved_byte_offset(x_byte: int, is_aux: bool) -> int: - """Returns 0..3 offset in ByteTuple for a given x_byte and is_aux""" + def byte_offset(x_byte: int, is_aux: bool) -> int: + """Returns 0..3 packed byte offset for a given x_byte and is_aux""" is_odd = x_byte % 2 == 1 if is_aux: if is_odd: @@ -470,207 +742,48 @@ class DHGRBitmap(Bitmap): else: return 1 + @staticmethod + @functools.lru_cache(None) + def _byte_offsets(is_aux: bool) -> Tuple[int, int]: + if is_aux: + offsets = (0, 2) + else: + offsets = (1, 3) + + return offsets + + # + # # XXX test + # @staticmethod + # def masked_update_scalar( + # byte_offset: int, + # old_value: np.uint64, + # new_value: np.uint8) -> np.uint64: + # # Mask out 7-bit value where update will go + # masked_value = old_value & ( + # ~np.uint64(0x7f << (7 * byte_offset + 3))) + # + # update = (new_value & np.uint64(0x7f)) << np.uint64( + # 7 * byte_offset + 3) + # + # new = masked_value ^ update + # return new + # XXX test @staticmethod - def masked_update_scalar( + def masked_update( byte_offset: int, - old_value: np.uint64, - new_value: np.uint8) -> np.uint64: + old_value: IntOrArray, + new_value: np.uint8) -> IntOrArray: + """Update int/array to store new value at byte_offset in every entry. + + Does not patch up headers/footers of neighbouring columns. + """ + # Mask out 7-bit value where update will go masked_value = old_value & ( ~np.uint64(0x7f << (7 * byte_offset + 3))) update = (new_value & np.uint64(0x7f)) << np.uint64( 7 * byte_offset + 3) - - new = masked_value ^ update - return new - - # XXX test - @staticmethod - def masked_update_array( - byte_offset: int, - old_value: np.ndarray, - new_value: int) -> np.ndarray: - # Mask out 7-bit value where update will go - masked_value = old_value & ( - ~np.uint64(0x7f << (7 * byte_offset + 3))) - - update = (new_value & np.uint64(0x7f)) << np.uint64(7 * byte_offset + 3) - - new = masked_value ^ update - - # TODO: don't leak headers across screen rows. - - if byte_offset == 0: - # Need to also update the 3-bit trailer of the preceding column - - shifted = np.roll(new, -1, axis=1) - - new &= np.uint64(2 ** 31 - 1) - new ^= (shifted & np.uint64(0b111 << 3)) << np.uint64(28) - elif byte_offset == 3: - # Need to also update the 3-bit header of the next column - - shifted = np.roll(new, 1, axis=1) - - new &= np.uint64((2 ** 31 - 1) << 3) - new ^= (shifted & np.uint64(0b111 << 28)) >> np.uint64(28) - return new - - # XXX test - def apply( - self, - page: int, - offset: int, - is_aux: bool, - value: np.uint8) -> None: - """Update packed representation of changing main/aux memory.""" - - byte_offset = self.interleaved_byte_offset(offset, is_aux) - packed_offset = offset // 2 - - self.packed[page, packed_offset] = self.masked_update_scalar( - byte_offset, self.packed[page, packed_offset], value) - - # TODO: don't leak headers/trailers across screen rows. - if byte_offset == 0 and packed_offset > 0: - # Need to also update the 3-bit trailer of the preceding column - self.packed[page, packed_offset - 1] &= np.uint64(2 ** 31 - 1) - - self.packed[page, packed_offset - 1] ^= ( - (self.packed[page, packed_offset] & np.uint64(0b111 << 3)) - << np.uint64(28) - ) - elif byte_offset == 3 and packed_offset < 127: - # Need to also update the 3-bit header of the next column - self.packed[page, packed_offset + 1] &= np.uint64( - (2 ** 31 - 1) << 3) - - self.packed[page, packed_offset + 1] ^= ( - (self.packed[page, packed_offset] & np.uint64(0b111 << 28)) - >> np.uint64(28) - ) - - def mask_and_shift_data( - self, - data: IntOrArray, - byte_offset: int) -> IntOrArray: - """Masks and shifts data into the 13-bit range.""" - res = (data & self.BYTE_MASK34[byte_offset]) >> ( - self.BYTE_SHIFTS[byte_offset]) - assert np.all(res <= 2 ** 13) - return res - - @functools.lru_cache(None) - def byte_pair_difference( - self, - byte_offset: int, - old_packed: np.uint64, - content: np.uint8 - ) -> int: - - old_pixels = self.mask_and_shift_data( - old_packed, byte_offset) - new_pixels = self.mask_and_shift_data( - self.masked_update_scalar( - byte_offset, old_packed, content), byte_offset) - - pair = (old_pixels << np.uint64(13)) + new_pixels - - return self.edit_distances(self.palette)[byte_offset][pair] - - def diff_weights( - self, - source: "DHGRBitmap", - is_aux: bool - ) -> np.ndarray: - return self._diff_weights(source.packed, is_aux) - - def _diff_weights( - self, - source_packed: np.ndarray, - is_aux: bool - ) -> np.ndarray: - """Computes diff from source_packed to self.packed""" - diff = np.ndarray((32, 256), dtype=np.int) - - if is_aux: - offsets = [0, 2] - else: - offsets = [1, 3] - - dists = [] - for o in offsets: - # Pixels influenced by byte offset o - source_pixels = self.mask_and_shift_data(source_packed, o) - target_pixels = self.mask_and_shift_data(self.packed, o) - - # Concatenate 13-bit source and target into 26-bit values - pair = (source_pixels << np.uint64(13)) + target_pixels - dist = self.edit_distances(self.palette)[o][pair].reshape( - pair.shape) - dists.append(dist) - - diff[:, 0::2] = dists[0] - diff[:, 1::2] = dists[1] - - return diff - - def compute_delta( - self, - content: int, - old: np.ndarray, - is_aux: bool - ) -> np.ndarray: - # TODO: use error edit distance - - # XXX reuse code - - diff = np.ndarray((32, 256), dtype=np.int) - - if is_aux: - # Pixels influenced by byte offset 0 - source_pixels0 = self.mask_and_shift_data( - self.masked_update_array(0, self.packed, content), 0) - target_pixels0 = self.mask_and_shift_data(self.packed, 0) - - # Concatenate 13-bit source and target into 26-bit values - pair0 = (source_pixels0 << np.uint64(13)) + target_pixels0 - dist0 = self.edit_distances(self.palette)[0][pair0].reshape( - pair0.shape) - - # Pixels influenced by byte offset 2 - source_pixels2 = self.mask_and_shift_data( - self.masked_update_array(2, self.packed, content), 2) - target_pixels2 = self.mask_and_shift_data(self.packed, 2) - # Concatenate 13-bit source and target into 26-bit values - pair2 = (source_pixels2 << np.uint64(13)) + target_pixels2 - dist2 = self.edit_distances(self.palette)[2][pair2].reshape( - pair2.shape) - - diff[:, 0::2] = dist0 - diff[:, 1::2] = dist2 - - else: - # Pixels influenced by byte offset 1 - source_pixels1 = self.mask_and_shift_data( - self.masked_update_array(1, self.packed, content), 1) - target_pixels1 = self.mask_and_shift_data(self.packed, 1) - pair1 = (source_pixels1 << np.uint64(13)) + target_pixels1 - dist1 = self.edit_distances(self.palette)[1][pair1].reshape( - pair1.shape) - - # Pixels influenced by byte offset 3 - source_pixels3 = self.mask_and_shift_data( - self.masked_update_array(3, self.packed, content), 3) - target_pixels3 = self.mask_and_shift_data(self.packed, 3) - pair3 = (source_pixels3 << np.uint64(13)) + target_pixels3 - dist3 = self.edit_distances(self.palette)[3][pair3].reshape( - pair3.shape) - - diff[:, 0::2] = dist1 - diff[:, 1::2] = dist3 - - # TODO: try different weightings - return (diff * 5) - old + return masked_value ^ update diff --git a/transcoder/screen_test.py b/transcoder/screen_test.py index 20bdff7..8de9f03 100644 --- a/transcoder/screen_test.py +++ b/transcoder/screen_test.py @@ -3,9 +3,12 @@ import unittest import numpy as np +from etaprogress.progress import ProgressBar import colours +from palette import Palette, PALETTES import screen +import sys class TestDHGRBitmap(unittest.TestCase): @@ -13,7 +16,65 @@ class TestDHGRBitmap(unittest.TestCase): self.aux = screen.MemoryMap(screen_page=1) self.main = screen.MemoryMap(screen_page=1) - def test_pixel_packing(self): + def test_edit_distances(self): + for p in PALETTES: + ed = screen._edit_distances("DHGR", p) + print(p) + + bar = ProgressBar((2 ** 13 * (2 ** 13 - 1)) / 2, max_width=80) + + cnt = 0 + for i in range(2 ** 13): + # Assert that self-distances are zero + + self.assertEqual(0, ed[0][(i << 13) + i]) + self.assertEqual(0, ed[1][(i << 13) + i]) + self.assertEqual(0, ed[2][(i << 13) + i]) + self.assertEqual(0, ed[3][(i << 13) + i]) + + # Assert that matrix is triangular + + for j in range(i): + + cnt += 1 + + if cnt % 10000 == 0: + bar.numerator = cnt + print(bar, end='\r') + sys.stdout.flush() + + self.assertEqual( + ed[0][(i << 13) + j], + ed[0][(j << 13) + i], + ) + self.assertEqual( + ed[1][(i << 13) + j], + ed[1][(j << 13) + i], + ) + self.assertEqual( + ed[2][(i << 13) + j], + ed[2][(j << 13) + i], + ) + self.assertEqual( + ed[3][(i << 13) + j], + ed[3][(j << 13) + i], + ) + + def test_make_header(self): + self.assertEqual( + 0b100, + screen.DHGRBitmap._make_header( + np.uint64(0b0001000011111010110000111110101000)) + ) + + def test_make_footer(self): + self.assertEqual( + 0b1010000000000000000000000000000000, + screen.DHGRBitmap._make_footer( + np.uint64(0b0001000011111010110000111110101000)) + ) + + def test_pixel_packing_offset_0(self): # PBBBAAAA self.aux.page_offset[0, 0] = 0b11110101 # PDDCCCCB @@ -24,571 +85,697 @@ class TestDHGRBitmap(unittest.TestCase): self.main.page_offset[0, 1] = 0b01000011 dhgr = screen.DHGRBitmap( - main_memory=self.main, aux_memory=self.aux) + main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) self.assertEqual( - 0b1000011111010110000111110101, + 0b0001000011111010110000111110101000, dhgr.packed[0, 0] ) - def test_interleaved_byte_offset(self): + # Check header on neighbouring byte self.assertEqual( - 0, - screen.DHGRBitmap.interleaved_byte_offset(0, is_aux=True) - ) - self.assertEqual( - 1, - screen.DHGRBitmap.interleaved_byte_offset(0, is_aux=False) - ) - self.assertEqual( - 2, - screen.DHGRBitmap.interleaved_byte_offset(1, is_aux=True) - ) - self.assertEqual( - 3, - screen.DHGRBitmap.interleaved_byte_offset(1, is_aux=False) + 0b0000000000000000000000000000000100, + dhgr.packed[0, 1] ) - def test_mask_and_shift_data(self): - int8_max = 2 ** 8 - 1 - int12_max = 2 ** 12 - 1 - int32_max = 2 ** 32 - 1 + # No other entries should be set, in particular no footer since we + # are at packed offset 0 + self.assertEqual(2, np.count_nonzero(dhgr.packed)) + + def test_pixel_packing_offset_1(self): + # PBBBAAAA + self.aux.page_offset[0, 2] = 0b11110101 + # PDDCCCCB + self.main.page_offset[0, 2] = 0b01000011 + # PFEEEEDD + self.aux.page_offset[0, 3] = 0b11110101 + # PGGGGFFF + self.main.page_offset[0, 3] = 0b01000011 dhgr = screen.DHGRBitmap( - main_memory=self.main, aux_memory=self.aux) + main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) self.assertEqual( - int8_max, - dhgr.mask_and_shift_data( - screen.DHGRBitmap.BYTE_MASK32[0], 0 - ) - ) - self.assertEqual( - int12_max, - dhgr.mask_and_shift_data( - screen.DHGRBitmap.BYTE_MASK32[1], 1 - ) - ) - self.assertEqual( - int12_max, - dhgr.mask_and_shift_data( - screen.DHGRBitmap.BYTE_MASK32[2], 2 - ) - ) - self.assertEqual( - int8_max, - dhgr.mask_and_shift_data( - screen.DHGRBitmap.BYTE_MASK32[3], 3 - ) + 0b0001000011111010110000111110101000, + dhgr.packed[0, 1] ) - # Now check complement, i.e. no bits taken from outside expected range + # Check footer on neighbouring byte + self.assertEqual( + 0b1010000000000000000000000000000000, + dhgr.packed[0, 0] + ) + + # Check header on neighbouring byte + self.assertEqual( + 0b0000000000000000000000000000000100, + dhgr.packed[0, 2] + ) + + # No other entries should be set + self.assertEqual(3, np.count_nonzero(dhgr.packed)) + + def test_pixel_packing_offset_127(self): + # PBBBAAAA + self.aux.page_offset[0, 254] = 0b11110101 + # PDDCCCCB + self.main.page_offset[0, 254] = 0b01000011 + # PFEEEEDD + self.aux.page_offset[0, 255] = 0b11110101 + # PGGGGFFF + self.main.page_offset[0, 255] = 0b01000011 + + dhgr = screen.DHGRBitmap( + main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) self.assertEqual( - 0, - dhgr.mask_and_shift_data( - ~screen.DHGRBitmap.BYTE_MASK32[0] & int32_max, 0 - ) + 0b0001000011111010110000111110101000, + dhgr.packed[0, 127] ) + + # Check footer on neighbouring byte self.assertEqual( - 0, - dhgr.mask_and_shift_data( - ~screen.DHGRBitmap.BYTE_MASK32[1] & int32_max, 1 - ) + 0b1010000000000000000000000000000000, + dhgr.packed[0, 126] ) - self.assertEqual( - 0, - dhgr.mask_and_shift_data( - ~screen.DHGRBitmap.BYTE_MASK32[2] & int32_max, 2 + + # No other entries should be set, in particular header should not + # propagate to next row + self.assertEqual(2, np.count_nonzero(dhgr.packed)) + + def test_byte_offset(self): + self.assertEqual(0, screen.DHGRBitmap.byte_offset(0, is_aux=True)) + self.assertEqual(1, screen.DHGRBitmap.byte_offset(0, is_aux=False)) + self.assertEqual(2, screen.DHGRBitmap.byte_offset(1, is_aux=True)) + self.assertEqual(3, screen.DHGRBitmap.byte_offset(1, is_aux=False)) + + def test_byte_offsets(self): + self.assertEqual((0, 2), screen.DHGRBitmap._byte_offsets(is_aux=True)) + self.assertEqual((1, 3), screen.DHGRBitmap._byte_offsets(is_aux=False)) + + def test_mask_and_shift_data(self): + int13_max = np.uint64(2 ** 13 - 1) + int34_max = np.uint64(2 ** 34 - 1) + + dhgr = screen.DHGRBitmap( + main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) + + for o in range(3): + self.assertEqual( + int13_max, + dhgr.mask_and_shift_data( + screen.DHGRBitmap.BYTE_MASKS[o], o + ) ) - ) - self.assertEqual( - 0, - dhgr.mask_and_shift_data( - ~screen.DHGRBitmap.BYTE_MASK32[3] & int32_max, 3 + + # Now check complement, i.e. no bits taken from outside expected + # range + self.assertEqual( + 0, + dhgr.mask_and_shift_data( + ~screen.DHGRBitmap.BYTE_MASKS[o] & int34_max, o + ) ) - ) def test_masked_update(self): self.assertEqual( - 0b0000000000000000000001111111, - screen.DHGRBitmap.masked_update(0, 0x00000000, 0xff) + 0b0000000000000000000000001111111000, + screen.DHGRBitmap.masked_update( + 0, np.uint64(0), np.uint8(0xff)) ) self.assertEqual( - 0b0000000000000011111110000000, - screen.DHGRBitmap.masked_update(1, 0x00000000, 0xff) + 0b0000000000000000011111110000000000, + screen.DHGRBitmap.masked_update( + 1, np.uint64(0), np.uint8(0xff)) ) self.assertEqual( - 0b0000000111111100000000000000, - screen.DHGRBitmap.masked_update(2, 0x00000000, 0xff) + 0b0000000000111111100000000000000000, + screen.DHGRBitmap.masked_update( + 2, np.uint64(0), np.uint8(0xff)) ) self.assertEqual( - 0b1111111000000000000000000000, - screen.DHGRBitmap.masked_update(3, 0x00000000, 0xff) + 0b0001111111000000000000000000000000, + screen.DHGRBitmap.masked_update( + 3, np.uint64(0), np.uint8(0xff)) ) # Now test masking out existing values - int28_max = 2 ** 28 - 1 + int34_max = np.uint64(2 ** 34 - 1) self.assertEqual( - 0b1111111111111111111110000000, - screen.DHGRBitmap.masked_update(0, int28_max, 0x00) + 0b1111111111111111111111110000000111, + screen.DHGRBitmap.masked_update(0, int34_max, np.uint8(0x00)) ) self.assertEqual( - 0b1111111111111100000001111111, - screen.DHGRBitmap.masked_update(1, int28_max, 0x00) + 0b1111111111111111100000001111111111, + screen.DHGRBitmap.masked_update(1, int34_max, np.uint8(0x00)) ) self.assertEqual( - 0b1111111000000011111111111111, - screen.DHGRBitmap.masked_update(2, int28_max, 0x00) + 0b1111111111000000011111111111111111, + screen.DHGRBitmap.masked_update(2, int34_max, np.uint8(0x00)) ) self.assertEqual( - 0b0000000111111111111111111111, - screen.DHGRBitmap.masked_update(3, int28_max, 0x00) + 0b1110000000111111111111111111111111, + screen.DHGRBitmap.masked_update(3, int34_max, np.uint8(0x00)) ) # Test that masked_update can broadcast to numpy arrays - ary = np.zeros((2, 2), dtype=np.uint32) + ary = np.zeros((2, 2), dtype=np.uint64) + + elt = np.uint64(0b1111111000) self.assertTrue(np.array_equal( - np.array([[0x7f, 0x7f], [0x7f, 0x7f]], dtype=np.uint32), - screen.DHGRBitmap.masked_update(0, ary, 0xff) + np.array([[elt, elt], [elt, elt]], dtype=np.uint64), + screen.DHGRBitmap.masked_update(0, ary, np.uint8(0xff)) )) def test_apply(self): dhgr = screen.DHGRBitmap( - main_memory=self.main, aux_memory=self.aux) + main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) - dhgr.apply(page=0, offset=0, is_aux=True, value=0xff) - self.assertEqual(0x0000007f, dhgr.packed[0, 0]) + dhgr.apply(page=0, offset=0, is_aux=True, value=np.uint8(0xff)) + self.assertEqual(0b1111111000, dhgr.packed[0, 0]) - dhgr.apply(page=12, offset=36, is_aux=True, value=0xff) - self.assertEqual(0x0000007f, dhgr.packed[12, 18]) - - # Now update the next aux offset in same uint32 - dhgr.apply(page=12, offset=37, is_aux=True, value=0xff) + dhgr.apply(page=12, offset=36, is_aux=True, value=np.uint8(0xff)) + # Neighbouring header self.assertEqual( - 0b0000000111111100000001111111, + 0, + dhgr.packed[12, 19]) + # Body + self.assertEqual( + 0b1111111000, + dhgr.packed[12, 18]) + # Neighbouring footer + self.assertEqual( + 0b1110000000000000000000000000000000, + dhgr.packed[12, 17]) + + # Now update the next aux offset in same uint64 + dhgr.apply(page=12, offset=37, is_aux=True, value=np.uint8(0xff)) + # Neighbouring header + self.assertEqual( + 0, + dhgr.packed[12, 19]) + # Body + self.assertEqual( + 0b0000000111111100000001111111000, dhgr.packed[12, 18] ) - - dhgr.apply(page=12, offset=37, is_aux=False, value=0b1010101) + # Neighbouring footer self.assertEqual( - 0b1010101111111100000001111111, + 0b1110000000000000000000000000000000, + dhgr.packed[12, 17]) + + # Update offset 3, should propagate to next header + dhgr.apply(page=12, offset=37, is_aux=False, value=np.uint8(0b1010101)) + self.assertEqual( + 0b101, + dhgr.packed[12, 19]) + self.assertEqual( + 0b1010101111111100000001111111000, dhgr.packed[12, 18] ) - - dhgr.apply(page=12, offset=36, is_aux=False, value=0b0001101) self.assertEqual( - 0b1010101111111100011011111111, + 0b1110000000000000000000000000000000, + dhgr.packed[12, 17]) + + dhgr.apply(page=12, offset=36, is_aux=False, value=np.uint8(0b0001101)) + self.assertEqual( + 0b101, + dhgr.packed[12, 19]) + self.assertEqual( + 0b1010101111111100011011111111000, dhgr.packed[12, 18] ) + self.assertEqual( + 0b1110000000000000000000000000000000, + dhgr.packed[12, 17]) + + # Change offset 0, should propagate to neighbouring footer + dhgr.apply(page=12, offset=36, is_aux=True, value=np.uint8(0b0001101)) + # Neighbouring header + self.assertEqual( + 0b101, + dhgr.packed[12, 19]) + self.assertEqual( + 0b1010101111111100011010001101000, + dhgr.packed[12, 18] + ) + # Neighbouring footer + self.assertEqual( + 0b1010000000000000000000000000000000, + dhgr.packed[12, 17]) + + # Now propagate new header from neighbour onto (12, 18) + dhgr.apply(page=12, offset=35, is_aux=False, value=np.uint8(0b1010101)) + self.assertEqual( + 0b1010101111111100011010001101101, + dhgr.packed[12, 18] + ) + # Neighbouring footer + self.assertEqual( + 0b1011010101000000000000000000000000, + dhgr.packed[12, 17]) + + def test_fix_array_neighbours(self): + dhgr = screen.DHGRBitmap( + main_memory=self.main, aux_memory=self.aux, palette=Palette.NTSC) + + packed = dhgr.masked_update(0, dhgr.packed, np.uint8(0x7f)) + dhgr._fix_array_neighbours(packed, 0) + + # Should propagate to all footers + self.assertEqual( + 0, np.count_nonzero( + packed[packed != 0b1110000000000000000000001111111000] + ) + ) + + # Should not change headers/footers + packed = dhgr.masked_update(1, packed, np.uint8(0b1010101)) + dhgr._fix_array_neighbours(packed, 1) + + self.assertEqual( + 0, np.count_nonzero( + packed[packed != 0b1110000000000000010101011111111000] + ) + ) + + # Should propagate to all headers + packed = dhgr.masked_update(3, packed, np.uint8(0b0110110)) + dhgr._fix_array_neighbours(packed, 3) + + self.assertEqual( + 0, np.count_nonzero( + packed[packed != 0b1110110110000000010101011111111011] + ) + ) def binary(a): return np.vectorize("{:032b}".format)(a) -class TestHGRBitmap(unittest.TestCase): - def setUp(self) -> None: - self.main = screen.MemoryMap(screen_page=1) - - def test_pixel_packing_p0_p0(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b01000011 - # PGGFFEED - self.main.page_offset[0, 1] = 0b01000011 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b1100000000111111000000001111 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - def test_pixel_packing_p0_p1(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b01000011 - # PGGFFEED - self.main.page_offset[0, 1] = 0b11000011 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b1000000001111111000000001111 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - def test_pixel_packing_p1_p0(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11000011 - # PGGFFEED - self.main.page_offset[0, 1] = 0b01000011 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b1100000000111110000000011110 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - def test_pixel_packing_p1_p1(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11000011 - # PGGFFEED - self.main.page_offset[0, 1] = 0b11000011 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b1000000001111110000000011110 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - def test_pixel_packing_p1_promote_p0(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b00000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b01000000 - - # PDCCBBAA - self.main.page_offset[0, 2] = 0b10000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b0000000000000000000000000001 - got = hgr.packed[0, 1] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - def test_pixel_packing_p1_promote_p1(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b00000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b11000000 - - # PDCCBBAA - self.main.page_offset[0, 2] = 0b10000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b0000000000000000000000000001 - got = hgr.packed[0, 1] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - def testNominalColours(self): - # PDCCBBAA - self.main.page_offset[0, 0] = 0b01010101 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00101010 - # PDCCBBAA - self.main.page_offset[0, 2] = 0b01010101 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b000110011001100110011001100110011 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - self.assertEqual( - ( - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - colours.HGRColours.VIOLET, - ), - colours.int34_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - # See Figure 8.15 from Sather, "Understanding the Apple IIe" - - def testNominalColoursSather1(self): - # Extend violet into light blue - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b01000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b10000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.LIGHT_BLUE, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather2(self): - # Cut off blue with black to produce dark blue - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.DARK_BLUE, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather3(self): - # Cut off blue with green to produce aqua - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00000001 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.AQUA, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather4(self): - # Cut off white with black to produce pink - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11100000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b0000000000000011100000000000 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - # TODO: BROWN(0001)/VIOLET(1100) should reframe to PINK (1011) - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BROWN, - colours.HGRColours.VIOLET, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather5(self): - # Extend green into light brown - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b01000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b10000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b0000000000000111000000000000 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - # TODO: LIGHT_BLUE should reframe to PINK (1011) - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.LIGHT_BLUE, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather6(self): - # Cut off orange with black to produce dark brown - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b00000000000000010000000000000 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - # TODO: DARK_BLUE should reframe to DARK_BROWN - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.DARK_BLUE, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather7(self): - # Cut off orange with violet to produce pink - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11000000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00000001 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b00000000000001110000000000000 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - # TODO: AQUA should reframe to PINK - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.AQUA, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], - colours.HGRColours) - ) - - def testNominalColoursSather8(self): - # Cut off white with black to produce aqua - - # PDCCBBAA - self.main.page_offset[0, 0] = 0b11100000 - # PGGFFEED - self.main.page_offset[0, 1] = 0b00000000 - - hgr = screen.HGRBitmap( - main_memory=self.main) - - want = 0b00000000000000011100000000000 - got = hgr.packed[0, 0] - - self.assertEqual( - want, got, "\n%s\n%s" % (binary(want), binary(got)) - ) - - # TODO: BROWN/VIOLET should reframe to AQUA - self.assertEqual( - ( - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BROWN, - colours.HGRColours.VIOLET, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - colours.HGRColours.BLACK, - ), - colours.int28_to_nominal_colour_pixels( - hgr.packed[0, 0], colours.HGRColours) - ) +# class TestHGRBitmap: # unittest.TestCase): +# def setUp(self) -> None: +# self.main = screen.MemoryMap(screen_page=1) +# +# def test_pixel_packing_p0_p0(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b01000011 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b01000011 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b1100000000111111000000001111 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# def test_pixel_packing_p0_p1(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b01000011 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b11000011 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b1000000001111111000000001111 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# def test_pixel_packing_p1_p0(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11000011 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b01000011 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b1100000000111110000000011110 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# def test_pixel_packing_p1_p1(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11000011 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b11000011 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b1000000001111110000000011110 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# def test_pixel_packing_p1_promote_p0(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b00000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b01000000 +# +# # PDCCBBAA +# self.main.page_offset[0, 2] = 0b10000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b0000000000000000000000000001 +# got = hgr.packed[0, 1] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# def test_pixel_packing_p1_promote_p1(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b00000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b11000000 +# +# # PDCCBBAA +# self.main.page_offset[0, 2] = 0b10000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b0000000000000000000000000001 +# got = hgr.packed[0, 1] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# def test_nominal_colours(self): +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b01010101 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00101010 +# # PDCCBBAA +# self.main.page_offset[0, 2] = 0b01010101 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b000110011001100110011001100110011 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# self.assertEqual( +# ( +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# colours.HGRColours.VIOLET, +# ), +# colours.int34_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# # See Figure 8.15 from Sather, "Understanding the Apple IIe" +# +# def test_nominal_colours_sather1(self): +# # Extend violet into light blue +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b01000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b10000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.LIGHT_BLUE, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather2(self): +# # Cut off blue with black to produce dark blue +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.DARK_BLUE, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather3(self): +# # Cut off blue with green to produce aqua +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00000001 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.AQUA, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather4(self): +# # Cut off white with black to produce pink +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11100000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b0000000000000011100000000000 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# # TODO: BROWN(0001)/VIOLET(1100) should reframe to PINK (1011) +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BROWN, +# colours.HGRColours.VIOLET, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather5(self): +# # Extend green into light brown +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b01000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b10000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b0000000000000111000000000000 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# # TODO: LIGHT_BLUE should reframe to PINK (1011) +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.LIGHT_BLUE, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather6(self): +# # Cut off orange with black to produce dark brown +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b00000000000000010000000000000 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# # TODO: DARK_BLUE should reframe to DARK_BROWN +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.DARK_BLUE, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather7(self): +# # Cut off orange with violet to produce pink +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11000000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00000001 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b00000000000001110000000000000 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# # TODO: AQUA should reframe to PINK +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.AQUA, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels(hgr.packed[0, 0], +# colours.HGRColours) +# ) +# +# def test_nominal_colours_sather8(self): +# # Cut off white with black to produce aqua +# +# # PDCCBBAA +# self.main.page_offset[0, 0] = 0b11100000 +# # PGGFFEED +# self.main.page_offset[0, 1] = 0b00000000 +# +# hgr = screen.HGRBitmap( +# main_memory=self.main) +# +# want = 0b00000000000000011100000000000 +# got = hgr.packed[0, 0] +# +# self.assertEqual( +# want, got, "\n%s\n%s" % (binary(want), binary(got)) +# ) +# +# # TODO: BROWN/VIOLET should reframe to AQUA +# self.assertEqual( +# ( +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BROWN, +# colours.HGRColours.VIOLET, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# colours.HGRColours.BLACK, +# ), +# colours.int28_to_nominal_colour_pixels( +# hgr.packed[0, 0], colours.HGRColours) +# ) if __name__ == '__main__': diff --git a/transcoder/video.py b/transcoder/video.py index 80cf050..612ecd9 100644 --- a/transcoder/video.py +++ b/transcoder/video.py @@ -122,7 +122,7 @@ class Video: pri, _, page, offset = heapq.heappop(priorities) assert not screen.SCREEN_HOLES[page, offset], ( - "Attempted to store into screen hole at (%d, %d)" % ( + "Attempted to store into screen hole at (%d, %d)" % ( page, offset)) # Check whether we've already cleared this diff while processing @@ -177,7 +177,7 @@ class Video: for cd in content_deltas.values(): cd[page, o] = 0 - byte_offset = target_pixelmap.interleaved_byte_offset(o, is_aux) + byte_offset = target_pixelmap.byte_offset(o, is_aux) old_packed = target_pixelmap.packed[page, o // 2] p = target_pixelmap.byte_pair_difference( diff --git a/transcoder/video_test.py b/transcoder/video_test.py index 3055ef6..3b98c6c 100644 --- a/transcoder/video_test.py +++ b/transcoder/video_test.py @@ -21,23 +21,23 @@ class TestVideo(unittest.TestCase): frame.page_offset[0, 1] = 0b1010101 target_pixelmap = screen.DHGRBitmap( - palette = palette.Palette.NTSC, + palette=palette.Palette.NTSC, main_memory=v.memory_map, aux_memory=frame ) self.assertEqual( - 0b0000000101010100000001111111, + 0b0000000000101010100000001111111000, target_pixelmap.packed[0, 0]) pal = palette.NTSCPalette diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True) - # Expect byte 0 to map to 0b00000000 01111111 - expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0000000001111111] + # Expect byte 0 to map to 0b0001111111000 + expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0001111111000] - # Expect byte 2 to map to 0b000000000000 000101010100 - expect2 = target_pixelmap.edit_distances(pal.ID)[2][0b000101010100] + # Expect byte 2 to map to 0b0001010101000 + expect2 = target_pixelmap.edit_distances(pal.ID)[2][0b0001010101000] self.assertEqual(expect0, diff[0, 0]) self.assertEqual(expect2, diff[0, 1]) @@ -46,7 +46,7 @@ class TestVideo(unittest.TestCase): v.aux_memory_map.page_offset = frame.page_offset v.pixelmap._pack() self.assertEqual( - 0b0000000101010100000001111111, + 0b0000000000101010100000001111111000, v.pixelmap.packed[0, 0] ) @@ -57,21 +57,23 @@ class TestVideo(unittest.TestCase): target_pixelmap = screen.DHGRBitmap( main_memory=v.memory_map, - aux_memory=frame + aux_memory=frame, + palette=pal.ID ) self.assertEqual( - 0b0000000011011000000001101101, + 0b0000000000011011000000001101101000, target_pixelmap.packed[0, 0] ) diff = target_pixelmap.diff_weights(v.pixelmap, is_aux=True) - # Expect byte 0 to map to 0b01111111 01101101 - expect0 = target_pixelmap.edit_distances(pal.ID)[0][0b0111111101101101] + # Expect byte 0 to map to 0b01111111 01101101 XXX + expect0 = target_pixelmap.edit_distances(pal.ID)[0][ + 0b00011111110000001101101000] # Expect byte 2 to map to 0b000101010100 000011011000 expect2 = target_pixelmap.edit_distances(pal.ID)[2][ - 0b0000101010100000011011000] + 0b00010101010000000110110000] self.assertEqual(expect0, diff[0, 0]) self.assertEqual(expect2, diff[0, 1])