File Coverage

blib/lib/Net/OnlineCode/GraphDecoder.pm
Criterion Covered Total %
statement 27 166 16.2
branch 0 40 0.0
condition 0 9 0.0
subroutine 9 20 45.0
pod 0 11 0.0
total 36 246 14.6


line stmt bran cond sub pod time code
1             package Net::OnlineCode::GraphDecoder;
2              
3 1     1   4 use strict;
  1         1  
  1         38  
4 1     1   4 use warnings;
  1         1  
  1         28  
5              
6 1     1   4 use Carp;
  1         1  
  1         54  
7              
8 1     1   3 use vars qw($VERSION);
  1         1  
  1         56  
9              
10             $VERSION = '0.04';
11              
12 1     1   4 use constant DEBUG => 0;
  1         1  
  1         57  
13 1     1   4 use constant TRACE => 0;
  1         1  
  1         40  
14 1     1   4 use constant ASSERT => 1;
  1         0  
  1         36  
15 1     1   3 use constant STEPPING => 1;
  1         1  
  1         32  
16              
17 1     1   372 use Net::OnlineCode::Bones;
  1         3  
  1         1477  
18              
19             # Implements a data structure for decoding the bipartite graph (not
20             # needed for encoding). Note that this does not store Block IDs or any
21             # actual block data and, consequently, does not do any XORs. Those
22             # tasks are left to the calling object/programs. The reason for this
23             # separation is to allow us to focus here on the graph algorithm
24             # itself and leave the implementation details (ie, synchronising the
25             # random number generator and storing and XORing blocks) to the user.
26              
27             # Simple low-level operations to improve readability (and allow for
28             # single debugging points)
29              
30             sub mark_as_unsolved {
31 0     0 0   my ($self,$node) = @_;
32              
33             # print "Marking block $node as unsolved\n" if DEBUG;
34 0           $self->{solution}->[$node] = 0;
35             }
36              
37             sub mark_as_solved {
38 0     0 0   my ($self,$node) = @_;
39              
40             # print "Marking block $node as solved\n" if DEBUG;
41              
42 0           $self->{solution}->[$node] = 1;
43             }
44              
45             # Rather than referring to left and right neighbours, I used the
46             # ordering of the array and higher/lower to indicate the relative
47             # positions of check, auxiliary and message blocks, respectively. The
48             # ordering is:
49             #
50             # message < auxiliary < check
51             #
52             # Using this ordering, message blocks are "lower" than auxiliary and
53             # check blocks and vice-versa. Equivalently, message blocks have no
54             # "lower" nodes and check blocks have no "higher" nodes, while
55             # auxiliary blocks have both.
56             #
57             # This can be used as a mnemonic: there is nothing lower than message
58             # blocks since without them, the sender would not be able to construct
59             # auxiliary or check blocks and the receiver would not be able to
60             # receive anything. Equivalently, think of aux and check blocks as
61             # "higher-level" constructs moving "up" the software stack.
62              
63             sub new {
64 0     0 0   my $class = shift;
65              
66             # constructor starts off knowing only about auxiliary block mappings
67             # my ($mblocks, $ablocks, $auxlist, $expand_aux) = @_;
68 0           my ($mblocks, $ablocks, $auxlist) = @_;
69              
70 0 0         unless ($mblocks >= 1) {
71 0           carp "$class->new: argument 1 (mblocks) invalid\n";
72 0           return undef;
73             }
74              
75 0 0         unless ($ablocks >= 1) {
76 0           carp "$class->new: argument 2 (ablocks) invalid\n";
77 0           return undef;
78             }
79              
80 0 0         unless (ref($auxlist) eq "ARRAY") {
81 0           carp "$class->new: argument 3 (auxlist) not a list reference\n";
82 0           return undef;
83             }
84              
85 0 0         unless (@$auxlist == $mblocks + $ablocks) {
86 0           carp "$class->new: auxlist does not have $mblocks + $ablocks entries\n";
87 0           return undef;
88             }
89              
90 0           my $self =
91             {
92             mblocks => $mblocks,
93             ablocks => $ablocks,
94             coblocks => $mblocks + $ablocks, # "composite"
95              
96             # Edges will be replaced again, this time with "bones", which are
97             # a combination of old v_edges and xor_lists. The top and bottom
98             # structures store links to the bones objects.
99             top => [], # from aux/check
100             bottom => [], # to message/aux
101             solution => [], # message/aux; will be a bone
102              
103             # how many unknown down edges does a node have?
104             unknowns => [],
105              
106             nodes => $mblocks + $ablocks, # running count
107             unresolved => [], # queue of nodes needing resolution
108              
109             unsolved_count => $mblocks,# count unsolved message blocks
110             done => 0, # all message nodes decoded?
111             };
112              
113 0           bless $self, $class;
114              
115             # set up basic structures
116 0           for my $i (0..$mblocks + $ablocks - 1) {
117             # mark blocks as unsolved, and having no XOR expansion
118 0           $self->mark_as_unsolved($i);
119 0           push @{$self->{bottom}}, {}; # hash, like n_edges
  0            
120             }
121              
122             # Set up auxiliary mapping in terms of bones
123 0           for my $aux ($mblocks .. $mblocks + $ablocks - 1) {
124              
125             # The top end aggregates several down links (like old v_edges)
126 0           my @down = @{$auxlist->[$aux]};
  0            
127 0           my $bone = [(1 + @down), $aux, @down];
128 0           Net::OnlineCode::Bones->bless($bone);
129 0           $self->{top}->[$aux-$mblocks] = $bone;
130              
131             # The links fan out at the bottom end
132 0           for my $msg (@down) {
133 0           $self->{bottom}->[$msg]->{$aux} = $bone;
134             }
135              
136             # Set count of unknown down edges
137 0           print "Set unknowns count for aux block $aux to ".
138             ($bone->unknowns - 1) . "\n" if DEBUG;
139 0           push @{$self->{unknowns}}, $bone->unknowns - 1;
  0            
140              
141             }
142              
143 0           if (DEBUG) {
144             print "Auxiliary mapping expressed as bones:\n";
145             for my $aux (0 .. $ablocks - 1) {
146             print " " . ($self->{top}->[$aux]->pp()) . "\n";
147             }
148             }
149              
150 0           $self;
151             }
152              
153             sub is_message {
154 0     0 0   my ($self, $i) = @_;
155 0           return ($i < $self->{mblocks});
156             }
157              
158             sub is_auxiliary {
159 0     0 0   my ($self, $i) = @_;
160 0   0       return (($i >= $self->{mblocks}) && ($i < $self->{coblocks}));
161             }
162              
163             sub is_composite {
164 0     0 0   my ($self, $i) = @_;
165 0           return ($i < $self->{coblocks});
166             }
167              
168             sub is_check {
169 0     0 0   my ($self, $i) = @_;
170 0           return ($i >= $self->{coblocks});
171             }
172              
173              
174              
175              
176             # the decoding algorithm is divided into two steps. The first adds a
177             # new check block to the graph, while the second resolves the graph to
178             # discover newly solvable auxiliary or message blocks.
179              
180             # Decoder object creates a check block and we store it in the graph
181             # here
182              
183             sub add_check_block {
184 0     0 0   my $self = shift;
185 0           my $nodelist = shift;
186 0           my $mblocks = $self->{mblocks};
187              
188 0 0         unless (ref($nodelist) eq "ARRAY") {
189 0           croak ref($self) . "->add_check_block: nodelist should be a listref!\n";
190             }
191              
192 0           my $node = $self->{nodes}++;
193              
194             # set up new array elements
195 0           $self->mark_as_solved($node);
196              
197             # Bones version handles edges and xor list in one list.
198             # The constructor also tests whether elements are solved/unsolved
199 0           my $bone = Net::OnlineCode::Bones->new($self, $node, $nodelist);
200 0 0         die "add_check_block: failed to create bone\n" unless ref($bone);
201              
202 0           $self->{unknowns}->[$node-$mblocks] = $bone->unknowns;
203 0           $self->{top}-> [$node-$mblocks] = $bone;
204              
205 0           print "Set unknowns count for check block $node to " .
206             ($bone->unknowns) . " \n" if DEBUG;
207              
208 0           if (DEBUG) {
209             print "New check block $node: " . ($bone->pp) . "\n";
210             }
211              
212             # Make reciprocal links
213 0           my ($min, $max) = $bone->unknowns_range;
214 0           for ($min .. $max) {
215 0           $self->{bottom}->[$bone->[$_]]->{$node} = $bone;
216             }
217              
218             # mark node as pending resolution
219 0           push @{$self->{unresolved}}, $node;
  0            
220              
221             # return index of newly created node
222 0           return $node;
223              
224             }
225              
226             # Graph resolution. Resolution of the graph has a "downward" part
227             # (resolve()) where nodes with one unsolved edge solve a message or
228             # aux block, and an upward part (cascade()) that works up from a
229             # newly-solved node.
230              
231             # helper function
232             sub apply_solution {
233              
234 0     0 0   my ($self, $node, $bone);
235              
236              
237              
238             }
239              
240             # Work up from a newly-solved block, potentially doing up-propagation
241             # rule
242             sub cascade {
243 0     0 0   my ($self,$node) = @_;
244              
245 0           my $mblocks = $self->{mblocks};
246 0           my $ablocks = $self->{ablocks};
247 0           my $coblocks = $mblocks + $ablocks;
248 0           my $pending = $self->{unresolved};
249              
250 0           my @upper = keys %{$self->{bottom}->[$node]};
  0            
251              
252 0           if (DEBUG) {
253             if (@upper) {
254             print "Solved node $node cascades to nodes " . (join " ", @upper)
255             . "\n\n";
256             } else {
257             print "Solved node $node has no cascade\n\n";
258             }
259             }
260              
261             # update the count of unsolved edges and maybe solve aux blocks
262 0           for my $to (@upper) {
263 0           print "Decrementing unknowns count for block $to\n" if DEBUG;
264 0           ($self->{unknowns}->[$to - $mblocks])--;
265            
266             }
267 0           push @$pending, @upper;
268            
269             }
270              
271             sub resolve {
272              
273 0     0 0   my ($self, @junk) = @_;
274              
275 0 0         if (ASSERT and scalar(@junk)) {
276 0           die "resolve doesn't take arguments\n";
277             }
278              
279 0           my $pending = $self->{unresolved};
280 0 0         unless (@$pending) {
281 0           return ($self->{done});
282             }
283              
284 0           my $start_node = $pending->[0];
285 0 0         if (ASSERT and $start_node < $self->{mblocks}) {
286 0           croak ref($self) . "->resolve: start node '$start_node' is a message block!\n";
287             }
288              
289 0           my @newly_solved = ();
290 0           my $mblocks = $self->{mblocks};
291 0           my $ablocks = $self->{ablocks};
292 0           my $coblocks = $self->{coblocks};
293              
294 0 0         unless ($self->{unsolved_count}) {
295 0           $self->{done}=1;
296 0           return (1);
297             }
298              
299 0           while (@$pending) {
300              
301 0           my ($from, $to, $min, $max) = (shift @$pending);
302              
303 0           my $bone = $self->{top} ->[$from - $mblocks];
304 0           my $unknowns = $self->{unknowns}->[$from - $mblocks];
305 0           my $solved = $self->{solution}->[$from];
306              
307 0 0         next unless ref($bone);
308              
309 0           if (DEBUG) {
310             print "\nStarting resolve at $from: " . $bone->pp .
311             " ($unknowns unknowns)\n";
312             }
313 0           if (DEBUG) {
314             my ($type, $status) = ("check", "unsolved");
315             $type = "auxiliary" if $from < $coblocks;
316             $status = "solved" if $solved;
317             print "Resolving from $status $type node $from\n";
318             }
319              
320             # I'm going back to the old way of doing up-propagation since the
321             # new way messes with single-stepping
322              
323 0 0         if ($unknowns == 0) {
    0          
324              
325 0 0 0       next unless $from < $coblocks and !$solved;
326              
327 0           if (DEBUG) {
328             print "Solving aux block $from based on aux rule\n";
329             }
330              
331 0 0         die "Aux rule: didn't have one unknown\n"
332             if ($from != $bone->known_unsolved($from));
333              
334 0           $self->{solution}->[$from] = $bone;
335              
336             # delete all edges that point up to us
337 0           ($min,$max) = $bone->knowns_range;
338 0           for ($min .. $max) {
339 0           my $lower = $bone->[$_];
340 0 0         die "Tried to delete non-existent up edge\n" if ASSERT and
341             !exists($self->{bottom}->[$lower]->{$from});
342 0           delete $self->{bottom}->[$lower]->{$from};
343             }
344              
345 0           $self->{top}->[$from - $mblocks] = undef;
346            
347 0           push @newly_solved, $bone;
348 0           cascade($self, $from);
349              
350             } elsif ($unknowns == 1) {
351              
352             # Propagation rule matched (one unknown down edge)
353              
354             # resolve() only solves a node if the upper node itself is solved.
355             # cascade() will handle the case of solving an unsolved aux block
356             # by solving its last unsolved message block (upward propagation)
357              
358              
359 0 0 0       if ($from < $coblocks and !$solved) {
360 0           print "Skipping down propagation rule on unsolved aux\n" if DEBUG;
361 0           next;
362             }
363              
364             # pull out the unknown node
365 0 0         if ($solved) {
366 0           $to = $bone->unknown_unsolved($self);
367             }
368              
369 0           if (DEBUG) {
370             my ($type, $status) = ("auxiliary", "an unsolved");
371             $type = "message" if $to < $mblocks;
372             $status = "a solved" if $self->{solution}->[$to];
373             print "To node $to is $status $type node.\n";
374             }
375              
376             # delete reciprocal links for all known edges
377 0           ($min, $max) = $bone->knowns_range;
378 0           for ($min .. $max) {
379 0           my $lower = $bone->[$_];
380             # next if $lower == $from;
381             # die "Tried to delete non-existent up edge $lower->$from\n"
382             # if ASSERT and !exists($self->{bottom}->[$lower]->{$from});
383 0           delete $self->{bottom}->[$lower]->{$from};
384             }
385              
386             # mark child node as solved
387 0           print "Marking block $to as solved\n" if DEBUG;
388 0           $self->{solution}->[$to] = $bone;
389 0           push @newly_solved, $bone;
390              
391 0 0         if ($to < $mblocks) {
392 0           print "Solved message block $to completely\n" if DEBUG;
393 0 0         unless (--($self->{unsolved_count})) {
394 0           $self->{done} = 1;
395             # comment out next two lines to continue decoding just in
396             # case there's a bug later
397 0           @$pending = ();
398 0           last; # finish searching
399             }
400             } else {
401 0           print "Solved auxiliary block $to completely\n" if DEBUG;
402 0           push @$pending, $to;
403             }
404 0           cascade($self, $to);
405             } else {
406 0           next; # go to next pending
407             }
408              
409 0           return ($self->{done}, @newly_solved) if STEPPING;
410              
411             }
412              
413 0           return ($self->{done}, @newly_solved);
414              
415              
416             }
417              
418              
419             1;
420              
421             __END__