File Coverage

blib/lib/Net/OnlineCode/GraphDecoder.pm
Criterion Covered Total %
statement 18 234 7.6
branch 0 80 0.0
condition 0 9 0.0
subroutine 6 19 31.5
pod 0 13 0.0
total 24 355 6.7


line stmt bran cond sub pod time code
1             package Net::OnlineCode::GraphDecoder;
2              
3 1     1   4 use strict;
  1         2  
  1         39  
4 1     1   5 use warnings;
  1         1  
  1         28  
5              
6 1     1   6 use Carp;
  1         1  
  1         57  
7              
8 1     1   4 use vars qw($VERSION);
  1         1  
  1         49  
9              
10             $VERSION = '0.02';
11              
12 1     1   4 use constant DEBUG => 0;
  1         11  
  1         59  
13 1     1   5 use constant TRACE => 0;
  1         8  
  1         3074  
14              
15             # Implements a data structure for decoding the bipartite graph (not
16             # needed for encoding). Note that this does not store Block IDs or any
17             # actual block data and, consequently, does not do any XORs. Those
18             # tasks are left to the calling object/programs. The reason for this
19             # separation is to allow us to focus here on the graph algorithm
20             # itself and leave the implementation details (ie, synchronising the
21             # random number generator and storing and XORing blocks) to the user.
22              
23             # Rather than referring to left and right neighbours, I used the
24             # ordering of the array and higher/lower to indicate the relative
25             # positions of check, auxiliary and message blocks, respectively. The
26             # ordering is:
27             #
28             # message < auxiliary < check
29              
30             sub new {
31 0     0 0   my $class = shift;
32              
33             # constructor starts off knowing only about auxiliary block mappings
34 0           my ($mblocks, $ablocks, $auxlist, $expand_aux) = @_;
35              
36 0 0         unless ($mblocks >= 1) {
37 0           carp "$class->new: argument 1 (mblocks) invalid\n";
38 0           return undef;
39             }
40              
41 0 0         unless ($ablocks >= 1) {
42 0           carp "$class->new: argument 2 (ablocks) invalid\n";
43 0           return undef;
44             }
45              
46 0 0         unless (ref($auxlist) eq "ARRAY") {
47 0           carp "$class->new: argument 3 (auxlist) not a list reference\n";
48 0           return undef;
49             }
50              
51 0 0         unless (@$auxlist == $mblocks + $ablocks) {
52 0           carp "$class->new: auxlist does not have $mblocks + $ablocks entries\n";
53 0           return undef;
54             }
55              
56 0 0         $expand_aux = 1 unless defined($expand_aux);
57              
58 0           my $self =
59             {
60             mblocks => $mblocks,
61             ablocks => $ablocks,
62             coblocks => $mblocks + $ablocks, # "composite"
63             expand_aux => $expand_aux,
64             neighbours => undef, # will only store aux block mappings
65             edges => [], # stores both check, aux block mappings
66             solved => [],
67             unsolved_count => $mblocks,
68             nodes => $mblocks + $ablocks, # running count
69             xor_hash => [],
70             iter => 0, # debug use
71             };
72              
73             # work already done in auxiliary_mapping in Decoder
74 0           $self->{neighbours} = $auxlist;
75              
76             # update internal structures
77 0           for my $i (0..$mblocks + $ablocks - 1) {
78             # mark blocks as unsolved, and having no XOR expansion
79 0           $self->{solved} ->[$i] = 0;
80 0           $self->{xor_hash} ->[$i] = {};
81              
82             # empty edge structure
83 0           push @{$self->{edges}}, {}; # 5.14
  0            
84             }
85              
86             # set up edge structure (same as neighbours, but using hashes)
87 0           for my $i (0..$mblocks + $ablocks - 1) {
88 0           for my $j (@{$auxlist->[$i]}) {
  0            
89 0           $self->{edges}->[$i]->{$j} = undef;
90 0           $self->{edges}->[$j]->{$i} = undef;
91             }
92             }
93              
94 0           bless $self, $class;
95             }
96              
97             # use graphviz to figure what's going on/going wrong
98              
99             sub dump_graph_panel {
100              
101 0     0 0   my $self = shift;
102 0           my $panel = shift; # name of the graph (also used as caption)
103 0           my $current = shift;
104              
105 0           my $graph = "subgraph_cluster$panel";
106              
107 0           my ($mblocks,$ablocks,$edges) = @{$self}{"mblocks","ablocks","edges"};
  0            
108              
109             # do a bottom-up construction
110              
111 0           my ($chk,$aux,$msg) = ("", "", "");
112              
113 0           $chk = <
114             subgraph cluster_check_$panel {
115             label="chk";
116             rankdir=LR;
117             rank=same
118             // rank=min;
119             EOT
120              
121 0           $aux = <
122             subgraph cluster_aux_$panel {
123             label="aux";
124             rankdir=LR;
125             rank=same
126             EOT
127              
128 0           $msg = <
129             subgraph cluster_msg_$panel {
130             label="msg";
131             rankdir=LR;
132             rank=same
133             EOT
134              
135             # nodes are described like:
136             # $node [label="\N {@keys}" style=bold];
137             # $node is the node number
138             # @keys are the keys from xor_hash
139             # bold if the node is marked as solved
140              
141 0           my $edgelist="";
142 0           foreach my $i (0 .. scalar @{$self->{neighbours}} -1) {
  0            
143              
144             # don't graph deleted nodes
145 0 0         next if $self->{deleted}->[$i];
146              
147 0           my $nodedesc = "${panel}_$i [label=\"$i {";
148 0           $nodedesc .= join ",", sort { $a <=> $b } keys(%{$self->{xor_hash}->[$i]});
  0            
  0            
149 0           $nodedesc .= "}\"";
150 0 0         $nodedesc .= " color=green" if $self->{solved}->[$i];
151 0 0         $nodedesc .= " style=filled" if $current == $i;
152 0           $nodedesc .= "];";
153              
154             # add invisible links between nodes in this cluster to keep them
155             # from being reordered
156 0 0 0       unless ($i == 0 or $i == $mblocks or $i == $mblocks + $ablocks) {
      0        
157 0           $nodedesc .= "\n ${panel}_";
158 0           $nodedesc .= $i-1 . " -- ${panel}_$i [style=invis]";
159             }
160              
161 0 0         if ($i < $mblocks) {
    0          
162 0           $msg .= " $nodedesc\n";
163             } elsif ($i < $mblocks + $ablocks) {
164 0           $aux .= " $nodedesc\n";
165             } else {
166 0           $chk .= " $nodedesc\n";
167             }
168              
169             # add invisible links between subgraphs
170             #$edgelist .= "cluster_chk -- cluster_aux;\n";
171             #$edgelist .= "cluster_aux -- cluster_msg;\n";
172              
173 0           my $href =$self->{edges}->[$i];
174 0           foreach my $j (sort {$a<=>$b} keys %$href) {
  0            
175 0 0         if ($j < $i) {
176 0 0         die "graph edge ($j,$i) does not have reciprocal link!\n"
177             unless exists($self->{edges}->[$i]->{$j});
178 0           next;
179             }
180              
181 0           my $edgedesc = "${panel}_$i -- ${panel}_$j [dir=";
182 0 0         if (exists($self->{edges}->[$j]->{$i})) {
183 0           $edgedesc .= "both]";
184             } else {
185 0           $edgedesc .= "forward]";
186             }
187              
188             #warn "adding edge description $edgedesc\n";
189 0           $edgelist .= " $edgedesc\n";
190             }
191             }
192              
193 0           my $subgraph =<
194             subgraph cluster_$panel {
195              
196             ranksep = 2;
197             rankdir=BT;
198             // rank=same;
199              
200             label="$panel";
201              
202             $chk }
203              
204             $aux }
205              
206             $msg }
207              
208             $edgelist}
209             EOT
210              
211 0           return $subgraph;
212             }
213              
214             sub is_message {
215 0     0 0   my ($self, $i) = @_;
216 0           return ($i < $self->{mblocks});
217             }
218              
219             sub is_auxiliary {
220 0     0 0   my ($self, $i) = @_;
221 0   0       return (($i >= $self->{mblocks}) && ($i < $self->{coblocks}));
222             }
223              
224             sub is_composite {
225 0     0 0   my ($self, $i) = @_;
226 0           return ($i < $self->{coblocks});
227             }
228              
229             sub is_check {
230 0     0 0   my ($self, $i) = @_;
231 0           return ($i >= $self->{coblocks});
232             }
233              
234              
235             # Set operator: inverts membership
236             sub toggle_xor {
237 0     0 0   my ($self, $target, $value, @junk) = @_;
238              
239             # updates target by xoring value into it
240              
241 0 0         croak "toggle_xor got extra junk parameter" if @junk;
242              
243 0           print "Toggling $value into $target\n" if DEBUG;
244              
245             # Profiling indicates that this is a very heavily-used sub, so a
246             # simple change to avoid various object dereferences should help:
247 0           my $href=$self->{xor_hash}->[$target];
248              
249 0 0         if (exists($href->{$value})) {
250 0           delete $href->{$value};
251             } else {
252 0           $href->{$value} = undef;
253             }
254             }
255              
256             # toggle all keys from a hashref into a solved node
257             sub merge_xor_hash {
258 0     0 0   my ($self, $target, $href) = @_;
259              
260 0 0         unless (ref($href) eq 'HASH') {
261 0           carp "merge_xor_hash: need a hashref as second argument\n";
262 0           return;
263             }
264              
265 0           print "merging node numbers: " . (join ",", keys %$href) . "\n" if DEBUG;
266 0           foreach (keys %$href) {
267 0           print "toggling term: $_\n" if DEBUG;
268 0           $self->toggle_xor($target,$_);
269             }
270             }
271              
272             # return a reference to the hash so that caller may modify values
273             sub xor_hash {
274 0     0 0   my ($self,$i) = @_;
275 0 0         if (defined ($i)) {
276 0           return $self->{xor_hash}->[$i];
277             } else {
278 0           croak "xor_hash: need an index parameter\n";
279             # return $self->{xor_hash};
280             }
281             }
282              
283             # return the keys of xor_hash as a list, honouring expand_aux flag
284             sub xor_list {
285 0     0 0   my ($self,$i) = @_;
286              
287 0 0         croak "xor_list requires a numeric argument (message block index)\n"
288             unless defined($i);
289              
290 0           my $href = $self->{xor_hash}->[$i];
291              
292 0 0         if ($self->{expand_aux}) {
293              
294 0           my $mblocks = $self->{mblocks};
295 0           my $coblocks = $self->{coblocks};
296 0           my %xors = ();
297 0           my @queue = keys %$href;
298              
299 0           while (@queue) {
300 0           my $block = shift @queue;
301 0 0         if ($block >= $coblocks) { # check block -> no expand
    0          
302 0 0         if (exists($xors{$block})) {
303 0           delete $xors{$block};
304             } else {
305 0           $xors{$block} = 1;
306             }
307             } elsif ($block >= $mblocks) { # aux block
308 0           push @queue, keys %{$self->{xor_hash}->[$block]}; # 5.14
  0            
309             } else {
310             # die "BUG: message block found in xor list!\n";
311 0 0         if (exists($xors{$block})) {
312 0           delete $xors{$block};
313             } else {
314 0           $xors{$block} = 1;
315             }
316             }
317             }
318            
319 0           return keys %xors;
320              
321             } else {
322             # return unfiltered list
323 0           return (keys %$href);
324             }
325             }
326              
327             # the decoding algorithm is divided into two steps. The first adds a
328             # new check block to the graph, while the second resolves the graph to
329             # discover newly solvable auxiliary or message blocks.
330              
331             # new approach to graph: use explicit edge structure and remove them
332             # as we resolve the graph
333              
334             sub add_check_block {
335 0     0 0   my $self = shift;
336 0           my $nodelist = shift;
337              
338 0 0         unless (ref($nodelist) eq "ARRAY") {
339 0           croak ref($self) . "->add_check_block: nodelist should be a listref!\n";
340             }
341              
342             # new node number for this check block
343 0           my $node = $self->{nodes}++;
344              
345             # we'll check whether this new block provides any new information by
346             # incrementing unsolved for each unsolved right neighbour
347 0           my $unsolved = 0;
348 0           foreach my $i (@$nodelist) {
349 0 0         ++$unsolved unless $self->{solved}->[$i];
350             }
351              
352 0 0         unless ($unsolved) {
353 0           push @{$self->{edges}}, undef;
  0            
354 0           push @{$self->{xor_hash}}, undef;
  0            
355 0           return 0;
356             }
357              
358             #warn "add_check_block: adding new node index $node\n";
359 0           print "New check block $node: " . (join " ", @$nodelist) . "\n" if DEBUG;
360              
361 0           my $new_hash = {}; # new edge hash for this check block
362              
363             # it simplifies the algorithm if each check block is marked as
364             # (trivially) being composed of only itself. (this way we don't have
365             # to include separate cases for check and aux blocks) (not necessary
366             # any more)
367              
368             # push @{$self->{xor_hash}}, { $node => undef}; # 5.14
369 0           push @{$self->{xor_hash}}, { }; # 5.14
  0            
370              
371             # also mark check block as solved (ie, value is known)
372 0           $self->{solved}->[$node]=1;
373              
374             # store edges, reciprocal links
375 0           foreach my $i (@$nodelist) {
376 0           if (0 and $self->{solved}->[$i]) {
377             # $self->merge_xor_hash($node,$self->{xor_hash}->[$i]);
378             $self->{xor_hash}->[$node]->{$i} = undef;
379             } else {
380 0           $new_hash->{$i} = undef;
381 0           $self->{edges}->[$i]->{$node} = undef;
382             }
383             }
384              
385             # store edges emanating from checkblock (hash form)
386 0           push @{$self->{edges}}, $new_hash; # 5.14
  0            
387              
388             # return index of newly created node
389 0           return $node;
390              
391             }
392              
393             sub delete_edge {
394              
395 0     0 0   my ($self,$from,$to) = @_;
396              
397 0           print "Deleting edge $from, $to\n" if DEBUG;
398              
399 0           delete $self->{edges}->[$from]->{$to};
400 0           delete $self->{edges}->[$to]->{$from};
401              
402             }
403              
404              
405             # the strategy here will be to simplify the graph on each call by
406             # deleting edges.
407             #
408             # For the sake of this explanation, assume that check nodes are to
409             # the left of the auxiliary and message blocks. Check nodes have a
410             # known value, while (initially, at least), auxiliary and message
411             # blocks have unknown values.
412             #
413             # We work our way from known nodes on the left to solve unknowns on
414             # the right. As nodes on the right become known, we save the list of
415             # known left nodes that comprise it in the node's xor list, and then
416             # delete those edges (in fact, each edge becomes an element in the
417             # xor list). When a node has no more left edges, it is marked as
418             # solved.
419             #
420             # There is one rule for propagating a known value from left to
421             # right: when the left node has exactly one right neighbour
422              
423             sub resolve {
424              
425             # same boilerplate as before
426 0     0 0   my $self = shift;
427 0           my $node = shift; # start node
428              
429 0 0         if ($node < $self->{mblocks}) {
430 0           croak ref($self) . "->resolve: start node '$node' is a message block!\n";
431             }
432              
433 0           my $finished = 0;
434 0           my @newly_solved = ();
435 0           my @pending= ($node);
436 0           my $mblocks = $self->{mblocks};
437 0           my $ablocks = $self->{ablocks};
438              
439 0 0         return (1) unless $self->{unsolved_count};
440 0           while (@pending) {
441              
442 0           my ($from, $to) = (shift @pending);
443              
444 0 0         unless ($self->{solved}->[$from]) {
445 0           print "skipping unsolved from node $from\n" if DEBUG;
446 0           next;
447             }
448              
449 0           my @right_nodes;
450 0           my @merge_list = ($from);
451              
452 0           foreach $to (keys %{$self->{edges}->[$from]}) {
  0            
453 0 0         next unless $to < $from;
454 0 0         if ($self->{solved}->[$to]) {
455 0           push @merge_list, $to;
456             } else {
457 0           push @right_nodes, $to; # unsolved
458 0 0         last if @right_nodes > 1; # optimisation
459             }
460             }
461              
462 0           print "Starting node: $from has right nodes: " . (join " ", @right_nodes)
463             . "\n" if DEBUG;
464              
465 0           my $original;
466 0           my $rule1="";
467 0           my $rule2="";
468 0           my $iter = $self->{iter};
469 0           ++$iter;
470 0           if (TRACE) {
471             $original = $self->dump_graph_panel("original",$from);
472             }
473              
474             # my @merge_list =(keys %{$self->{xor_hash}->[$from]});
475 0           while (0) { # $right_degree--) {
476             my $to = shift @right_nodes;
477             if ($self->{solved}->[$to]) {
478             push @merge_list, $to;
479             } else {
480             push @right_nodes, $to; # unsolved
481             }
482             }
483              
484 0           print "Unsolved right degree: " . scalar(@right_nodes) . "\n" if DEBUG;
485              
486 0           if (TRACE) {
487             $rule1 = $self->dump_graph_panel("rule1",$from);
488             }
489              
490 0 0         if (@right_nodes == 0) {
    0          
491 0           next;
492              
493             # if this is a check block with no unsolved right nodes, free
494             # any memory it uses
495 0 0         next if $from < $mblocks + $ablocks;
496              
497 0           $self->{xor_hash}->[$node] = undef;
498 0           foreach my $to (@merge_list) {
499 0           $self->delete_edge($from,$to);
500             }
501              
502             } elsif (@right_nodes == 1) {
503              
504             # we have found a node that matches the propagation rule
505 0           $to = shift @right_nodes;
506              
507 0           print "Node $from solves node $to\n" if DEBUG;
508              
509 0           $self->delete_edge($from,$to);
510 0           foreach my $i (@merge_list) {
511             # $self->merge_xor_hash($to, $self->{xor_hash}->[$i]);
512 0           $self->{xor_hash}->[$to]->{$i}=undef;
513 0           $self->delete_edge($from,$i);
514             }
515              
516             # left nodes are to's left nodes
517 0           my @left_nodes = grep { $_ > $to } keys %{$self->{edges}->[$to]}; # 5.14
  0            
  0            
518              
519             # mark node as solved
520 0           $self->{solved}->[$to] = 1;
521 0           push @newly_solved, $to;
522              
523 0 0         if ($to < $mblocks) {
524 0           print "Solved message block $to completely\n" if DEBUG;
525 0 0         unless (--($self->{unsolved_count})) {
526 0           $finished = 1;
527             # comment out next two lines to continue decoding just in
528             # case there's a bug later
529 0           @pending = ();
530 0           last; # finish searching
531             }
532              
533             } else {
534 0           print "Solved auxiliary block $to completely\n" if DEBUG;
535 0           push @pending, $to;
536             }
537              
538             # if this is a checkblock, free space reserved for xor_hash
539 0 0         if ($from > $mblocks + $ablocks) {
540 0           $self->{xor_hash}->[$from] = undef;
541             }
542              
543 0 0         if (@left_nodes) {
544 0           print "Solved node $to still has left nodes " . (join " ", @left_nodes)
545             . "\n" if DEBUG;
546             } else {
547 0           print "Solved node $to has no left nodes\n" if DEBUG;
548             }
549 0           push @pending, @left_nodes;
550              
551             #@pending = sort { $b <=> $a } @pending;
552              
553             # for my $back (@left_nodes) {
554             # $self->merge_xor_hash($back, $self->{xor_hash}->[$to]);
555             # $self->delete_edge($back,$to);
556             # }
557              
558             }
559              
560              
561 0           if (TRACE) {
562             $rule2=$self->dump_graph_panel("rule2",$from);
563             my $filename = "dump-" . sprintf("%05d", $iter) . ".txt";
564             die "File create? $!\n" unless open DUMP, ">", $filename;
565             print DUMP "graph test {\n$original\n$rule1\n$rule2\n}\n";
566             close DUMP;
567             $self->{iter}=$iter;
568             }
569              
570             }
571              
572             # do a pass over all check, aux blocks to make sure that they can't
573             # solve more blocks
574 0           if (0) {
575             for my $i ($mblocks .. $self->{nodes} - 1) {
576              
577             next unless $self->{solved}->[$i];
578             my @right = grep { $_ < $i &&
579             !$self->{solved}->[$_] }
580             keys %{$self->{edges}->[$i]};
581             if (@right == 1) {
582             my $from = shift @right;
583             warn "algorithm failed to reach node $from that could be solved\n";
584             }
585             }
586             }
587              
588 0           return ($finished, @newly_solved);
589              
590             }
591              
592              
593             1;
594              
595             __END__