Skip to content

Commit 38191c0

Browse files
committed
Added codec: hamming
1 parent 924b0ce commit 38191c0

7 files changed

Lines changed: 141 additions & 2 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ This category also contains `ascii85`, `adobe`, `[x]btoa`, `zeromq` with the `ba
312312
- [X] `excess3`: uses Excess-3 (aka Stibitz code) binary encoding to convert characters from their ordinals
313313
- [X] `fibonacci`: Fibonacci universal code, encoding characters from their ordinals (or lists of integers with `-integers`)
314314
- [X] `gray`: aka reflected binary code
315+
- [X] `hamming`: Hamming(7,4) error-correcting code, correcting single-bit errors (and detecting double-bit errors with `hamming84`)
315316
- [X] `manchester`: XORes each bit of the input with `01`
316317
- [X] `manchester-inverted`: variant of `manchester` ; XORes each bit of the input with `10`
317318
- [X] `nrzi`: Non-Return-to-Zero Inverted line code, writing 1 (or 0 with `nrzi-space`) as a transition

‎docs/pages/enc/binary.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,26 @@ Also called *reflected binary code*, it implements the Gray code applied to char
186186

187187
-----
188188

189+
### Hamming
190+
191+
The [Hamming(7,4)](https://en.wikipedia.org/wiki/Hamming(7,4)) code, invented by Richard Hamming in 1950, writes 4 data bits with 3 parity bits, as the codeword `p1 p2 d1 p3 d2 d3 d4`, where `p1 = d1 ^ d2 ^ d4`, `p2 = d1 ^ d3 ^ d4` and `p3 = d2 ^ d3 ^ d4`: the parity checks of a received codeword give the position of a flipped bit, hence a single-bit error is corrected. The extended Hamming(8,4) code (aka SECDED) adds an overall parity bit, so that two flipped bits are detected instead of being wrongly corrected. Each half of a byte is encoded as a codeword in a byte (whose most significant bit is the overall parity bit with Hamming(8,4) and 0 with Hamming(7,4)), hence each input byte gives two output bytes.
192+
193+
**Codec** | **Conversions** | **Aliases** | **Comment**
194+
:---: | :---: | --- | ---
195+
`hamming` | data <-> Hamming(7,4) codewords | `hamming74`, `hamming-7-4` | single-bit errors silently corrected (two flipped bits in a codeword are wrongly corrected)
196+
`hamming84` | data <-> Hamming(8,4) codewords | `hamming-8-4`, `secded` | single-bit errors silently corrected ; double-bit errors (and a trailing byte) only replaced, dropped or left with a non-strict errors handling
197+
198+
```python
199+
>>> codext.encode(b"Hi", "hamming84")
200+
b'\xcc\xf0f\x99'
201+
>>> codext.decode(b"\xc8\xf0f\x99", "hamming84") # with a flipped bit in the first codeword
202+
b'Hi'
203+
>>> codext.decode(b"\x03\xc3", "hamming84", "replace") # with two flipped bits in the first codeword
204+
b'?'
205+
```
206+
207+
-----
208+
189209
### Manchester
190210

191211
This codec XORes each group of 4 bits of the input text with a 1-byte clock signal, e.g. `0x55` giving in binary `01010101`.

‎src/codext/binary/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .excess3 import *
77
from .fibonacci import *
88
from .gray import *
9+
from .hamming import *
910
from .manchester import *
1011
from .nrzi import *
1112
from .rotate import *

‎src/codext/binary/hamming.py‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# -*- coding: UTF-8 -*-
2+
"""Hamming Codec - Hamming(7,4) error-correcting content encoding.
3+
4+
The Hamming(7,4) code, invented by Richard Hamming in 1950, writes 4 data bits (d1 to d4) with 3 parity bits (p1, p2
5+
and p3), as the codeword p1 p2 d1 p3 d2 d3 d4, where p1 = d1 ^ d2 ^ d4, p2 = d1 ^ d3 ^ d4 and p3 = d2 ^ d3 ^ d4: the
6+
parity checks of a received codeword (its syndrome) give the position of a flipped bit, hence a single-bit error is
7+
corrected. The extended Hamming(8,4) code (aka SECDED, for single error correction and double error detection) adds an
8+
overall parity bit, so that two flipped bits are detected (instead of being wrongly corrected).
9+
10+
Each half of a byte (from the most significant one) is encoded as a codeword in a byte (whose most significant bit is
11+
the overall parity bit with Hamming(8,4), and 0 with Hamming(7,4)), hence each input byte gives two output bytes.
12+
Decoding silently corrects the single-bit errors ; with Hamming(8,4), the double-bit errors are reported (or, with a
13+
non-strict errors handling, replaced, dropped or left as is), as well as a trailing byte.
14+
15+
This codec:
16+
- en/decodes strings from str to str
17+
- en/decodes strings from bytes to bytes
18+
- decodes file content to str (read)
19+
- encodes file content from str to bytes (write)
20+
21+
Reference: https://en.wikipedia.org/wiki/Hamming(7,4)
22+
"""
23+
from ..__common__ import *
24+
25+
26+
__examples1__ = {
27+
'enc(hamming|hamming74|hamming-7-4|hamming_7_4)': {'\xb3': "3C", 'A': "Li", '\x00\xff': "\x00\x00\x7f\x7f"},
28+
'dec(hamming)': {'3C': "\xb3", '\x13C': "\xb3", '3B': "\xb3", '\xb3C': "\xb3", '3': None},
29+
'enc-dec(hamming)': ["", "@random{512}"],
30+
}
31+
__examples2__ = {
32+
'enc(hamming84|hamming-8-4|secded)': {'\xb3': "3\xc3", 'A': "\xcci", '\x00\xff': "\x00\x00\xff\xff"},
33+
'dec(hamming84)': {'3\xc3': "\xb3", '\x13\xc3': "\xb3", '\xb3\xc3': "\xb3", '\x03\xc3': None, '3': None},
34+
'enc-dec(hamming84)': ["", "@random{512}"],
35+
}
36+
37+
38+
def _codeword(nibble, extended):
39+
d1, d2, d3, d4 = (nibble >> 3) & 1, (nibble >> 2) & 1, (nibble >> 1) & 1, nibble & 1
40+
c = (d1 ^ d2 ^ d4) << 6 | (d1 ^ d3 ^ d4) << 5 | d1 << 4 | (d2 ^ d3 ^ d4) << 3 | d2 << 2 | d3 << 1 | d4
41+
# the overall parity bit (Hamming(8,4)) is the most significant bit of the byte
42+
return c | (bin(c).count("1") & 1) << 7 if extended else c
43+
44+
45+
def _decoding(extended):
46+
""" Table of the decoded nibbles of the bytes, None for the uncorrectable errors. """
47+
table = []
48+
for byte in range(256):
49+
c = byte & 0x7f
50+
bits = [c >> (6 - i) & 1 for i in range(7)]
51+
# the syndrome is the position (from 1) of the flipped bit, 0 if none
52+
s = (bits[0] ^ bits[2] ^ bits[4] ^ bits[6]) | (bits[1] ^ bits[2] ^ bits[5] ^ bits[6]) << 1 | \
53+
(bits[3] ^ bits[4] ^ bits[5] ^ bits[6]) << 2
54+
# with the overall parity, an even number of flipped bits with a non-null syndrome is a double-bit error
55+
if extended and s and not bin(byte).count("1") & 1:
56+
table.append(None)
57+
continue
58+
if s:
59+
bits[s - 1] ^= 1
60+
table.append(bits[2] << 3 | bits[4] << 2 | bits[5] << 1 | bits[6])
61+
return table
62+
63+
64+
def hamming_encode(extended):
65+
codewords = [_codeword(n, extended) for n in range(16)]
66+
def encode(text, errors="strict"):
67+
return bytes(codewords[x >> i & 15] for x in b(ensure_str(text)) for i in (4, 0)).decode("latin-1"), len(text)
68+
return encode
69+
70+
71+
def hamming_decode(extended):
72+
ename, table = "hamming84" if extended else "hamming", _decoding(extended)
73+
def decode(text, errors="strict"):
74+
_h, data, r = handle_error(ename, errors, decode=True, kind="codewords"), b(ensure_str(text)), ""
75+
# a trailing byte is reported first, then (with a non-strict errors handling) replaced, dropped or left as is
76+
# at the end
77+
trailing = _h(data[-1:].decode("latin-1"), len(data) - 1, r) if len(data) % 2 else ""
78+
for i in range(0, len(data) - 1, 2):
79+
hi, lo = table[data[i]], table[data[i+1]]
80+
r += _h(data[i:i+2].decode("latin-1"), i, r) if hi is None or lo is None else chr(hi << 4 | lo)
81+
return (r + trailing).encode("latin-1"), len(text)
82+
return decode
83+
84+
85+
add("hamming", hamming_encode(False), hamming_decode(False), r"^hamming(?:[-_]?7[-_]?4)?$", examples=__examples1__)
86+
add("hamming84", hamming_encode(True), hamming_decode(True), r"^(?:hamming[-_]?8[-_]?4|secded)$",
87+
examples=__examples2__)

‎src/codext/binary/manchester.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ def _code(text, errors="strict"):
9393
_h(f"{h1}{h2}", 8 * j + 14 - 2 * i)
9494
y, level = y << 1 | (transition if h1 != level else 1 - transition), h2
9595
r.append(y)
96-
return bytes(r).decode("latin-1"), len(text)
96+
# the decoded data is given as bytes (then as text if it was text)
97+
return bytes(r), len(text)
9798
return _code
9899

99100

‎src/codext/binary/nrzi.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def _code(text, errors="strict"):
4343
level ^= bit == transition
4444
y = y << 1 | level
4545
r.append(y)
46-
return bytes(r).decode("latin-1"), len(text)
46+
# the encoded levels are given as latin-1 text, the decoded data as bytes (then as text if it was text)
47+
return (bytes(r) if decode else bytes(r).decode("latin-1")), len(text)
4748
return _code
4849

4950

‎tests/test_manual.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,39 @@ def recording(keys, rate=8000, width=2, channels=1, tone=.1, pause=.1, drift=0.,
140140
def test_codec_morse(self):
141141
self.assertRaises(LookupError, codecs.encode, "test", "morse-AAB")
142142

143+
def test_codec_hamming(self):
144+
import random
145+
from itertools import combinations
146+
from codext.binary.hamming import _codeword, _decoding
147+
# example of Wikipedia, minimum distances (3 and 4), correction of all the single-bit errors and, with
148+
# Hamming(8,4), detection of all the double-bit errors (that Hamming(7,4) wrongly corrects)
149+
self.assertEqual(_codeword(0b1011, False), 0b0110011)
150+
for extended, n, distance in [(False, 7, 3), (True, 8, 4)]:
151+
codewords, table = [_codeword(d, extended) for d in range(16)], _decoding(extended)
152+
self.assertEqual(min(bin(a ^ c).count("1") for a, c in combinations(codewords, 2)), distance)
153+
for d, c in enumerate(codewords):
154+
self.assertTrue(all(table[c ^ 1 << i] == d for i in range(8)))
155+
for i, j in combinations(range(n), 2):
156+
r = table[c ^ 1 << i ^ 1 << j]
157+
self.assertTrue(r is None if extended else r != d)
158+
# a message with a flipped bit in each codeword is fully corrected
159+
text, rnd = "The quick brown fox jumps over the lazy dog".encode() * 10, random.Random(1)
160+
for e in ["hamming", "hamming84"]:
161+
noisy = bytes(c ^ 1 << rnd.randrange(8 if e == "hamming84" else 7) for c in codecs.encode(text, e))
162+
self.assertEqual(codecs.decode(noisy, e), text)
163+
# with a non-strict errors handling, double-bit errors are replaced, dropped or left as is, as a trailing byte
164+
for e, r in [("replace", b"A??"), ("ignore", b"A"), ("leave", b"A\x03\xc3\x01")]:
165+
self.assertEqual(codecs.decode(b"\xcci\x03\xc3\x01", "hamming84", e), r)
166+
self.assertRaises(ValueError, codecs.decode, b"\xcci\x03\xc3", "hamming84")
167+
self.assertRaises(ValueError, codecs.decode, b"\xcci\x01", "hamming84", "BAD_ERRORS")
168+
143169
def test_codec_nrzi_differential_manchester(self):
144170
# these line codes do not depend on the polarity of the signal: the inverted signal gives the same bits, except
145171
# the first one (depending on the initial level)
146172
data = bytes(range(256))
147173
for e in ["nrzi", "nrzi-space", "differential-manchester", "differential-manchester-inverted"]:
174+
# text with characters outside latin-1 is given back
175+
self.assertEqual(codecs.decode(codecs.encode("caf\u00e9 \u20ac", e), e), "caf\u00e9 \u20ac")
148176
inverted = bytes(x ^ 0xff for x in codecs.encode(data, e))
149177
r = codecs.decode(inverted, e)
150178
self.assertEqual(r[0] & 0x7f, data[0] & 0x7f)

0 commit comments

Comments
 (0)