File Coverage

blib/lib/Net/OnlineCode/GraphDecoder.pm
Criterion Covered Total %
statement 21 201 10.4
branch 0 52 0.0
condition 0 6 0.0
subroutine 7 21 33.3
pod 0 14 0.0
total 28 294 9.5


line stmt bran cond sub pod time code
1             package Net::OnlineCode::GraphDecoder;
2              
3 1     1   5 use strict;
  1         1  
  1         54  
4 1     1   6 use warnings;
  1         1  
  1         37  
5              
6 1     1   6 use Carp;
  1         1  
  1         83  
7              
8 1     1   6 use vars qw($VERSION);
  1         2  
  1         80  
9              
10             $VERSION = '0.03';
11              
12 1     1   5 use constant DEBUG => 0;
  1         1  
  1         58  
13 1     1   3 use constant TRACE => 0;
  1         6  
  1         35  
14 1     1   4 use constant ASSERT => 1;
  1         1  
  1         2842  
15              
16             # Implements a data structure for decoding the bipartite graph (not
17             # needed for encoding). Note that this does not store Block IDs or any
18             # actual block data and, consequently, does not do any XORs. Those
19             # tasks are left to the calling object/programs. The reason for this
20             # separation is to allow us to focus here on the graph algorithm
21             # itself and leave the implementation details (ie, synchronising the
22             # random number generator and storing and XORing blocks) to the user.
23              
24             # Simple low-level operations to improve readability (and allow for
25             # single debugging points)
26              
27             sub mark_as_unsolved {
28 0     0 0   my ($self,$node) = @_;
29              
30 0           print "Marking block $node as unsolved\n" if DEBUG;
31 0           $self->{solved}->[$node] = 0;
32             }
33              
34             sub mark_as_solved {
35 0     0 0   my ($self,$node) = @_;
36              
37 0           print "Marking block $node as solved\n" if DEBUG;
38              
39 0           $self->{solved}->[$node] = 1;
40             }
41              
42              
43             sub add_edge {
44              
45 0     0 0   my ($self,$high,$low) = @_;
46              
47             # moving to up and down edges means order is important
48 0           if (ASSERT) {
49 0 0         die "add_edge: from must be greater than to" unless $high > $low;
50             }
51              
52 0           my $mblocks = $self->{mblocks};
53              
54 0           $self->{n_edges}->[$low]->{$high} = undef;
55 0           push @{$self->{v_edges}->[$high-$mblocks]}, $low;
  0            
56            
57             }
58              
59             sub delete_up_edge {
60              
61 0     0 0   my ($self,$high,$low) = @_;
62              
63 0           print "Deleting edge $high, $low\n" if DEBUG;
64              
65             # I also want to incorporate updates to the count of unsolved edges
66             # here, and that would require that $high is greater than $low:
67 0 0         if (ASSERT and $low >= $high) {
68 0           die "delete_edge: 1st arg $high not greater than 2nd arg $low\n";
69             }
70              
71 0           delete $self->{n_edges}->[$low]->{$high};
72              
73             # my counting arrays don't include entries for message blocks
74 0           my $mblocks = $self->{mblocks};
75 0 0         if ($high >= $mblocks) {
76 0 0         die "Count for node $high went negative\n" unless
77             ($self->{edge_count}->[$high - $mblocks])--;
78             }
79              
80             }
81              
82              
83             # Rather than referring to left and right neighbours, I used the
84             # ordering of the array and higher/lower to indicate the relative
85             # positions of check, auxiliary and message blocks, respectively. The
86             # ordering is:
87             #
88             # message < auxiliary < check
89             #
90             # Using this ordering, message blocks are "lower" than auxiliary and
91             # check blocks and vice-versa. Equivalently, message blocks have no
92             # "lower" nodes and check blocks have no "higher" nodes, while
93             # auxiliary blocks have both.
94             #
95             # This can be used as a mnemonic: there is nothing lower than message
96             # blocks since without them, the sender would not be able to construct
97             # auxiliary or check blocks and the receiver would not be able to
98             # receive anything. Equivalently, think of aux and check blocks as
99             # "higher-level" constructs moving "up" the software stack.
100              
101             sub new {
102 0     0 0   my $class = shift;
103              
104             # constructor starts off knowing only about auxiliary block mappings
105             # my ($mblocks, $ablocks, $auxlist, $expand_aux) = @_;
106 0           my ($mblocks, $ablocks, $auxlist) = @_;
107              
108 0 0         unless ($mblocks >= 1) {
109 0           carp "$class->new: argument 1 (mblocks) invalid\n";
110 0           return undef;
111             }
112              
113 0 0         unless ($ablocks >= 1) {
114 0           carp "$class->new: argument 2 (ablocks) invalid\n";
115 0           return undef;
116             }
117              
118 0 0         unless (ref($auxlist) eq "ARRAY") {
119 0           carp "$class->new: argument 3 (auxlist) not a list reference\n";
120 0           return undef;
121             }
122              
123 0 0         unless (@$auxlist == $mblocks + $ablocks) {
124 0           carp "$class->new: auxlist does not have $mblocks + $ablocks entries\n";
125 0           return undef;
126             }
127              
128 0           my $self =
129             {
130             mblocks => $mblocks,
131             ablocks => $ablocks,
132             coblocks => $mblocks + $ablocks, # "composite"
133             # edges => [], # stores both check, aux block mappings
134              
135             # Replace single edges list with lists of v_edges (down) and
136             # n_egdes (up). Up edges will continue to be tracked as a list of
137             # hashes, but down edges will move to being a list of lists since
138             # we never need to delete single edges from it. These lists will
139             # also be indexed differently: the up edge list starts with
140             # message blocks (as before) but the down edge list starts with
141             # aux blocks (since message blocks have no down edges)
142             v_edges => [], # down: mnemonic "v" points down
143             n_edges => [], # up: mnemonic "n" is like upside-down "v"
144              
145             edge_count => [], # count unsolved "v" edges (aux, check only)
146             edge_count_x => [], # "transparent" edge count (check only)
147             solved => [],
148             nodes => $mblocks + $ablocks, # running count
149             xor_list => [],
150             unresolved => [], # queue of nodes needing resolution
151              
152             unsolved_count => $mblocks,# count unsolved message blocks
153             done => 0, # all message nodes decoded?
154             };
155              
156 0           bless $self, $class;
157              
158             # set up basic structures
159 0           for my $i (0..$mblocks + $ablocks - 1) {
160             # mark blocks as unsolved, and having no XOR expansion
161 0           $self->mark_as_unsolved($i);
162 0           $self->{xor_list} ->[$i] = [];
163 0           push @{$self->{n_edges}}, {};
  0            
164 0 0         push @{$self->{v_edges}}, [] if $i >= $mblocks;
  0            
165             }
166              
167             # set up edge structure (convert from auxlist's list of lists)
168 0           for my $i (0..$mblocks -1) {
169 0           for my $j (@{$auxlist->[$i]}) {
  0            
170 0           $self->{n_edges}->[$i]->{$j} = undef;
171             }
172             }
173 0           for my $i ($mblocks .. $mblocks + $ablocks - 1) {
174 0           push @{$self->{v_edges}->[$i-$mblocks]}, @{$auxlist->[$i]}
  0            
  0            
175             }
176              
177              
178             # set up edge counts for aux blocks
179 0           for my $i (0 .. $ablocks - 1) {
180 0           push @{$self->{edge_count}}, scalar(@{$auxlist->[$mblocks + $i]});
  0            
  0            
181             }
182              
183 0           $self;
184             }
185              
186             sub is_message {
187 0     0 0   my ($self, $i) = @_;
188 0           return ($i < $self->{mblocks});
189             }
190              
191             sub is_auxiliary {
192 0     0 0   my ($self, $i) = @_;
193 0   0       return (($i >= $self->{mblocks}) && ($i < $self->{coblocks}));
194             }
195              
196             sub is_composite {
197 0     0 0   my ($self, $i) = @_;
198 0           return ($i < $self->{coblocks});
199             }
200              
201             sub is_check {
202 0     0 0   my ($self, $i) = @_;
203 0           return ($i >= $self->{coblocks});
204             }
205              
206              
207              
208              
209             # the decoding algorithm is divided into two steps. The first adds a
210             # new check block to the graph, while the second resolves the graph to
211             # discover newly solvable auxiliary or message blocks.
212              
213             # Decoder object creates a check block and we store it in the graph
214             # here
215              
216             sub add_check_block {
217 0     0 0   my $self = shift;
218 0           my $nodelist = shift;
219              
220 0 0         unless (ref($nodelist) eq "ARRAY") {
221 0           croak ref($self) . "->add_check_block: nodelist should be a listref!\n";
222             }
223              
224             # The original code that I was using here would create a new entry
225             # in the graph structure regardless of whether the check block
226             # actually added any new information or not. Later, I modified this
227             # to only add checkblocks that add new info to the graph.
228             # Unfortunately, that led to a (fairly trivial) bug in my codec code
229             # where it failed to check the return value of
230             # Decoder->accept_check_block and got confused about its check block
231             # array.
232             #
233             # Given the choice of making more work for the calling program (and
234             # making them more error-prone) and using slightly more memory, I've
235             # decided that the latter option is best. So this routine will now
236             # revert to the original method and always add a check block,
237             # regardless of whether it adds new information or not.
238              
239 0           my $node = $self->{nodes}++;
240              
241             # set up new array elements
242             #push @{$self->{xor_hash}}, { $node => undef };
243 0           push @{$self->{xor_list}}, [$node];
  0            
244 0           $self->mark_as_solved($node);
245 0           push @{$self->{v_edges}}, [];
  0            
246              
247 0           my $solved = 0; # just used for debug output
248 0           my @solved = (); # ditto
249 0           my $unsolved_count = 0;
250              
251             # set up graph edges and/or xor list
252 0           foreach my $i (@$nodelist) {
253 0 0         if ($self->{solved}->[$i]) {
254 0           ++$solved;
255 0           push @solved, $i;
256             # solved, so add node $i to our xor list
257 0           push @{$self->{xor_list}->[$node]}, $i;
  0            
258            
259             } else {
260             # unsolved, so add edge to $i
261 0           $self->add_edge($node,$i);
262 0           ++$unsolved_count;
263             }
264             }
265 0           push @{$self->{edge_count}}, $unsolved_count;
  0            
266              
267             # TODO: also expand any aux blocks and create separate edges
268             # pointing directly to message blocks
269              
270 0           if (DEBUG) {
271             print "New check block $node: " . (join " ", @$nodelist) . "\n";
272             print "of which, there are $solved solved node(s): " .
273             (join " ", @solved) . "\n";
274             }
275              
276             # mark node as pending resolution
277 0           push @{$self->{unresolved}}, $node;
  0            
278              
279             # return index of newly created node
280 0           return $node;
281              
282             }
283              
284             # Graph resolution. Resolution of the graph has a "downward" part
285             # (resolve()) where nodes with one unsolved edge solve a message or
286             # aux block, and an upward part (cascade()) that works up from a
287             # newly-solved node.
288              
289              
290             # an unsolved aux block can be solved if it has no unsolved neighbours
291             sub aux_rule {
292 0     0 0   my ($self, $from, $solved) = @_;
293              
294 0           if (DEBUG) {
295             print "Solving aux block $from based on aux rule\n";
296             print "XORing expansion of these solved message blocks: " .
297             (join " ", @$solved) . "\n";
298             }
299              
300 0           $self->mark_as_solved($from);
301              
302 0           push @{$self->{xor_list}->[$from]}, @$solved;
  0            
303 0           for my $to (@$solved) {
304             # don't call delete_edge: unsolved counts would be wrong
305 0           delete $self->{n_edges}->[$to]->{$from};
306             }
307 0           $self->{v_edges}->[$from - $self->{mblocks}] = [];
308             }
309              
310              
311             # Work up from a newly-solved message or auxiliary block
312              
313             sub cascade {
314 0     0 0   my ($self,$node) = @_;
315              
316 0           my $mblocks = $self->{mblocks};
317 0           my $ablocks = $self->{ablocks};
318 0           my $pending = $self->{unresolved};
319              
320 0           my @higher_nodes = keys %{$self->{n_edges}->[$node]};
  0            
321              
322 0           if (DEBUG) {
323             if (@higher_nodes) {
324             print "Solved node $node cascades to nodes " . (join " ", @higher_nodes)
325             . "\n\n";
326             } else {
327             print "Solved node $node has no cascade\n\n";
328             }
329             }
330              
331             # update the count of unsolved edges.
332 0           for my $to (@higher_nodes) {
333 0           ($self->{edge_count}->[$to - $mblocks])--;
334             }
335 0           push @$pending, @higher_nodes;
336              
337             }
338              
339             # helper function to delete (solved) edges from a solved node and
340             # optionally free the XOR list
341             sub decommission_node {
342              
343 0     0 0   my ($self, $node, $solved_list) = @_;
344              
345 0           foreach (@$solved_list) {
346 0           delete $self->{n_edges}->[$_]->{$node};
347             }
348 0           $self->{v_edges}->[$node - $self->{mblocks}] = [];
349              
350             # we can free XOR list only if this is a check block
351 0 0         if ($node >= $self->{coblocks}) {
352 0           $self->{xor_list}->[$node] = undef;
353             }
354              
355             }
356              
357             # Work down from a check or auxiliary block
358             sub resolve {
359              
360             # now doesn't take any arguments (uses unresolved queue instead)
361 0     0 0   my ($self, @junk) = @_;
362              
363 0 0         if (ASSERT and scalar(@junk)) {
364 0           die "resolve doesn't take arguments\n";
365             }
366              
367 0           my $pending = $self->{unresolved};
368              
369             # Indicate to caller that our queue is empty and they need to add
370             # another check block (see example code at top of Decoder man page)
371 0 0         unless (@$pending) {
372 0           return ($self->{done});
373             }
374              
375 0           my $start_node = $pending->[0];
376              
377 0 0         if (ASSERT and $start_node < $self->{mblocks}) {
378 0           croak ref($self) . "->resolve: start node '$start_node' is a message block!\n";
379             }
380              
381 0           my @newly_solved = ();
382 0           my $mblocks = $self->{mblocks};
383 0           my $ablocks = $self->{ablocks};
384              
385             # exit if all message blocks are already solved
386 0 0         unless ($self->{unsolved_count}) {
387 0           $self->{done}=1;
388 0           return (1);
389             }
390              
391 0           while (@$pending) {
392              
393 0           my ($from, $to) = (shift @$pending);
394              
395 0 0 0       unless ($self->is_auxiliary($from) or $self->{solved}->[$from]) {
396 0           print "skipping unproductive node $from\n" if DEBUG;
397 0           next;
398             }
399              
400 0           my @solved_nodes = ();
401 0           my @unsolved_nodes;
402 0           my $count_unsolved = $self->{edge_count}->[$from - $mblocks];
403              
404 0           if (DEBUG) {
405             print "\nStarting resolve at $from; XOR list is " .
406             (join ", ", @{$self->{xor_list}->[$from]}) . "\n";
407             }
408              
409 0 0         next unless $count_unsolved < 2;
410              
411 0           my @lower_nodes = @{$self->{v_edges}->[$from-$mblocks]};
  0            
412              
413 0           print "Node links to $count_unsolved unsolved nodes\n" if DEBUG;
414              
415 0 0         if ($count_unsolved == 0) {
    0          
416              
417 0 0         if ($self->is_check($from)) {
418              
419             # This check block can't provide any more useful information,
420             # so delete any remaining edges and free xor space
421              
422 0           $self->decommission_node($from, \@lower_nodes);
423 0           next;
424              
425             } else {
426              
427 0 0         if ($self->{solved}->[$from]) {
428             # Was previously solved by propagation resolve, so we don't
429             # solve again. Delete the graph edges since they're all
430             # solved too.
431 0           $self->decommission_node($from, \@lower_nodes);
432              
433             } else {
434             # Otherwise solve it with aux rule
435 0           $self->aux_rule($from, \@lower_nodes);
436              
437 0           print "Aux rule solved auxiliary block $from completely\n" if DEBUG;
438              
439 0           push @newly_solved, $from;
440 0           $self->cascade($from);
441             }
442             }
443              
444             } elsif ($count_unsolved == 1) {
445              
446 0 0         next unless $self->{solved}->[$from];
447              
448             # Propagation rule matched; separate solved from unsolved
449              
450             # TODO: this whole section (up to "Update global structure") can
451             # be rewritten to only scan @lower_nodes list once
452 0           for my $i (@lower_nodes) {
453 0 0         if ($self->{solved}->[$i]) {
454 0           push @solved_nodes, $i;
455             } else {
456 0           $to = $i;
457             }
458             }
459              
460 0           print "Node $from solves node $to\n" if DEBUG;
461              
462 0           $self->mark_as_solved($to);
463 0           push @newly_solved, $to;
464              
465             # create XOR list for the newly-solved node, comprising this
466             # node's XOR list plus all nodes in the @solved array
467              
468 0           if (DEBUG) {
469             print "Node $from has XOR list: " .
470             (join ", ", @{$self->{xor_list}->[$from]}) . "\n";
471             }
472            
473 0           $self->delete_up_edge($from,$to);
474 0           push @{$self->{xor_list}->[$to]}, @{$self->{xor_list}->[$from]};
  0            
  0            
475 0           push @{$self->{xor_list}->[$to]}, @solved_nodes;
  0            
476              
477 0           $self->decommission_node($from, \@solved_nodes);
478              
479             # Update global structure and decide if we're done
480 0 0         if ($to < $mblocks) {
481 0           print "Solved message block $to completely\n" if DEBUG;
482 0 0         unless (--($self->{unsolved_count})) {
483 0           $self->{done} = 1;
484             # comment out next two lines to continue decoding just in
485             # case there's a bug later
486 0           @$pending = ();
487 0           last; # finish searching
488             }
489             } else {
490 0           print "Solved auxiliary block $to completely\n" if DEBUG;
491 0           push @$pending, $to;
492             }
493              
494             # Cascade to potentially find more solvable blocks
495 0           $self->cascade($to);
496              
497             }
498              
499             }
500              
501 0           return ($self->{done}, @newly_solved);
502              
503             }
504              
505              
506             1;
507              
508             __END__