File Coverage

blib/lib/Schedule/Activity.pm
Criterion Covered Total %
statement 562 606 92.7
branch 173 220 78.6
condition 88 136 64.7
subroutine 30 30 100.0
pod 0 11 0.0
total 853 1003 85.0


line stmt bran cond sub pod time code
1             package Schedule::Activity;
2              
3 1     1   5853 use strict;
  1         2  
  1         35  
4 1     1   4 use warnings;
  1         2  
  1         57  
5 1     1   5 use List::Util qw/any/;
  1         2  
  1         136  
6 1     1   614 use Ref::Util qw/is_arrayref is_hashref is_plain_hashref is_ref/;
  1         2932  
  1         143  
7 1     1   8 use Scalar::Util qw/blessed/;
  1         1  
  1         106  
8 1     1   502 use Schedule::Activity::Annotation;
  1         4  
  1         45  
9 1     1   587 use Schedule::Activity::Attributes;
  1         4  
  1         38  
10 1     1   559 use Schedule::Activity::Message;
  1         3  
  1         49  
11 1     1   594 use Schedule::Activity::Node;
  1         3  
  1         48  
12 1     1   636 use Schedule::Activity::NodeFilter;
  1         3  
  1         8705  
13              
14             our $VERSION='0.3.0';
15              
16             sub new {
17 55     55 0 555326 my ($ref,%opt)=@_;
18 55   33     321 my $class=is_ref($ref)||$ref;
19             my %self=(
20             config =>$opt{configuration}//{},
21             attr =>undef,
22             valid =>0,
23             built =>undef,
24             reach =>undef,
25 55   50     855 unsafe =>$opt{unsafe}//0,
      50        
26             PNA =>undef, # per node attribute prefix, loaded via config
27             );
28 55         715 return bless(\%self,$class);
29             }
30              
31             # validate()
32             # compile()
33             # schedule(activities=>[...])
34              
35             sub _attr {
36 131909     131909   251087 my ($self)=@_;
37 131909   66     293761 $$self{attr}//=Schedule::Activity::Attributes->new();
38 131909         556480 return $$self{attr};
39             }
40              
41             sub validate {
42 55     55 0 151 my ($self,$force)=@_;
43 55 50 33     329 if($$self{valid}&&!$force) { return }
  0         0  
44 55 50 50     153 $$self{config}//={}; if(!is_hashref($$self{config})) { return ('Configuration must be a hash') }
  55         230  
  0         0  
45 55         218 my @errors=$self->_validateConfig();
46 55 100       146 if(!@errors) { $$self{valid}=1 }
  52         138  
47 55         135 return @errors;
48             }
49              
50             sub _validateConfig {
51 55     55   121 my ($self)=@_;
52 55         155 my $attr=$self->_attr();
53 55         113 my %config=%{$$self{config}};
  55         262  
54 55         140 my (@errors,@invalids);
55 55 50       154 if(!is_hashref($config{node})) { push @errors,'Config is missing: node'; $config{node}={} }
  0         0  
  0         0  
56 55 100       183 if($config{attributes}) {
57 3 50       9 if(!is_hashref($config{attributes})) { push @errors,'Attributes invalid structure' }
  0         0  
58 3         6 else { while(my ($k,$v)=each %{$config{attributes}}) { push @errors,$attr->register($k,%$v) } }
  7         24  
  10         29  
59             }
60 55 100       159 if($config{messages}) {
61 2 50       8 if(!is_hashref($config{messages})) { push @errors,'Messages invalid structure' }
  0         0  
62             else {
63 2         4 while(my ($namea,$msga)=each %{$config{messages}}) {
  5         22  
64 3 50 66     20 if(!is_hashref($msga)) { push @errors,"Messages $namea invalid structure"; next }
  0 50       0  
  0         0  
65 0         0 elsif(defined($$msga{attributes})&&!is_hashref($$msga{attributes})) { push @errors,"Messages $namea invalid attributes"; delete($$msga{attributes}) }
  0         0  
66 3         13 else { foreach my $kv (Schedule::Activity::Message::attributesFromConf($msga)) { push @errors,$attr->register($$kv[0],%{$$kv[1]}) } }
  3         6  
  3         10  
67 3 100       16 if(is_hashref($$msga{message})) {
68 1 50 33     10 if(defined($$msga{message}{alternates})&&!is_arrayref($$msga{message}{alternates})) { push @errors,"Messages $namea invalid alternates" }
  0         0  
69 1         4 else { foreach my $kv (Schedule::Activity::Message::attributesFromConf($$msga{message})) { push @errors,$attr->register($$kv[0],%{$$kv[1]}) } }
  2         6  
  2         10  
70             }
71             }
72             } } # messages
73 55         121 while(my ($k,$node)=each %{$config{node}}) {
  462         1647  
74 407 50       806 if(!is_hashref($node)) { push @errors,"Node $k, Invalid structure"; next }
  0         0  
  0         0  
75 407         1307 Schedule::Activity::Node::defaulting($node);
76 407         1499 my @nerrors=Schedule::Activity::Node::validate(%$node);
77 407 100       1249 if($$node{attributes}) {
78 179 50       490 if(!is_hashref($$node{attributes})) { push @nerrors,"attributes, Invalid structure" }
  0         0  
79 179         306 else { while(my ($k,$v)=each %{$$node{attributes}}) { push @nerrors,$attr->register($k,%$v) } }
  200         944  
  379         1313  
80             }
81 407         1762 push @nerrors,Schedule::Activity::Message::validate($$node{message},names=>$config{messages});
82 407         1412 foreach my $kv (Schedule::Activity::Message::attributesFromConf($$node{message})) { push @nerrors,$attr->register($$kv[0],%{$$kv[1]}) }
  14         18  
  14         30  
83 407 100       910 if(@nerrors) { push @errors,map {"Node $k, $_"} @nerrors; next }
  2         3  
  2         7  
  2         5  
84 405   100     1747 @invalids=grep {!defined($config{node}{$_})} Schedule::Activity::Node->nextnames(0,$$node{next}//[]);
  846         2015  
85 405 50       853 if(@invalids) { push @errors,"Node $k, Undefined name in array: next" }
  0         0  
86 405 100 100     1512 if(defined($$node{finish})&&!defined($config{node}{$$node{finish}})) { push @errors,"Node $k, Undefined name: finish" }
  1         4  
87             }
88 55   100     272 $config{annotations}//={};
89 55 50       175 if(!is_hashref($config{annotations})) { push @errors,'Annotations must be a hash' }
  0         0  
90 55         117 else { while(my ($k,$notes)=each %{$config{annotations}}) {
  60         225  
91 5         18 push @errors,map {"Annotation $k: $_"} map {(
  0         0  
92             Schedule::Activity::Annotation::validate(%$_),
93             Schedule::Activity::Message::validate($$_{message},names=>$config{messages})
94 6         44 )} @$notes } }
95 55         177 return @errors;
96             }
97              
98             sub _reachability {
99 54     54   13532 my ($self)=@_;
100 54         79 my $changed;
101 54         230 my %reach=(min=>{},max=>{});
102 54         98 foreach my $namea (keys %{$$self{built}{node}}) {
  54         287  
103 411         782 my $nodea=$$self{built}{node}{$namea};
104 411         540 my @nodes;
105 411 100       1091 if (is_arrayref($$nodea{next})) { @nodes=@{$$nodea{next}} }
  306 100       386  
  306         857  
106 5         12 elsif(is_hashref($$nodea{next})) { @nodes=map {$$_{node}} values %{$$nodea{next}} }
  24         49  
  5         15  
107 411         830 foreach my $nodeb (@nodes) {
108 853         2418 $reach{min}{$nodea}{$nodeb}=$$nodea{tmmin};
109 853 100       3183 $reach{max}{$nodea}{$nodeb}=(($nodea eq $nodeb)?'+':$$nodea{tmmax});
110             }
111             }
112 54         152 $changed=1;
113 54         167 while($changed) { $changed=0;
  110         208  
114 110         213 foreach my $nodea (keys %{$reach{min}}) {
  110         371  
115 730         1070 foreach my $nodeb (keys %{$reach{min}{$nodea}}) {
  730         1734  
116 1669         2243 foreach my $nodec (keys %{$reach{min}{$nodeb}}) {
  1669         3884  
117 3460         5651 my $x=$reach{min}{$nodea}{$nodec};
118 3460         6361 my $y=$reach{min}{$nodea}{$nodeb}+$reach{min}{$nodeb}{$nodec};
119 3460 100 100     11845 if(!defined($x)||($x>$y)) {
120 152         311 $reach{min}{$nodea}{$nodec}=$y;
121 152         287 $changed=1;
122             }
123             } } }
124             }
125             my $triadd=sub {
126 828     828   1533 my ($x,$y)=@_;
127 828 100       2531 if($x eq '+') { return '+' }
  361         707  
128 467 100       936 if($y eq '+') { return '+' }
  279         670  
129 188         331 return $x+$y;
130 54         401 };
131 54         112 $changed=1;
132 54         143 while($changed) { $changed=0;
  116         173  
133 116         193 foreach my $nodea (keys %{$reach{max}}) {
  116         422  
134 776         1053 foreach my $nodeb (keys %{$reach{max}{$nodea}}) {
  776         1907  
135 1789         2382 foreach my $nodec (keys %{$reach{max}{$nodeb}}) {
  1789         4089  
136 3749 100       6835 if($nodea eq $nodec) { $reach{max}{$nodea}{$nodec}='+'; next }
  784         1461  
  784         1463  
137 2965         12033 my $x=$reach{max}{$nodea}{$nodec};
138 2965 100 100     8734 if(defined($x)&&($x eq '+')) { next }
  2137         3802  
139 828         1824 my $y=&$triadd($reach{max}{$nodea}{$nodeb},$reach{max}{$nodeb}{$nodec});
140 828 100 100     2934 if(!defined($x)||($y eq '+')||($x<$y)) {
      100        
141 778         1713 $reach{max}{$nodea}{$nodec}=$y;
142 778         1397 $changed=1;
143             }
144             } } }
145             }
146 54         149 $$self{reach}=\%reach;
147 54         398 return $self;
148             }
149              
150             # These checks ignore any filtering that might be active during construction; these are only sanity checks.
151             # Recommend stashing the reachability results in $self for later.
152             #
153             # Here are the tests and their defined orders.
154             # 1. Activity that cannot reach finish
155             # 2. Orphaned actions (no activity reaches them)
156             # 3. Dual-parent action nodes (with more than a single root activity) NOT(item2)
157             # 4. Dual-finish action nodes NOT(item3)
158             # 5. Dangling actions (cannot reach their finish node) NOT(item1||item4)
159             # 6. Action nodes with tmavg=0 NOT(activity|finish) (this is only a problem if there's a cycle)
160             #
161             sub safetyChecks {
162 51     51 0 131 my ($self)=@_;
163 51         96 my (@errors,$changed);
164 51         176 $self->_reachability();
165 51         105 my %reach=%{$$self{reach}};
  51         269  
166             #
167             # Be very cautious about names versus stringified references.
168 51         176 my $builtNode=$$self{built}{node};
169 51         281 my %activities=map {$$builtNode{$_}=>$$builtNode{$_}{finish}} grep {defined($$builtNode{$_}{finish})} keys(%$builtNode);
  99         408  
  401         921  
170 51         186 my %finishes=map {$_=>1} values(%activities);
  99         308  
171 51   100     180 my %actions=map {$_=>$$builtNode{$_}} grep {!exists($activities{$$builtNode{$_}})&&!exists($finishes{$$builtNode{$_}})} keys(%$builtNode);
  203         518  
  401         1589  
172 51         221 my %incompleteActivities=map {$_=>1} grep{!defined($reach{min}{$$builtNode{$_}}{$activities{$$builtNode{$_}}})} grep {defined($$builtNode{$_}{finish})} keys(%$builtNode);
  2         8  
  99         487  
  401         831  
173             #
174 51         161 push @errors,map {"Finish for activity $_ is unreachable"} keys(%incompleteActivities);
  2         7  
175             #
176 51         133 my (%orphans,%dualParent,%dualFinish,%dangling,%infiniteCycle);
177 51         151 foreach my $action (keys %actions) {
178 203         353 my $parents=0;
179 203         311 my $terminals=0;
180 203 100       425 foreach my $activity (keys %activities) { if(defined($reach{min}{$activity}{$actions{$action}})) { $parents++ } }
  493         1418  
  203         374  
181 203 100       455 foreach my $finish (keys %finishes) { if(defined($reach{min}{$actions{$action}}{$finish})) { $terminals++ } }
  493         1502  
  200         371  
182 203 100       672 if($parents==0) { $orphans{$action}=1 }
  2 100       4  
183 2         4 elsif($parents>1) { $dualParent{$action}=1 }
184 203 100       519 if($terminals>1) { $dualFinish{$action}=1 }
  2 100       6  
185 5         12 elsif(!$terminals) { $dangling{$action}=1 }
186 203 100 100     684 if(($actions{$action}{tmavg}==0)&&(defined($reach{min}{$actions{$action}}{$actions{$action}}))) { $infiniteCycle{$action}=1 }
  3         10  
187             }
188 51         131 push @errors,map {"Action $_ belongs to no activity"} keys(%orphans);
  2         6  
189 51         128 push @errors,map {"Action $_ belongs to multiple activities"} keys(%dualParent);
  2         11  
190 51         154 push @errors,map {"Action $_ reaches multiple finish nodes"} keys(%dualFinish);
  2         6  
191 51         109 push @errors,map {"Dangling action $_"} keys(%dangling);
  5         16  
192 51         110 push @errors,map {"No progress will be made for action $_"} keys(%infiniteCycle);
  3         8  
193 51         374 return @errors;
194             }
195              
196             sub _buildConfig {
197 52     52   194 my ($self)=@_;
198 52         88 my %base=%{$$self{config}};
  52         220  
199 52         163 my $attr=$self->_attr();
200 52         114 my %res;
201 52 100       167 if($base{PNA}) { $$self{PNA}=$base{PNA} }
  2         7  
202 52         90 while(my ($k,$node)=each %{$base{node}}) {
  452         1305  
203 400 50       700 if(is_plain_hashref($node)) { $res{node}{$k}=Schedule::Activity::Node->new(%$node) }
  400 0       1297  
204 0         0 elsif(blessed($node)) { $res{node}{$k}=$node }
205 0         0 else { die "Invalid node $k when building config" }
206 400         1075 $res{node}{$k}{keyname}=$k;
207 400 100       902 if($$self{PNA}) {
208 8         31 $res{node}{$k}{attributes}{"$$self{PNA}$k"}={incr=>1};
209 8         28 $attr->register("$$self{PNA}$k",type=>'int',value=>0);
210             }
211             }
212 52   100     254 my $msgNames=$base{messages}//{};
213 52         113 while(my ($k,$node)=each %{$res{node}}) {
  452         1387  
214 400         1122 $node->nextremap($res{node});
215 400 100       867 if(defined($$node{finish})) { $$node{finish}=$res{node}{$$node{finish}} }
  99         247  
216 400         1214 $$node{msg}=Schedule::Activity::Message->new(message=>$$node{message},names=>$msgNames);
217 400 100       1788 if(is_plain_hashref($$node{require})) { $$node{require}=Schedule::Activity::NodeFilter->new(%{$$node{require}}) }
  7         16  
  7         52  
218             }
219 52         159 $$self{built}=\%res;
220 52         161 return $self;
221             }
222              
223             sub compile {
224 17161     17161 0 53993 my ($self,%opt)=@_;
225 17161 100       54460 if($$self{built}) { return }
  17106         51144  
226 55         248 my @errors=$self->validate();
227 55 100       135 if(@errors) { return (error=>\@errors) }
  3         11  
228 52         254 $self->_buildConfig();
229 52 100       175 if(!$opt{unsafe}) { @errors=$self->safetyChecks(); if(@errors) { return (error=>\@errors) } }
  50 100       169  
  50         174  
  2         8  
230 50         158 return;
231             }
232              
233             sub _nodeMessage {
234 185980     185980   404115 my ($optattr,$tm,$node)=@_;
235 185980         599092 my ($message,$msg)=$$node{msg}->random();
236 185980 100       583516 if($$node{attributes}) {
237 114659         187432 while(my ($k,$v)=each %{$$node{attributes}}) {
  311435         1091470  
238 196776         595449 $optattr->change($k,%$v,tm=>$tm) } }
239 185980 50       431644 if(is_hashref($msg)) { while(my ($k,$v)=each %{$$msg{attributes}}) {
  185980         288087  
  186046         561218  
240 66         277 $optattr->change($k,%$v,tm=>$tm);
241             } }
242             #
243 185980   50     730695 return Schedule::Activity::Message->new(message=>$message,attributes=>$$msg{attributes}//{});
244             }
245              
246             sub findpath {
247 18839     18839 0 108851 my (%opt)=@_;
248 18839         58236 my ($tm,$slack,$buffer,@res)=(0,0,0);
249             my %tension=(
250             slack =>1-($opt{tensionslack} //$opt{tension}//0.5),
251 18839   66     103045 buffer=>1-($opt{tensionbuffer}//$opt{tension}//0.85659008),
      50        
      66        
      50        
252             );
253 18839 50       41751 foreach my $k (qw/slack buffer/) { if($tension{$k}>1){$tension{$k}=1}; if($tension{$k}<0){$tension{$k}=0} }
  37678 50       97774  
  0         0  
  37678         93010  
  0         0  
254 18839         45095 my ($node,$conclusion)=($opt{start},$opt{finish});
255 18839         72011 $opt{attr}->push();
256 18839   66     110174 while($node&&($node ne $conclusion)) {
257 167141         489869 push @res,[$tm,$node];
258 167141         288417 push @{$res[-1]},_nodeMessage($opt{attr},$tm+$opt{tmoffset},$node);
  167141         612283  
259 167141         729079 $node->increment(\$tm,\$slack,\$buffer);
260 167141         621877 $opt{attr}->push();
261 167141         573017 $opt{attr}->log($tm);
262 167141 100       806783 if($tm-$tension{slack}*$slack+rand($tension{buffer}*$buffer+$tension{slack}*$slack)<=$opt{goal}) {
    100          
263 147508   66     571475 $node=$node->nextrandom(not=>$conclusion,tm=>$tm,attr=>$opt{attr}{attr})//$node->nextrandom(tm=>$tm,attr=>$opt{attr}{attr}) }
264 18729         35532 elsif($node->hasnext($conclusion)) { $node=$conclusion }
265 904   33     3587 else { $node=$node->nextrandom(not=>$conclusion,tm=>$tm,attr=>$opt{attr}{attr})//$node->nextrandom(tm=>$tm,attr=>$opt{attr}{attr}) }
266 167141         515563 $opt{attr}->pop();
267             }
268 18839 50 33     81620 if($node&&($node eq $conclusion)) {
269 18839         57830 push @res,[$tm,$conclusion];
270 18839         38245 push @{$res[-1]},_nodeMessage($opt{attr},$tm+$opt{tmoffset},$conclusion);
  18839         76592  
271 18839         80324 $conclusion->increment(\$tm,\$slack,\$buffer);
272 18839         36873 $node=undef;
273             }
274 18839         69778 $opt{attr}->pop();
275             return (
276 18839         160088 steps =>\@res,
277             tm =>$tm,
278             slack =>$slack,
279             buffer=>$buffer,
280             );
281             }
282              
283             sub scheduler {
284 18875     18875 0 107289 my (%opt)=@_; # goal,node,config
285 18875 50       55829 if(!is_hashref($opt{node})) { die 'scheduler called with invalid node' }
  0         0  
286 18875 50       54923 if(!defined($opt{node}{finish})) { die 'scheduler called with non-activity node' }
  0         0  
287 18875   100     89383 $opt{retries}//=10; $opt{retries}--;
  18875         36984  
288 18875 100 50     45299 if($opt{retries}<0) { die $opt{error}//'scheduling retries exhausted' }
  36         22925  
289             #
290             my %path=findpath(
291             start =>$opt{node},
292             finish =>$opt{node}{finish},
293             goal =>$opt{goal},
294             retries =>$opt{retries},
295             backtracks=>2*$opt{retries},
296             attr =>$opt{attr},
297             tmoffset =>$opt{tmoffset},
298             tensionslack =>$opt{tensionslack} //$opt{tension},
299             tensionbuffer=>$opt{tensionbuffer}//$opt{tension},
300 18839   66     149452 );
      66        
301 18839 50 0     56735 if($path{retry}) { return scheduler(%opt,retries=>$opt{retries},error=>$path{error}//'Retries exhausted') }
  0         0  
302 18839         34714 my @res=@{$path{steps}};
  18839         61872  
303 18839         63648 my ($tm,$slack,$buffer)=@path{qw/tm slack buffer/};
304 18839 50       86014 if($res[-1][1] ne $opt{node}{finish}) { return scheduler(%opt,retries=>$opt{retries},error=>q|Didn't reach finish node|) }
  0         0  
305             #
306 18839         45715 my $excess=$tm-$opt{goal};
307 18839 100       52798 if(abs($excess)>0.5) {
308 996 100 100     4088 if(($excess>0)&&($excess>$slack)) { return scheduler(%opt,retries=>$opt{retries},error=>"Excess exceeds slack ($excess>$slack)") }
  391         3529  
309 605 100 100     2127 if(($excess<0)&&(-$excess>$buffer)) { return scheduler(%opt,retries=>$opt{retries},error=>'Shortage exceeds buffer ('.(-$excess).">$buffer)") }
  10         51  
310 595         1438 my ($reduction,$rate)=(0);
311 595 100       1316 if($excess>0) { $rate=$excess/$slack }
  438         897  
312 157         370 else { $rate=$excess/$buffer }
313 595         2278 foreach my $entry (@res[0..$#res]) {
314 7170         12922 $$entry[0]=$$entry[0]-$reduction;
315 7170         10328 my $dt;
316 7170 100       12173 if($excess>0) { $dt=$rate*($$entry[1]->slack()) }
  5511         12122  
317 1659         3297 else { $dt=$rate*($$entry[1]->buffer()) }
318 7170         14252 $reduction+=$dt;
319             }
320             }
321 18438         65926 foreach my $i (0..$#res) {
322 173489         237932 my $dt;
323 173489 100       300954 if($i<$#res) { $dt=$res[$i+1][0]-$res[$i][0] }
  155051         286715  
324 18438         36841 else { $dt=$opt{goal}-$res[$i][0] }
325 173489         379523 $res[$i][3]=$res[$i][4]=0;
326 173489   50     375162 $dt-=$res[$i][1]{tmavg}//0;
327 173489 100       324120 if($dt>0) { $res[$i][4]=$dt }
  1350         2347  
328 172139         321116 else { $res[$i][3]=-$dt }
329             }
330             #
331             # Message selection occurs in _nodeMessage during path construction.
332             # Message attributes apply during path construction to permit node
333             # filtering, but final materialization occurs after slack/buffer
334             # adjustments have been made.
335             #
336             # Both nodes and their messages may change attributes, but node
337             # attributes are applied first, so message attributes will "win" if
338             # both contain 'set' operations. Documented in "/Precedence".
339             #
340 18438         50164 foreach my $i (0..$#res) {
341 173489         319224 my $node=$res[$i][1];
342 173489 100       374618 if($$node{attributes}) {
343 112701         172499 while(my ($k,$v)=each %{$$node{attributes}}) {
  307453         953767  
344 194752         735518 $opt{attr}->change($k,%$v,tm=>$res[$i][0]+$opt{tmoffset}) } }
345 173489         482518 my ($message,$msg)=$res[$i][2]->random();
346 173489         804013 $res[$i][1]=Schedule::Activity::Node->new(%$node,message=>$message);
347 173489 50       463507 if(is_hashref($msg)) { while(my ($k,$v)=each %{$$msg{attributes}}) {
  173489         280243  
  173555         588212  
348 66         276 $opt{attr}->change($k,%$v,tm=>$res[$i][0]+$opt{tmoffset});
349             } }
350             }
351 18438         168752 return @res;
352             }
353              
354             sub goalScheduling {
355 146     146 0 530 my ($self,%opt)=@_;
356 146         290 my %goal=%{delete($opt{goal})};
  146         728  
357 146 50       587 if(!is_hashref($goal{attribute})) { return (error=>'goal{attribute} must be hash') }
  0         0  
358 146         321 { my $attr=$self->_attr();
  146         475  
359 146         452 my %validOp=map {$_=>undef} (qw/min max eq ne/);
  584         1717  
360 146         453 my %valueOp=map {$_=>undef} (qw/eq ne/);
  292         829  
361 146         325 foreach my $k (keys %{$goal{attribute}}) {
  146         625  
362 176 50       704 if(!defined($$attr{attr}{$k})) { return (error=>"goal-requested attribute does not exist: $k") }
  0         0  
363 176 50       628 if(!is_hashref($goal{attribute}{$k})) { return (error=>"goal attribute $k must be a hash") }
  0         0  
364 176 50       635 if(!defined($goal{attribute}{$k}{op})) { return (error=>"missing operator in goal $k") }
  0         0  
365 176 50 50     843 if(!exists($validOp{$goal{attribute}{$k}{op}//''})) { return (error=>"invalid operator in goal $k") }
  0         0  
366 176 50 66     1247 if(exists($valueOp{$goal{attribute}{$k}{op}})&&!defined($goal{attribute}{$k}{value})) { return (error=>"missing value in goal $k") }
  0         0  
367             }
368             }
369 146   50     533 my $cycles=$goal{cycles}//10;
370 146         296 my %schedule;
371 146         308 eval { %schedule=$self->schedule(%opt) };
  146         584  
372 146         394 my ($bestscore,%best);
373             my $notemerge=sub {
374 16048 100   16048   65069 if(!defined($schedule{annotations})) { return }
  13210         30500  
375 2838         5391 my %seen;
376 2838         4893 my @activities=@{$schedule{activities}};
  2838         11399  
377 2838         5759 foreach my $group (sort {$a cmp $b} keys(%{$schedule{annotations}})) {
  0         0  
  2838         11515  
378 2838 50       9490 if($seen{$group}) { next }
  0         0  
379 2838 50       9538 if(!defined($schedule{annotations}{$group})) { next }
  0         0  
380 2838         5839 push @activities,@{$schedule{annotations}{$group}{events}};
  2838         8324  
381 2838         8069 $seen{$group}=1;
382             }
383 2838 50       8796 if(%seen) {
384 2838         12737 @activities=sort {$$a[0]<=>$$b[0]} @activities;
  115411         188856  
385 2838         12402 %{$schedule{attributes}}=$self->computeAttributes(@activities);
  2838         21849  
386             }
387 146         1160 };
388             my $score=sub {
389 16048     16048   34583 my $res=-1e6;
390 16048 100       51203 if(!defined($schedule{attributes})) { return $res }
  1         3  
391 16047         29136 $res=0;
392 16047         27699 foreach my $k (keys %{$goal{attribute}}) {
  16047         72006  
393 31195         50016 my %cmp=%{$goal{attribute}{$k}};
  31195         113897  
394 31195   50     57742 my %attr=%{$schedule{attributes}{$k}//{}};
  31195         139798  
395 31195         61391 my $avg;
396 31195 50 33     104934 if($$self{PNA}&&($k=~/^\Q$$self{PNA}\E/)) { $avg=$attr{y}//0 }
  0   0     0  
397 31195   50     81881 else { $avg=$attr{avg}//0 }
398 31195   100     118069 my $weight=$cmp{weight}//1;
399 31195 100       94664 if ($cmp{op} eq 'max') { $res+=$avg*$weight }
  26916 100       95214  
    100          
    50          
    0          
400 466         2097 elsif($cmp{op} eq 'min') { $res-=$avg*$weight }
401 1159         7770 elsif($cmp{op} eq 'eq') { $res-=abs($avg-$cmp{value})*$weight }
402 2654         15634 elsif($cmp{op} eq 'ne') { $res+=abs($avg-$cmp{value})*$weight }
403             elsif($cmp{op} eq 'XX') {
404 0   0     0 my $xy=$attr{xy}//[];
405 0         0 foreach my $i (0..$#$xy-1) {
406             $res-=($$xy[1+$i][0]-$$xy[$i][0])*abs(0.5*$$xy[$i][1]+0.5*$$xy[1+$i][1]-$cmp{value})
407 0         0 }
408             }
409             }
410 16047         42196 return $res;
411 146         1098 };
412 146         488 &$notemerge();
413 146         360 $bestscore=&$score(); %best=%schedule;
  146         728  
414 146         448 my $lasterr;
415             #
416 146         504 while(--$cycles) {
417 15921         33839 eval { %schedule=$self->schedule(%opt) };
  15921         77561  
418 15921 100       59487 if($@) { $lasterr=$@; next }
  19         51  
  19         78  
419 15902         60256 &$notemerge();
420 15902         53033 my $s=&$score();
421 15902 100       69350 if($s>$bestscore) {
422 525         1049 $bestscore=$s;
423 525         38408 %best=%schedule;
424             }
425             }
426 146 100 66     843 if(!%best&&$lasterr) { die $lasterr }
  1         52  
427 145         20140 return %best;
428             }
429              
430             sub incrementalScheduling {
431 22     22 0 110 my ($self,%opt)=@_;
432 22         57 my $activities=$opt{activities};
433 22         42 my $i=0;
434 22         48 my %after=();
435 22         40 my %schedule;
436 22         123 while($i<=$#$activities) {
437 44         106 my $j=$i;
438 44   66     303 while(($j<$#$activities)&&(!is_hashref($$activities[$j][2])||!defined($$activities[$j][2]{goal}))) { $j++ }
  22   100     173  
439 44         84 my @acts;
440 44         166 foreach my $activity (@$activities[$i..$j]) {
441 66         226 push @acts,[@$activity[0,1]];
442 66 100       319 if(is_hashref($$activity[2])) { push @{$acts[-1]},{map {$_=>$$activity[2]{$_}} grep {$_ ne 'goal'} keys(%{$$activity[2]})} }
  22         48  
  22         72  
  0         0  
  22         130  
  22         83  
443             }
444 44         319 %schedule=$self->schedule(%opt,%after,activities=>\@acts,goal=>$$activities[$j][2]{goal});
445 44 100       335 if($i<$#$activities) { %after=(after=>{%schedule}) }
  22         121  
446 44         289 $i=1+$j;
447             }
448 22         952 return %schedule;
449             }
450              
451             sub schedule {
452 17160     17160 0 126603 my ($self,%opt)=@_;
453 17160   66     112134 my %check=$self->compile(unsafe=>$opt{unsafe}//$$self{unsafe});
454 17160 100       45391 if($check{error}) { return (error=>$check{error}) }
  4         23  
455 17156 50       49358 if(!is_arrayref($opt{activities})) { return (error=>'Activities must be an array') }
  0         0  
456 17156 100   18686   95733 if(any {is_hashref($$_[2])&&defined($$_[2]{goal})} @{$opt{activities}}) { return $self->incrementalScheduling(%opt) }
  18686 100       113651  
  17156         89230  
  22         157  
457 17134 100 66     103850 if($opt{goal}&&%{$opt{goal}}) { return $self->goalScheduling(%opt) }
  146         782  
  146         687  
458 16988   50     60709 my $tmoffset=$opt{tmoffset}//0;
459 16988         68930 my %res=(stat=>{slack=>0,buffer=>0});
460 16988 100       51584 if($opt{after}) {
461 24         252 delete($$self{attr});
462 24         79 my $attr=$self->_attr();
463 24         55 push @{$$attr{stack}},$opt{after}{_attr};
  24         105  
464 24         88 $attr->pop();
465 24         61 $tmoffset=$opt{after}{_tmmax};
466 24         47 %{$res{stat}}=(%{$res{stat}},%{$opt{after}{stat}});
  24         122  
  24         71  
  24         85  
467             }
468 16988         47588 $self->_attr()->push();
469 16988         28907 foreach my $activity (@{$opt{activities}}) {
  16988         46127  
470 18474         109163 foreach my $entry (scheduler(goal=>$$activity[0],node=>$$self{built}{node}{$$activity[1]},config=>$$self{built},attr=>$self->_attr(),tmoffset=>$tmoffset,tensionslack=>$opt{tensionslack},tensionbuffer=>$opt{tensionbuffer})) {
471 173489         245590 push @{$res{activities}},[$$entry[0]+$tmoffset,@$entry[1..$#$entry]];
  173489         648307  
472 173489         343671 $res{stat}{slack}+=$$entry[3]; $res{stat}{buffer}+=$$entry[4];
  173489         278899  
473 173489         378261 $res{stat}{slackttl}+=$$entry[1]{tmavg}-$$entry[1]{tmmin};
474 173489         378739 $res{stat}{bufferttl}+=$$entry[1]{tmmax}-$$entry[1]{tmavg};
475             }
476 18438         112135 $tmoffset+=$$activity[0];
477             }
478 16952         71820 $self->_attr()->log($tmoffset);
479 16952 100       54517 if($opt{after}) { unshift @{$res{activities}},@{$opt{after}{activities}} }
  24         61  
  24         60  
  24         183  
480 16952         46125 %{$res{attributes}}=$self->_attr()->report();
  16952         54504  
481 16952 100       57016 if(!$opt{nonote}) { while(my ($group,$notes)=each %{$$self{config}{annotations}}) {
  16941         29034  
  19783         92347  
482 2842         5733 my @schedule;
483 2842         7527 foreach my $note (@$notes) {
484 2843         16665 my $annotation=Schedule::Activity::Annotation->new(%$note);
485 2843         6470 foreach my $note ($annotation->annotate(@{$res{activities}})) {
  2843         10636  
486 14169   50     82177 my ($message,$mobj)=Schedule::Activity::Message->new(message=>$$note[1]{message},names=>$$self{config}{messages}//{})->random();
487 14169   50     45950 my %node=(%{$mobj//{}},message=>$message);
  14169         62568  
488 14169 50       43656 if($$note[1]{annotations}) { $node{annotations}=$$note[1]{annotations} }
  0         0  
489 14169         68995 push @schedule,[$$note[0],\%node,@$note[2..$#$note]];
490             }
491             }
492 2842         13527 @schedule=sort {$$a[0]<=>$$b[0]} @schedule;
  19912         37515  
493 2842         9070 for(my $i=0;$i<$#schedule;$i++) { ## no critic (CStyleForLoops)
494 11330 100       35955 if($schedule[$i+1][0]==$schedule[$i][0]) {
495 1         3 splice(@schedule,$i+1,1); $i-- } }
  1         6  
496 2842         14065 $res{annotations}{$group}{events}=\@schedule;
497             } }
498 16952         46280 $self->_attr()->push(); $res{_attr}=pop(@{$$self{attr}{stack}}); # store a copy in {_attr}
  16952         28599  
  16952         69357  
499 16952         52373 $self->_attr()->pop();
500 16952         50036 $res{_tmmax}=$tmoffset;
501 16952         924359 return %res;
502             }
503              
504             sub computeAttributes {
505 2839     2839 0 9796 my ($self,@activities)=@_;
506 2839         9031 $self->_attr()->push();
507 2839         7607 $self->_attr()->reset();
508 2839         11188 foreach my $event (sort {$$a[0]<=>$$b[0]} @activities) {
  79762         128205  
509 48224         96573 my ($tm,$node,$msg)=@$event;
510 48224 100       106610 if($$node{attributes}) {
511 17003         27030 while(my ($k,$v)=each %{$$node{attributes}}) {
  34006         106130  
512 17003         40296 $self->_attr()->change($k,%$v,tm=>$tm) } }
513 48224 100       103072 if(is_hashref($msg)) { while(my ($k,$v)=each %{$$msg{attributes}}) {
  34059         51411  
  34062         104858  
514 3         5 $self->_attr()->change($k,%$v,tm=>$tm);
515             } }
516             }
517 2839         7546 my %res=$self->_attr()->report();
518 2839         8203 $self->_attr()->pop();
519 2839         11857 return %res;
520             }
521              
522             sub loadMarkdown {
523 1     1 0 3641 my ($text)=@_;
524 1         16 my $list=qr/(?:\d+\.|[-*])/;
525 1         3 my (%config,@activities,@siblings,$activity,$tm);
526 1         12 foreach my $line (split(/\n/,$text)) {
527 14 100       30 if($line=~/^\s*$/) { next }
  2         2  
528 12 100       184 if($line=~/^$list\s*(.*)$/) {
    50          
529 3         6 $activity=$1; $tm=0;
  3         4  
530 3 50       12 if($activity=~/(?.*?),\s*(?\d+)(?min|sec)\s*$/) {
531 3 50       12 $activity=$+{name}; $tm=$+{tm}; if($+{unit} eq 'min') { $tm*=60 } }
  3         9  
  3         10  
  3         5  
532 3 50       8 if(defined($config{node}{$activity})) { die "Name conflict: $activity" }
  0         0  
533 3         5 push @activities,[$tm,$activity];
534 3         4 @siblings=();
535 3         12 $config{node}{$activity}={
536             message=>$activity,
537             next=>[],
538             tmavg=>0,
539             finish=>"$activity, conclude",
540             };
541 3         12 $config{node}{"$activity, conclude"}={tmavg=>0};
542             }
543             elsif($line=~/^\s+$list\s*(.*)$/) {
544 9 50       13 if(!$activity) { die 'Action without activity' }
  0         0  
545 9         12 my $action=$1; $tm=60;
  9         32  
546 9 50       24 if($action=~/(?.*?),\s*(?\d+)(?min|sec)\s*$/) {
547 9 50       23 $action=$+{name}; $tm=$+{tm}; if($+{unit} eq 'min') { $tm*=60 } }
  9         20  
  9         24  
  9         11  
548 9         16 $action="$activity, $action";
549 9         8 push @{$config{node}{$activity}{next}},$action;
  9         16  
550 9   33     15 $activities[-1][0]||=$tm;
551 9 50       16 if(defined($config{node}{$action})) { die "Name conflict: $action" }
  0         0  
552 9         24 $config{node}{$action}={
553             message=>$action,
554             next=>[@siblings,"$activity, conclude"],
555             tmavg=>$tm,
556             };
557 9         13 foreach my $sibling (@siblings) { push @{$config{node}{$sibling}{next}},$action }
  9         8  
  9         17  
558 9         12 push @siblings,$action;
559             }
560             }
561 1         9 return (configuration=>\%config,activities=>\@activities);
562             }
563              
564             1;
565              
566             __END__