@@ -341,57 +341,18 @@ function verify_solution(sol::Vector{Int}, graph::Graph)
341341 return true
342342end
343343
344+
344345"""
345- Push the given solution to a local optimium if needed: keeps increasing
346- the states of the given solution as long as no constraints are violated.
347- It also removes unnecessary parts of the solution which are unconnected
348- to the required packages.
346+ Uninstall unreachable packages:
347+ start from the required ones and keep only the packages reachable from them along the graph.
349348"""
350- function enforce_optimality ! (sol:: Vector{Int} , graph:: Graph )
349+ function _uninstall_unreachable ! (sol:: Vector{Int} , why :: Vector{Union{Symbol,Int} } , graph:: Graph )
351350 np = graph. np
352351 spp = graph. spp
353352 gadj = graph. gadj
354353 gmsk = graph. gmsk
355354 gconstr = graph. gconstr
356- pkgs = graph. data. pkgs
357355
358- # keep a track for the log
359- why = Union{Symbol,Int}[0 for p0 = 1 : np]
360-
361- restart = true
362- while restart
363- restart = false
364- for p0 = 1 : np
365- s0 = sol[p0]
366- s0 == spp[p0] && (why[p0] = :uninst ; continue ) # the package is not installed
367-
368- # check if bumping to the higher version would violate a constraint
369- gconstr[p0][s0+ 1 ] || (why[p0] = :constr ; continue )
370-
371- # check if bumping to the higher version would violate a constraint
372- viol = false
373- for (j1,p1) in enumerate (gadj[p0])
374- s1 = sol[p1]
375- msk = gmsk[p0][j1]
376- if ! msk[s1, s0+ 1 ]
377- viol = true
378- why[p0] = p1
379- break
380- end
381- end
382- viol && continue
383-
384- # So the solution is non-optimal: we bump it manually
385- sol[p0] += 1
386- restart = true
387- end
388- end
389-
390- # Finally uninstall unneeded packages:
391- # start from the required ones and keep only
392- # the packages reachable from them along the graph.
393- # (These should have been removed in the previous step, but in principle
394- # an unconnected yet self-sustaining cycle may have survived.)
395356 uninst = trues (np)
396357 staged = Set {Int} (p0 for p0 = 1 : np if ! gconstr[p0][end ])
397358 seen = copy (staged)
@@ -400,7 +361,7 @@ function enforce_optimality!(sol::Vector{Int}, graph::Graph)
400361 staged_next = Set {Int} ()
401362 for p0 in staged
402363 s0 = sol[p0]
403- @assert s0 < spp[p0]
364+ s0 == spp[p0] && continue
404365 uninst[p0] = false
405366 for (j1,p1) in enumerate (gadj[p0])
406367 gmsk[p0][j1][end ,s0] && continue # the package is not required by p0 at version s0
@@ -415,6 +376,173 @@ function enforce_optimality!(sol::Vector{Int}, graph::Graph)
415376 sol[p0] = spp[p0]
416377 why[p0] = :uninst
417378 end
379+ end
380+
381+ """
382+ Push the given solution to a local optimum if needed: keeps increasing
383+ the states of the given solution as long as no constraints are violated.
384+ It might also install additional packages, if needed to bump the ones already
385+ installed.
386+ It also removes unnecessary parts of the solution which are unconnected
387+ to the required packages.
388+ """
389+ function enforce_optimality! (sol:: Vector{Int} , graph:: Graph )
390+ np = graph. np
391+ spp = graph. spp
392+ gadj = graph. gadj
393+ gmsk = graph. gmsk
394+ gconstr = graph. gconstr
395+ pkgs = graph. data. pkgs
396+
397+ # keep a track for the log
398+ why = Union{Symbol,Int}[0 for p0 = 1 : np]
399+
400+ # Strategy:
401+ # There's a cycle in which first the unnecessary (unconnected) packages are removed,
402+ # then we make a pass over the whole packages trying to bump each of them.
403+ # We repeat the above two steps until no further action is allowed.
404+ # When attempting to bump a package, we may attempt to bump or install other packages
405+ # if needed. Except if the bump would uninstall a package, in which cases we don't
406+ # touch anything else: we do it only if it has no consequence at all. This strategy
407+ # favors installing packages as needed.
408+ # During the bumping pass, we keep an upper and lower bound for each package, which
409+ # progressively shrink. These are used when adjusting for the effect of an attempted bump.
410+ # The way it's written should ensure that no package is ever downgraded (unless it was
411+ # originally unneeded, and then got removed, and later reinstalled to a lower version as
412+ # a consequence of a bump of some other package).
413+
414+ # move_up is used to keep track of which packages can move up
415+ # (they start installed and can be bumped) and which down (they start uninstalled and
416+ # can be installed)
417+ move_up = BitVector (undef, length (sol))
418+ # lower and upper bounds on the valid range of each package
419+ upperbound = similar (spp)
420+ lowerbound = similar (spp)
421+ # backup space for restoring the state if an attempted bump fails
422+ bk_sol = similar (sol)
423+ bk_lowerbound = similar (lowerbound)
424+ bk_upperbound = similar (upperbound)
425+
426+ # auxiliary sets to perform breadth-first search on the graph
427+ seen = Set {Int} ()
428+ staged = Set {Int} ()
429+ staged_next = Set {Int} ()
430+
431+ old_sol = similar (sol) # to detect if we made any changes
432+ allsols = Set {Vector{Int}} () # used to make 100% sure we avoid infinite loops
433+
434+ while true
435+ copy! (old_sol, sol)
436+ push! (allsols, copy (sol))
437+
438+ # step 1: uninstall unneded packages
439+ _uninstall_unreachable! (sol, why, graph)
440+
441+ # setp 2: try to bump each installed package in turn
442+
443+ move_up .= sol .≠ spp
444+ copy! (upperbound, spp)
445+ let move_up = move_up
446+ lowerbound .= [move_up[p0] ? sol[p0] : 1 for p0 = 1 : np]
447+ end
448+
449+ for p0 = 1 : np
450+ s0 = sol[p0]
451+ s0 == spp[p0] && (why[p0] = :uninst ; continue ) # the package is not installed
452+ move_up[p0] || continue # the package is only installed as a result of a previous bump, skip it
453+
454+ @assert upperbound[p0] == spp[p0]
455+
456+ # pick the next version that doesn't violate a constraint (if any)
457+ bump_range = collect (s0+ 1 : spp[p0])
458+ bump = let gconstr = gconstr
459+ findfirst (v0-> gconstr[p0][v0], bump_range)
460+ end
461+
462+ # no such version was found, skip this package
463+ bump ≡ nothing && (why[p0] = :constr ; continue )
464+
465+ # assume that we will succeed in bumping the version (otherwise we
466+ # roll-back at the end)
467+
468+ new_s0 = bump_range[bump]
469+ try_uninstall = new_s0 == spp[p0] # are we trying to uninstall a package?
470+
471+ copy! (bk_sol, sol)
472+ copy! (bk_lowerbound, lowerbound)
473+ copy! (bk_upperbound, upperbound)
474+ sol[p0] = new_s0
475+
476+ # if we're trying to uninstall, the bump is "soft": we don't update the
477+ # lower bound so that the package can be reinstalled later in the pass
478+ # if needed by another package
479+ try_uninstall || (lowerbound[p0] = new_s0) # note that we're in the move_up case
480+
481+ empty! (seen)
482+ empty! (staged)
483+ empty! (staged_next)
484+ push! (staged, p0)
485+
486+ while ! isempty (staged)
487+ for f0 in staged
488+ for (j1,f1) in enumerate (gadj[f0])
489+ f1 == p0 && continue
490+ f1 ∈ seen && continue
491+ s1 = sol[f1]
492+ msk = gmsk[f0][j1]
493+ if try_uninstall
494+ bump_range = [s1] # when uninstalling, no further changes are allowed
495+ else
496+ lb1 = lowerbound[f1]
497+ ub1 = upperbound[f1]
498+ @assert lb1 ≤ s1 ≤ ub1
499+ if move_up[f1]
500+ s1 > lb1 && @assert s1 == spp[f1]
501+ # the arrangement of the range gives precedence to improving the
502+ # current situation, but allows reinstalling a package if needed
503+ bump_range = vcat (s1: ub1, s1- 1 : - 1 : lb1)
504+ else
505+ bump_range = collect (ub1: - 1 : lb1)
506+ end
507+ end
508+ bump = let gconstr = gconstr
509+ findfirst (v1-> (gconstr[f1][v1] && msk[v1, sol[f0]]), bump_range)
510+ end
511+ if bump ≡ nothing
512+ why[p0] = f1 # TODO : improve this? (ideally we might want the path from p0 to f1)
513+ @goto abort
514+ end
515+ new_s1 = bump_range[bump]
516+ sol[f1] = new_s1
517+ new_s1 == s1 && continue
518+ push! (staged_next, f1)
519+ if move_up[f1]
520+ lowerbound[f1] = new_s1
521+ else
522+ upperbound[f1] = new_s1
523+ end
524+ end
525+ end
526+ union! (seen, staged)
527+ staged, staged_next = staged_next, staged
528+ empty! (staged_next)
529+ end
530+
531+ # if we're here the bump was successful, there's nothing more to do
532+ continue
533+
534+ # # abort the bumping: restore the solution
535+ @label abort
536+
537+ copy! (sol, bk_sol)
538+ copy! (lowerbound, bk_lowerbound)
539+ copy! (upperbound, bk_upperbound)
540+ end
541+ sol ≠ old_sol || break
542+ # It might be possible in principle to contrive a situation in which
543+ # the solutions oscillate
544+ sol ∈ allsols && break
545+ end
418546
419547 @assert verify_solution (sol, graph)
420548
0 commit comments