Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separated the logic of branch and bound search #97

Merged
merged 30 commits into from
Mar 7, 2019

Conversation

Kolaru
Copy link
Collaborator

@Kolaru Kolaru commented Sep 29, 2018

I finally found the courage to update to 0.7/1.0 and to implement the idea proposed by @dpsanders in #72 to separate the logic of the search from the rest of the package. Therefore this PR superseed #72 (I do not update it though, as the scope is quite different).

Changes in this PR:

  • Introduction of a generic customizable branch and bound search interface.
  • Introduction of a tree type as the fundamental data structure for the search. Some advantage come from that as for example pretty printing of the tree. The roots function still return a simple list of intervals.
  • Separation of the branch and bound logic in the rootsearch_iterator.jl file, so the transition to the code in another package can be done easily.
  • Rework of the interface to avoid storing functions in an object (and the kind of akwardly high number of parameters that resulted).
  • Use of Root object during the search rather than interval, which is forced by the genericity of the branch and bound algorithm (status of roots does not exactly match with the status of the elements in a search).
  • Fix of Repeating Newton gives :unknown #29 along the way.

@codecov-io
Copy link

codecov-io commented Sep 29, 2018

Codecov Report

Merging #97 into master will decrease coverage by 3.4%.
The diff coverage is 58.74%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master      #97      +/-   ##
==========================================
- Coverage   59.58%   56.17%   -3.41%     
==========================================
  Files          11       12       +1     
  Lines         532      623      +91     
==========================================
+ Hits          317      350      +33     
- Misses        215      273      +58
Impacted Files Coverage Δ
src/IntervalRootFinding.jl 5.55% <ø> (ø) ⬆️
src/branch_and_bound.jl 38.82% <38.82%> (ø)
src/roots.jl 72.88% <85%> (-8.16%) ⬇️
src/contractors.jl 95.55% <94.44%> (-1.95%) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update becade4...864a337. Read the comment docs.

@dpsanders
Copy link
Member

This looks great, thanks a lot!

I am a bit worried about the performance implications of using a tree like that. Have you done any benchmarks?

In general, I think we should try to leave as general as possible the data structures used, so you could specify that you wanted to store the nodes in a tree, or a vector as before, etc. How possible would that be?

@Kolaru
Copy link
Collaborator Author

Kolaru commented Sep 29, 2018

So I quickly added some benchmark using the Smiley and Chun examples introduced by @gwater and the PkgBenchmark.jl package (this PR is necessary to run its basics tasks though). Here are the results:

Master:

ID time GC time memory allocations
["Smiley", "Krawczyk", "Smiley and Chun (2001), Example 2.2"] 12.457 ms (5%) 8.41 MiB (1%) 250556
["Smiley", "Krawczyk", "Smiley and Chun (2001), Example 5.2"] 215.253 ms (5%) 24.078 ms 115.33 MiB (1%) 3433780
["Smiley", "Krawczyk", "Smiley and Chun (2001), Example 5.4"] 9.902 ms (5%) 7.51 MiB (1%) 219406
["Smiley", "Newton", "Smiley and Chun (2001), Example 2.2"] 12.983 ms (5%) 8.76 MiB (1%) 259155
["Smiley", "Newton", "Smiley and Chun (2001), Example 5.2"] 255.685 ms (5%) 28.522 ms 136.83 MiB (1%) 4044788
["Smiley", "Newton", "Smiley and Chun (2001), Example 5.4"] 12.438 ms (5%) 9.32 MiB (1%) 271491

This PR:

ID time GC time memory allocations
["Smiley", "Krawczyk", "Smiley and Chun (2001), Example 2.2"] 12.540 ms (5%) 8.46 MiB (1%) 252695
["Smiley", "Krawczyk", "Smiley and Chun (2001), Example 5.2"] 226.340 ms (5%) 27.452 ms 116.05 MiB (1%) 3450366
["Smiley", "Krawczyk", "Smiley and Chun (2001), Example 5.4"] 9.968 ms (5%) 7.52 MiB (1%) 220525
["Smiley", "Newton", "Smiley and Chun (2001), Example 2.2"] 13.686 ms (5%) 8.81 MiB (1%) 261324
["Smiley", "Newton", "Smiley and Chun (2001), Example 5.2"] 285.322 ms (5%) 37.126 ms 137.49 MiB (1%) 4064908
["Smiley", "Newton", "Smiley and Chun (2001), Example 5.4"] 12.503 ms (5%) 9.35 MiB (1%) 272998

It looks like this PR implies about 5% to 10% penalty (PkgBenchmark comparison is not yet functional for Julia 0.7/1.0, so it's the best estimate I can easily do for now). It may be possible to remove this penalty completely, since the tree structure closely correspond to a branch and bound algorithm.

On the other hand I was expecting way more allocations with this PR. Good surprise I guess.

As for allowing to store the data in arbitrary structure, while designin this PR I considerd that the ability to easily convert the result toward any data structure would be sufficient. It is also the simplest solution from the developper side.

However if the current penalty we pay to keep maximal information about the run of the algorithm is too high (seems reasonnable to me, but I essentially use the package to solve rather simple equations so I may not be representative), it is possible to make it more generic (may become tricky thought).

@dpsanders
Copy link
Member

Wow, that's great! Let me look over this in detail then.

@dpsanders
Copy link
Member

Do you have a simple example to show how to use the new functionality / see the tree etc?

I have been thinking that roots should create a RootsProblem (or whatever) object that stores all the parameters used and all the intermediate results etc.

@Kolaru
Copy link
Collaborator Author

Kolaru commented Oct 2, 2018

I updated the example in the examples dir and put it in a new file (root_search_iterator.jl). It is abundantly commented, so I hope everything is clear.

By default, the intermediate results are not stored in the tree to spare memory. However, one of the example show how to do this by changing the process function.

Also the search object already store all final parameters (final in the sense that the contractor is stored, but not the input parameter such as the original function or if it uses automatic differentiation for example).

@Kolaru Kolaru mentioned this pull request Dec 12, 2018
@dpsanders
Copy link
Member

Sorry, this now has conflicts :/

tuples `(node_id, lvl)` where `lvl` is the depth of the node in the tree.
"""
struct BBTree{DATA}
nodes::Dict{Int, Union{BBNode, BBLeaf{DATA}}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way of avoiding this Union?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to have two dict: one for the branchings (BBNode) and one for the leaves (BBLeaf{DATA}).

@dpsanders
Copy link
Member

I can attempt to rebase this if you would like.

@Kolaru
Copy link
Collaborator Author

Kolaru commented Feb 8, 2019

@dpsanders No need to, thanks. I'll do it soon.

@Kolaru
Copy link
Collaborator Author

Kolaru commented Feb 24, 2019

Below is the benchmark of the current state of this PR against master. For now we see a slowdowns ranging from 5% to 20% in worst cases. I'll try to identify if there is a bottleneck and if I can do something to speed things up.

The decrease in memory usage is a welcomed suprise.


Benchmark Report for IntervalRootFinding

Job Properties

  • Time of benchmarks:
    • Target: 24 Feb 2019 - 01:40
    • Baseline: 24 Feb 2019 - 01:42
  • Package commits:
    • Target: 46dea2
    • Baseline: becade
  • Julia commits:
    • Target: 80516c
    • Baseline: 80516c
  • Julia command flags:
    • Target: None
    • Baseline: None
  • Environment variables:
    • Target: None
    • Baseline: None

Results

A ratio greater than 1.0 denotes a possible regression (marked with ❌), while a ratio less
than 1.0 denotes a possible improvement (marked with ✅). Only significant results - results
that indicate possible regressions or improvements - are shown below (thus, an empty table means that all
benchmark results remained invariant between builds).

ID time ratio memory ratio
["Dietmar-Ratz", "Dietmar-Ratz 1", "Krawczyk"] 1.13 (5%) ❌ 0.21 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 1", "Newton"] 1.11 (5%) ❌ 0.21 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 2", "Newton"] 1.05 (5%) ❌ 1.01 (1%)
["Dietmar-Ratz", "Dietmar-Ratz 3", "Krawczyk"] 1.05 (5%) ❌ 1.03 (1%) ❌
["Dietmar-Ratz", "Dietmar-Ratz 3", "Newton"] 1.08 (5%) ❌ 1.04 (1%) ❌
["Dietmar-Ratz", "Dietmar-Ratz 3", "Slope expansion"] 1.06 (5%) ❌ 1.00 (1%)
["Dietmar-Ratz", "Dietmar-Ratz 4", "Automatic differentiation"] 1.05 (5%) ❌ 1.00 (1%)
["Dietmar-Ratz", "Dietmar-Ratz 4", "Krawczyk"] 1.05 (5%) ❌ 0.98 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 4", "Newton"] 1.04 (5%) 0.96 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 5", "Krawczyk"] 1.18 (5%) ❌ 0.19 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 5", "Newton"] 1.14 (5%) ❌ 0.19 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 6", "Krawczyk"] 1.04 (5%) 0.90 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 6", "Newton"] 1.04 (5%) 0.94 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 7", "Automatic differentiation"] 1.06 (5%) ❌ 1.00 (1%)
["Dietmar-Ratz", "Dietmar-Ratz 7", "Krawczyk"] 1.12 (5%) ❌ 0.33 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 7", "Newton"] 1.09 (5%) ❌ 0.33 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 9", "Krawczyk"] 1.09 (5%) ❌ 0.18 (1%) ✅
["Dietmar-Ratz", "Dietmar-Ratz 9", "Newton"] 1.10 (5%) ❌ 0.18 (1%) ✅
["Rastigrin stationary points", "Krawczyk"] 1.06 (5%) ❌ 1.00 (1%)
["Rastigrin stationary points", "Newton"] 1.09 (5%) ❌ 1.01 (1%)
["Smiley", "Smiley and Chun (2001), Example 5.2", "Krawczyk"] 1.07 (5%) ❌ 1.01 (1%)
["Smiley", "Smiley and Chun (2001), Example 5.2", "Newton"] 1.09 (5%) ❌ 1.01 (1%)
["Smiley", "Smiley and Chun (2001), Example 5.4", "Newton"] 1.06 (5%) ❌ 1.01 (1%)

Benchmark Group List

Here's a list of all the benchmark groups executed by this job:

  • ["Dietmar-Ratz", "Dietmar-Ratz 1"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 2"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 3"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 4"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 5"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 6"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 7"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 8"]
  • ["Dietmar-Ratz", "Dietmar-Ratz 9"]
  • ["Linear equations", "n = 10"]
  • ["Linear equations", "n = 2"]
  • ["Linear equations", "n = 5"]
  • ["Rastigrin stationary points"]
  • ["Smiley", "Smiley and Chun (2001), Example 2.2"]
  • ["Smiley", "Smiley and Chun (2001), Example 5.2"]
  • ["Smiley", "Smiley and Chun (2001), Example 5.4"]

Julia versioninfo

Target

Julia Version 1.1.0
Commit 80516ca202 (2019-01-21 21:24 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
      Microsoft Windows [version 10.0.17763.316]
  CPU: Intel(R) Core(TM) i5-6600K CPU @ 3.50GHz: 
              speed         user         nice          sys         idle          irq
       #1  3504 MHz   41728312            0     24420343    250501062      4959093  ticks
       #2  3504 MHz   42282593            0     17158515    257208390       409375  ticks
       #3  3504 MHz   61348546            0     15767125    239533843       290093  ticks
       #4  3504 MHz   47695703            0     16460234    252493578       311796  ticks
       
  Memory: 15.95343017578125 GB (8507.94140625 MB free)
  Uptime: 622735.0 sec
  Load Avg:  0.0  0.0  0.0
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, skylake)

Baseline

Julia Version 1.1.0
Commit 80516ca202 (2019-01-21 21:24 UTC)
Platform Info:
  OS: Windows (x86_64-w64-mingw32)
      Microsoft Windows [version 10.0.17763.316]
  CPU: Intel(R) Core(TM) i5-6600K CPU @ 3.50GHz: 
              speed         user         nice          sys         idle          irq
       #1  3504 MHz   41786609            0     24435312    250581875      4961656  ticks
       #2  3504 MHz   42332593            0     17169453    257301531       409421  ticks
       #3  3504 MHz   61406390            0     15776750    239620453       290281  ticks
       #4  3504 MHz   47762937            0     16471140    252569515       311968  ticks
       
  Memory: 15.95343017578125 GB (8257.20703125 MB free)
  Uptime: 622889.0 sec
  Load Avg:  0.0  0.0  0.0
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.1 (ORCJIT, skylake)

@Kolaru
Copy link
Collaborator Author

Kolaru commented Mar 3, 2019

It seems like most of the overhead comes from the creation of the tree and the associated dictionnaries. However due to the profiler sometime crashing on Windows (JuliaLang/julia#30902) I have not yet been able to do extensive profiling.

Also note that the build failures only concern Julia 0.7.

@dpsanders
Copy link
Member

I would say that we should just remove support for Julia 0.7 and merge this.

@Kolaru
Copy link
Collaborator Author

Kolaru commented Mar 4, 2019

Since the implementation is accepted, I can now do the tedious but necessary secondary work (add tests, check the examples are still valid and write some beautiful documentation). Hopefully I will be able to do it soon and then it will be ready for merging.

@Kolaru
Copy link
Collaborator Author

Kolaru commented Mar 6, 2019

@dpsanders This is now ready.

@dpsanders
Copy link
Member

Could you please update REQUIRE to put Julia 1.0 as the lower bound.
Or change the settings on your fork to allow me to modify your PR.

@Kolaru
Copy link
Collaborator Author

Kolaru commented Mar 7, 2019

@dpsanders Done. I've also remove 0.7 build from travis and appveyor.

@dpsanders
Copy link
Member

This looks really great, thanks!

I think the best thing to do is merge once green and then iterate later if necessary.

@dpsanders
Copy link
Member

I think some of the printing etc. could probably be simplified using https://github.com/Keno/AbstractTrees.jl

I'm not sure if there are any ready-made packages for trees that could simplify some of the tree logic itself.

@dpsanders
Copy link
Member

OK I'm going to merge for now.

Thanks a lot!

@dpsanders dpsanders merged commit a58edad into JuliaIntervals:master Mar 7, 2019
@Kolaru
Copy link
Collaborator Author

Kolaru commented Mar 7, 2019

@dpsanders When I started working on this PR I checked for such packages. The closer which seems available are networks from packages such as LightGraphs.jl. However I preferred to implement my own tree type, as networks are more general and I was a bit worried about performance because of that.

I'll look into simplifying some part of this with AbstractTree.jl once I've dealt with my other pending PR.

@Kolaru Kolaru deleted the separated_branch_and_bound branch March 7, 2019 23:57
@dpsanders
Copy link
Member

Yes, true, LightGraphs.jl has a lot of relevant functionality. There's no rush in any case!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants