diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..06fd450243217f4db170db1c64876470590cddcb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+.vscode
+.DS_Store
+
+exp/ICASSP/*.txt
+
+# Julia's .gitignore
+Manifest.toml
+*.jl.cov
+*.jl.*.cov
+*.jl.mem
+/docs/build/
+/docs/Manifest.toml
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..0b6a1b677be0dd6e6dd97760d40ff749fc3b30bb
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2021 Guyard Theo
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Project.toml b/Project.toml
new file mode 100644
index 0000000000000000000000000000000000000000..fcd9fed7b2b1d01cedcca1c8e2d876d0de901d0f
--- /dev/null
+++ b/Project.toml
@@ -0,0 +1,5 @@
+[deps]
+CPLEX = "a076750e-1247-5638-91d2-ce28b192dca0"
+Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
+JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
+StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91"
diff --git a/README.md b/README.md
index 93681b263161d3155879badd4704e4e0dd28ca7c..842383736e0cb1dabd8c9d46ee578b7c282c200b 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,72 @@
-# BnB screening
+# Branch-screening tests for the L0-penalized least-square problem
 
+This repository contains numerical procedures linked to the paper
+
+**> ADD CITATION**
+
+If you encounter a bug or something unexpected, please let me know by raising an issue on the project page or by contacting me by email at `theo.guyard@insa-rennes.fr`.
+
+## Requirements
+
+This repository works with Julia v1.5+. Please refer to the [Julia download page](https://julialang.org/downloads/) for install instructions.
+
+Dependencies :
+* [JuMP.jl](https://github.com/jump-dev/JuMP.jl)
+* [CPLEX.jl](https://github.com/jump-dev/CPLEX.jl)
+* [Distributions.jl](https://github.com/JuliaStats/Distributions.jl)
+* [StatsBase](https://github.com/JuliaStats/StatsBase.jl)
+
+Our code works with the CPLEX solver through [CPLEX.jl](https://github.com/jump-dev/CPLEX.jl). You cannot use CPLEX without having purchased and installed a copy of CPLEX Optimization Studio from [IBM](https://www.ibm.com). However, CPLEX is available for free to [academics and students](https://community.ibm.com/community/user/datascience/blogs/xavier-nodet1/2020/07/09/cplex-free-for-students). We recommend to follow carefully the install instructions described at [CPLEX.jl](https://github.com/jump-dev/CPLEX.jl).
+
+## Install instructions
+
+1. Clone the repository
+```bash
+git clone https://gitlab.insa-rennes.fr/Theo.Guyard/bnb-screening.git
+```
+
+2. Enter in the project folder
+```bash
+cd bnb-screening
+```
+
+3. Ensure that Julia v1.5+ is installed
+```bash
+julia -v
+> julia version 1.5.3 (or higher)
+```
+
+4. Install dependencies using [Julia's Pkg](https://docs.julialang.org/en/v1/stdlib/Pkg/) in REPL mode
+```julia
+(@v1.5) pkg> activate .
+(@v1.5) pkg> add JuMP
+(@v1.5) pkg> add CPLEX
+(@v1.5) pkg> add Distributions
+(@v1.5) pkg> add StatsBase
+```
+
+## ICASSP experiments
+
+Experiments presented in the paper corresponding to this repository are located in the folder `bnb-screening/exp/ICASSP/`. The available experiment are 
+* `performances.jl` corresponding to table 1
+
+Run experiments directly from the project root folder :
+```bash
+julia --project=. -t 1 exp/ICASSP/performances.jl
+```
+
+The `--project=.` argument installs package dependencies if needed. The flag `-t 1` restricts computations to only 1 CPU core in order to avoid bias due to parallelization capabilities. Logs are saved in the same folder as `performances.jl`. You can edit the experimental setup directly in the script, which is well commented.
+
+## Running demo examples
+
+The `example` folder contains portions of code explaining how to use our package.
+
+## Licence
+
+This software is distributed under the [MIT Licence](https://mit-license.org).
+
+## Cite this work
+
+If you use this package for your own work, please consider citing it as :
+
+**ADD CITATION**
diff --git a/examples/solve_bnb.jl b/examples/solve_bnb.jl
new file mode 100644
index 0000000000000000000000000000000000000000..d08ade7c26090f19a3cbbfbd3309b2ca4a9bff3d
--- /dev/null
+++ b/examples/solve_bnb.jl
@@ -0,0 +1,39 @@
+include("../src/BnbScreening.jl")
+
+# ---------------------------------------------------------------------------- #
+# Example : Solving a L0-penalized least-square problem using a BnB algorithm  #
+# ---------------------------------------------------------------------------- #
+
+# Generate problem data (gaussian setup or toeplitz setup)
+k, m, n, snr = 5, 500, 300, 10*log10(10)
+A, y, λ, M = data_toeplitz(k,m,n,snr)
+#k, m, n, snr = 5, 500, 1000, 10*log10(10)
+#A, y, λ, M = data_gaussian(k,m,n,snr)
+
+# BnB parameters
+bnbparams = BnbParams(
+    lb_method=ACTIVESET,
+    # Relaxation solving method
+    #   - ACTIVESET : the relaxation is solved using an Active-Set algorithm
+    #   - LBMIP : the relaxation is solved using CPLEX convex solver
+    ub_method=BVLS,
+    # Heuristic method
+    #   - BVLS : the heuristic is solved using the Bounded-Variable least-square algorithm
+    #   - LBMIP : the heuristic is solved using CPLEX convex solver
+    maxtime=1000,           # Maximum solution time allowed
+    tol=1e-8,               # Integer tolerance, ie. |x|<=tol <=> x=0
+    maxiter=1000,           # Maximum number of iterations allowed in the Active-Set method
+    screening=false,        # Toogle branch-screening rules
+    verbosity=true,         # Toogle verbosity
+    showevery=1,            # If verbosity=true, number of nodes between two displays
+)
+
+# Solve the problem using the BnB algorithm
+result_bnb = solve_bnb(A,y,λ,M,bnbparams=BnbParams(verbosity=false))
+
+# Display BnB results and statistics
+println("BnB")
+println("  Status    : $(result_bnb.termination_status)")
+println("  Objective : $(result_bnb.objective_value)")
+println("  Nodes     : $(result_bnb.node_count)")
+println("  Timer     : $(result_bnb.solve_time)")
\ No newline at end of file
diff --git a/examples/solve_mip.jl b/examples/solve_mip.jl
new file mode 100644
index 0000000000000000000000000000000000000000..4630f50e81c704c6472a5b73f0d907964fad585d
--- /dev/null
+++ b/examples/solve_mip.jl
@@ -0,0 +1,27 @@
+include("../src/BnbScreening.jl")
+
+# ---------------------------------------------------------------------------- #
+# Example : Solving a L0-penalized least-square problem using a MIP solver     #
+# ---------------------------------------------------------------------------- #
+
+# Generate problem data (gaussian setup or toeplitz setup)
+k, m, n, snr = 5, 500, 300, 10*log10(10)
+A, y, λ, M = data_toeplitz(k,m,n,snr)
+#k, m, n, snr = 5, 500, 1000, 10*log10(10)
+#A, y, λ, M = data_gaussian(k,m,n,snr)
+
+# MIP solver parameters
+mipparams = MipParams(
+    maxtime = 1000,         # Maximum solution time in seconds
+    silent = false,         # Toogle solver verbosity
+)
+
+# Solve the problem using a MIP solver
+result_mip = solve_mip(A,y,λ,M)
+
+# Display solver results and statistics
+println("MIP")
+println("  Status    : $(result_mip.termination_status)")
+println("  Objective : $(result_mip.objective_value)")
+println("  Nodes     : $(result_mip.node_count)")
+println("  Timer     : $(result_mip.solve_time)")
\ No newline at end of file
diff --git a/exp/ICASSP/performances.jl b/exp/ICASSP/performances.jl
new file mode 100644
index 0000000000000000000000000000000000000000..a668323127ebfc72914f268073a2a38a95fd2071
--- /dev/null
+++ b/exp/ICASSP/performances.jl
@@ -0,0 +1,171 @@
+# ICASSP experient - Performances
+
+include("../../src/BnbScreening.jl")
+using StatsBase
+
+################################ Configuration #################################
+
+# This is the only part of code you may have to edit
+
+setup = "toeplitz"          # Setup to use : "gaussian" or "toeplitz"
+k = 7                       # Sparsity level
+m, n = 500, 300            # Dictionnary dimension
+snr = 10. * log10(10.)      # SNR of the observation noise
+maxtime = 1000              # Maximum solution time (in second) allowed
+maxiter = 1000              # Maximum Active-Set iterations allowed per relaxation resolution
+tol = 1.e-8                 # Integer tolerance (ie, |x|<tol <=> x=0)
+repeats = 100               # Number of repeats in the experiment
+
+
+################################## Experiment ##################################
+
+function precompilation(
+    setup::String,
+    k::Int,
+    m::Int,
+    n::Int,
+    snr::Float64,
+    maxtime::Int,
+    maxiter::Int,
+    tol::Float64,
+    )
+    println("Precomiling methods ...")
+    try
+        if setup == "gaussian"
+            A, y, λ, M = data_gaussian(k,m,n,snr)
+        elseif setup == "toeplitz"
+            A, y, λ, M = data_toeplitz(k,m,n,snr)
+        else
+            error("Parameter `setup` must be either \"gaussian\" or \"toeplitz\".")
+        end
+        result_mip = solve_mip(A,y,λ,M,mipparams=MipParams(maxtime=maxtime))
+        result_bnb = solve_bnb(A,y,λ,M,bnbparams=BnbParams(maxtime=maxtime,maxiter=maxiter,tol=tol,verbosity=false))
+        result_bnbs = solve_bnb(A,y,λ,M,bnbparams=BnbParams(maxtime=maxtime,maxiter=maxiter,tol=tol,verbosity=false,screening=true))
+    catch e
+        e == InterruptException() && rethrow(e)
+        println("Precompilation failed with the following error")
+        throw(e)
+    end
+    return nothing
+end
+
+logfile_name = string(
+    @__DIR__,
+    "/",
+    Dates.format(Dates.now(),"yyyy-mm-dd-HH:MM:SS"),
+    ".txt"
+    )
+logfile = open(logfile_name, "w")
+
+println("--------------------------------")
+println("ICASSP experiment - Performances\n")
+println("Configuration")
+println("  setup   : $(setup)")
+println("  k       : $(k)")
+println("  m, n    : $(m), $(n)")
+println("  snr     : $(snr)")
+println("  maxtime : $(maxtime)")
+println("  maxiter : $(maxiter)")
+println("  tol     : $(tol)")
+println("  repeats : $(repeats)")
+println()
+
+write(logfile,"ICASSP experiment - Performances\n\n")
+write(logfile,"Configuration\n")
+write(logfile,"  setup   : $(setup)\n")
+write(logfile,"  k       : $(k)\n")
+write(logfile,"  m, n    : $(m), $(n)\n")
+write(logfile,"  snr     : $(snr)\n")
+write(logfile,"  maxtime : $(maxtime)\n")
+write(logfile,"  maxiter : $(maxiter)\n")
+write(logfile,"  tol     : $(tol)\n")
+write(logfile,"  repeats : $(repeats)\n")
+
+fail_mip = Vector{Union{Bool,Missing}}(missing,repeats)
+fail_bnb = Vector{Union{Bool,Missing}}(missing,repeats)
+fail_bnbs = Vector{Union{Bool,Missing}}(missing,repeats)
+node_mip = Vector{Union{Int64,Missing}}(missing,repeats)
+node_bnb = Vector{Union{Int64,Missing}}(missing,repeats)
+node_bnbs = Vector{Union{Int64,Missing}}(missing,repeats)
+time_mip = Vector{Union{Float64,Missing}}(missing,repeats)
+time_bnb = Vector{Union{Float64,Missing}}(missing,repeats)
+time_bnbs = Vector{Union{Float64,Missing}}(missing,repeats)
+
+precompilation(setup,k,m,n,snr,maxtime,maxiter,tol)
+
+for i in 1:repeats
+    println("Repeat $i/$repeats")
+    try
+        # Generate data
+        if setup == "gaussian"
+            A, y, λ, M = data_gaussian(k,m,n,snr)
+        elseif setup == "toeplitz"
+            A, y, λ, M = data_toeplitz(k,m,n,snr)
+        end
+
+        # Run the tree methods
+        result_mip = solve_mip(A,y,λ,M,mipparams=MipParams(maxtime=maxtime))
+        result_bnb = solve_bnb(A,y,λ,M,bnbparams=BnbParams(maxtime=maxtime,maxiter=maxiter,tol=tol,verbosity=false))
+        result_bnbs = solve_bnb(A,y,λ,M,bnbparams=BnbParams(maxtime=maxtime,maxiter=maxiter,tol=tol,verbosity=false,screening=true))
+
+        # Gather results
+        if result_mip.termination_status == MOI.OPTIMAL
+            fail_mip[i] = false
+            node_mip[i] = result_mip.node_count
+            time_mip[i] = result_mip.solve_time
+        else
+            fail_mip[i] = true
+        end
+        if result_bnb.termination_status == MOI.OPTIMAL
+            fail_bnb[i] = false
+            node_bnb[i] = result_bnb.node_count
+            time_bnb[i] = result_bnb.solve_time
+        else
+            fail_bnb[i] = true
+        end
+        if result_bnbs.termination_status == MOI.OPTIMAL
+            fail_bnbs[i] = false
+            node_bnbs[i] = result_bnbs.node_count
+            time_bnbs[i] = result_bnbs.solve_time
+        else
+            fail_bnbs[i] = true
+        end
+    catch e
+        e == InterruptException() && rethrow(e)
+        println("warning : Repeat $i failed with the following error")
+        println(e)
+    end
+end
+
+
+################################### Results ####################################
+
+println()
+println("Fails")
+println("  MIP  : $(100 * mean(skipmissing(fail_mip)))%")
+println("  BnB  : $(100 * mean(skipmissing(fail_bnb)))%")
+println("  SBnB : $(100 * mean(skipmissing(fail_bnbs)))%")
+println("Node count")
+println("  MIP  : $(mean(skipmissing(node_mip)))")
+println("  BnB  : $(mean(skipmissing(node_bnb)))")
+println("  SBnB : $(mean(skipmissing(node_bnbs)))")
+println("Solve time")
+println("  MIP  : $(round(mean(skipmissing(time_mip)),digits=3)) seconds")
+println("  BnB  : $(round(mean(skipmissing(time_bnb)),digits=3)) seconds")
+println("  SBnB : $(round(mean(skipmissing(time_bnbs)),digits=3)) seconds")
+println("\n--------------------------------")
+
+write(logfile,"\n")
+write(logfile,"Fails\n")
+write(logfile,"  MIP  : $(100 * mean(skipmissing(fail_mip)))%\n")
+write(logfile,"  BnB  : $(100 * mean(skipmissing(fail_bnb)))%\n")
+write(logfile,"  SBnB : $(100 * mean(skipmissing(fail_bnbs)))%\n")
+write(logfile,"Node count\n")
+write(logfile,"  MIP  : $(mean(skipmissing(node_mip)))\n")
+write(logfile,"  BnB  : $(mean(skipmissing(node_bnb)))\n")
+write(logfile,"  SBnB : $(mean(skipmissing(node_bnbs)))\n")
+write(logfile,"Solve time\n")
+write(logfile,"  MIP  : $(round(mean(skipmissing(time_mip)),digits=3)) seconds\n")
+write(logfile,"  BnB  : $(round(mean(skipmissing(time_bnb)),digits=3)) seconds\n")
+write(logfile,"  SBnB : $(round(mean(skipmissing(time_bnbs)),digits=3)) seconds\n")
+close(logfile)
\ No newline at end of file
diff --git a/src/BnbScreening.jl b/src/BnbScreening.jl
new file mode 100644
index 0000000000000000000000000000000000000000..6c47a7bd21d88039a3cd5f810f3f169dc3299a67
--- /dev/null
+++ b/src/BnbScreening.jl
@@ -0,0 +1,25 @@
+# Standard libraries
+using Dates
+using Printf
+using Random
+
+# External libraries
+using CPLEX
+using Distributions
+using JuMP
+using LinearAlgebra
+
+version() = "v1.0"
+author() = "Theo Guyard"
+contact() = "theo.guyard@insa-rennes.fr"
+license() = "MIT License (https://mit-license.org)"
+
+# Module files
+include("types.jl")
+include("activeset.jl")
+include("bnb.jl")
+include("bvls.jl")
+include("data.jl")
+include("displays.jl")
+include("mips.jl")
+include("screen.jl")
\ No newline at end of file
diff --git a/src/activeset.jl b/src/activeset.jl
new file mode 100644
index 0000000000000000000000000000000000000000..5365243c4520bbfc1ae40ca148b1df33ed1c48c6
--- /dev/null
+++ b/src/activeset.jl
@@ -0,0 +1,520 @@
+tolsign(x::Float64, tol::Float64) = sign(x) * (abs(x) > tol)
+
+function symbi_app(Ainv::Matrix{Float64}, a::Vector{Float64})
+    Ainv_updated = Matrix{Float64}(undef,length(a),length(a))
+    @views begin
+        d = a[1:end-1]
+        g = a[end]
+        dw = BLAS.symv('U',Ainv,-d)
+        gw = 1. / (g + dw' * d)
+        gw_dw = gw .* dw
+        BLAS.syr!('U',gw,dw,Ainv)
+        Ainv_updated[1:end-1,1:end-1] = Ainv
+        Ainv_updated[1:end-1,end] = gw_dw
+        Ainv_updated[end,end] = gw
+    end
+    return Ainv_updated
+end
+
+function symbi_rem(Ainv::Matrix{Float64}, i::Int)
+    n = size(Ainv)[1]
+    Ainv_updated = Matrix{Float64}(undef,n-1,n-1)
+    @views begin
+        q = -1. / Ainv[i,i]
+        c1 = 1:i-1
+        r1 = Ainv[c1,i]
+        c2 = i+1:n
+        r2 = Ainv[i,c2]
+        c2red = i:n-1
+        Ainv_updated[c1,c1] = BLAS.syr!('U',q,r1,Ainv[c1,c1])
+        Ainv_updated[c1,c2red] = BLAS.ger!(q,r1,r2,Ainv[c1,c2])
+        Ainv_updated[c2red,c2red] = BLAS.syr!('U',q,r2,Ainv[c2,c2])
+    end
+    return Ainv_updated
+end
+
+function symbi_ro!(Ainv::Matrix{Float64}, r::Vector{Float64}, g::Float64)
+    Ainv_r = BLAS.symv('U',Ainv,r)
+    c = -g / (1. + g * (r' * Ainv_r))
+    BLAS.syr!('U',c,Ainv_r,Ainv)
+    return nothing
+end
+
+function swap_Sbarin_to_Sbar0!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    i::Int
+    )
+    iSbarin = findfirst(j->j==i,node.support.Sbarin)
+    node.GSbarin_inv = symbi_rem(node.GSbarin_inv,iSbarin)
+    push!(node.support.Sbar0,i)
+    deleteat!(node.support.Sbarin,iSbarin)
+    deleteat!(asviews.xSbarin,iSbarin)
+    deleteat!(asviews.sSbarin,iSbarin)
+    deleteat!(asviews.xnewSbarin,iSbarin)
+    asviews.ASbarin = A[:,node.support.Sbarin]
+    return nothing
+end
+
+function swap_Sbarin_to_Sbarbox!(node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    i::Int
+    )
+    iSbarin = findfirst(j->j==i,node.support.Sbarin)
+    node.GSbarin_inv = symbi_rem(node.GSbarin_inv,iSbarin)
+    push!(node.support.Sbarbox,i)
+    push!(asviews.xSbarbox,popat!(asviews.xSbarin,iSbarin))
+    push!(asviews.sSbarbox,popat!(asviews.sSbarin,iSbarin))
+    deleteat!(asviews.xnewSbarin,iSbarin)
+    deleteat!(node.support.Sbarin,iSbarin)
+    asviews.ASbarin = A[:,node.support.Sbarin]
+    asviews.ASbarbox = A[:,node.support.Sbarbox]
+    return nothing
+end
+
+function swap_S1in_to_S1box!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64}, 
+    i::Int
+    )
+    iS1in = findfirst(j->j==i,node.support.S1in)
+    push!(node.support.S1box,i)
+    push!(asviews.xS1box,popat!(asviews.xS1in,iS1in))
+    push!(asviews.sS1box,popat!(asviews.sS1in,iS1in))
+    deleteat!(asviews.xnewS1in,iS1in)
+    deleteat!(node.support.S1in,iS1in)
+    asviews.AS1in = A[:,node.support.S1in]
+    asviews.AS1box = A[:,node.support.S1box]
+
+    @views begin
+        a = A[:,i]
+        d = vcat(node.GS1in_inv[1:(iS1in-1),iS1in], node.GS1in_inv[iS1in,(iS1in+1):end])
+        e = node.GS1in_inv[iS1in,iS1in]
+        Ad = asviews.AS1in * d
+        r1 = (Ad./e) + a
+        BLAS.syr!('U',e,r1,B)
+        node.GS1in_inv = symbi_rem(node.GS1in_inv,iS1in)
+        symbi_ro!(node.GSbarin_inv, asviews.ASbarin'*r1, e)
+    end
+    
+    return nothing
+end
+
+function swap_Sbar0_to_Sbarin!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64}, 
+    i::Int
+    )
+    @views begin  
+        a = A[:,i]
+        Ba = BLAS.symv('U',B,a)
+        ABa = asviews.ASbarin' * Ba
+        aBa = a' * Ba
+        v = vcat(ABa,aBa)
+        node.GSbarin_inv = symbi_app(node.GSbarin_inv,v)
+    end
+    push!(node.support.Sbarin,i)
+    filter!(j->j!=i,node.support.Sbar0)
+    push!(asviews.xSbarin, 0.)
+    push!(asviews.sSbarin, 0.)
+    push!(asviews.xnewSbarin, 0.)
+    asviews.ASbarin = A[:,node.support.Sbarin]
+    return nothing
+end
+
+function swap_Sbarbox_to_Sbarin!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64}, 
+    i::Int
+    )
+    iSbarbox = findfirst(j->j==i,node.support.Sbarbox)
+    @views begin
+        a = A[:,i]
+        Ba = BLAS.symv('U',B,a)
+        ABa = asviews.ASbarin' * Ba
+        aBa = a' * Ba
+        v = vcat(ABa,aBa)
+        node.GSbarin_inv = symbi_app(node.GSbarin_inv,v)
+    end
+    push!(node.support.Sbarin, popat!(node.support.Sbarbox,iSbarbox))
+    push!(asviews.xSbarin, popat!(asviews.xSbarbox,iSbarbox))
+    push!(asviews.sSbarin, popat!(asviews.sSbarbox,iSbarbox))
+    push!(asviews.xnewSbarin, asviews.xSbarin[end])
+    asviews.ASbarin = A[:,node.support.Sbarin]
+    asviews.ASbarbox = A[:,node.support.Sbarbox]
+    return nothing
+end
+
+function swap_S1box_to_Sbarin!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64}, 
+    i::Int
+    )
+    iS1box = findfirst(j->j==i,node.support.S1box)
+    @views begin
+        a = A[:,i]
+        Ba = BLAS.symv('U',B,a)
+        ABa = asviews.ASbarin' * Ba
+        aBa = a' * Ba
+        v = vcat(ABa,aBa)
+        node.GSbarin_inv = symbi_app(node.GSbarin_inv,v)
+    end
+    push!(node.support.Sbarin, popat!(node.support.S1box,iS1box))
+    push!(asviews.xSbarin, popat!(asviews.xS1box,iS1box))
+    push!(asviews.sSbarin, popat!(asviews.sS1box,iS1box))
+    push!(asviews.xnewSbarin, asviews.xSbarin[end])
+    asviews.ASbarin = A[:,node.support.Sbarin]
+    asviews.AS1box = A[:,node.support.S1box]
+    return nothing
+end
+
+function swap_Sbar_to_S0!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    i::Int
+    )
+    iSbarin = findfirst(j->j==i,node.support.Sbarin)
+    if !isa(iSbarin,Nothing)
+        node.GSbarin_inv = symbi_rem(node.GSbarin_inv,iSbarin)
+        deleteat!(node.support.Sbarin,iSbarin)
+        deleteat!(asviews.xSbarin,iSbarin)
+        deleteat!(asviews.sSbarin,iSbarin)
+        deleteat!(asviews.xnewSbarin,iSbarin)
+        asviews.ASbarin = A[:,node.support.Sbarin]
+    else
+        iSbarbox = findfirst(j->j==i,node.support.Sbarbox)
+        if !isa(iSbarbox,Nothing)
+            deleteat!(node.support.Sbarbox,iSbarbox)
+            deleteat!(asviews.xSbarbox,iSbarbox)
+            deleteat!(asviews.sSbarbox,iSbarbox)
+            asviews.ASbarbox = A[:,node.support.Sbarbox]
+        else
+            filter!(j->j!=i,node.support.Sbar0)
+        end
+    end
+    return nothing
+end
+
+function swap_Sbar_to_S1!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64}, 
+    i::Int
+    )
+    iSbarbox = findfirst(j->j==i,node.support.Sbarbox)
+    if !isa(iSbarbox,Nothing)
+        push!(node.support.S1box, popat!(node.support.Sbarbox,iSbarbox))
+        push!(asviews.xS1box, popat!(asviews.xSbarbox,iSbarbox))
+        push!(asviews.sS1box, popat!(asviews.sSbarbox,iSbarbox))
+        asviews.ASbarbox = A[:,node.support.Sbarbox]
+        asviews.AS1box = A[:,node.support.S1box]
+    else
+        @views begin
+            asviews.AS1in = A[:,node.support.S1in]
+            a = A[:,i]
+            Ata = asviews.AS1in' * a
+            ata = a' * a
+            d = - BLAS.symv('U',node.GS1in_inv,Ata)
+            g = -1. / (ata + d'*Ata)
+            r1 = asviews.AS1in * d + a
+            v = vcat(Ata,ata)
+            node.GS1in_inv = symbi_app(node.GS1in_inv,v)
+            BLAS.syr!('U',g,r1,B)
+        end
+        iSbarin = findfirst(j->j==i,node.support.Sbarin)
+        if !isa(iSbarin,Nothing)
+            node.GSbarin_inv = symbi_rem(node.GSbarin_inv,iSbarin)
+            push!(node.support.S1in, popat!(node.support.Sbarin,iSbarin))
+            push!(asviews.xS1in, popat!(asviews.xSbarin,iSbarin))
+            push!(asviews.sS1in, popat!(asviews.sSbarin,iSbarin))
+            push!(asviews.xnewS1in, popat!(asviews.xnewSbarin,iSbarin))
+            asviews.ASbarin = A[:,node.support.Sbarin]
+            asviews.AS1in = A[:,node.support.S1in]
+            symbi_ro!(node.GSbarin_inv, asviews.ASbarin'*r1,g)
+        else
+            iSbar0 = findfirst(j->j==i,node.support.Sbar0)
+            push!(node.support.S1in, popat!(node.support.Sbar0,iSbar0))
+            push!(asviews.xS1in, 0.)
+            push!(asviews.sS1in, 0.)
+            push!(asviews.xnewS1in, 0.)
+            asviews.AS1in = A[:,node.support.S1in]
+            symbi_ro!(node.GSbarin_inv, asviews.ASbarin'*r1,g)
+        end
+    end
+    return nothing
+end
+
+function solve_kkt_system!(
+    node::Node,
+    asviews::ActivesetViews,
+    B::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64
+    )
+    Axbox = asviews.ASbarbox * asviews.xSbarbox + asviews.AS1box * asviews.xS1box
+    r = y - Axbox
+    rhsSbarin = asviews.ASbarin' * BLAS.symv('U',B,r) - (λ/M) .* asviews.sSbarin
+    asviews.xnewSbarin = BLAS.symv('U',node.GSbarin_inv,rhsSbarin)
+    rhsS1in = asviews.AS1in' * (r - asviews.ASbarin * asviews.xnewSbarin)
+    asviews.xnewS1in = BLAS.symv('U',node.GS1in_inv,rhsS1in)
+    return Axbox
+end
+
+function discrete_line_search!(
+    node::Node,
+    asviews::ActivesetViews,
+    A::Matrix{Float64},
+    Axbox::Vector{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    bnbparams::BnbParams,
+    )
+
+    # TODO : optimize
+    
+    xdiffSbarin = asviews.xnewSbarin - asviews.xSbarin
+    xdiffS1in = asviews.xnewS1in - asviews.xS1in
+    Ax = asviews.ASbarin * asviews.xSbarin + asviews.AS1in * asviews.xS1in + Axbox
+    Axdiff = asviews.ASbarin * xdiffSbarin + asviews.AS1in * xdiffS1in
+    u = y - Ax
+    λ_M = λ / M
+    λ_S1 = λ * length(node.branch.S1)
+
+    a = Axdiff' * Axdiff
+    b_base = u' * Axdiff
+
+    if a == 0.
+        Atu = A' * u
+        lb = 0.5*u'*u + λ_M*asviews.xSbarin'*asviews.sSbarin + λ_S1
+        return u, Atu, lb
+    end
+
+    tflip = []
+    tmax  = 1.
+    for i in 1:length(node.support.Sbarin)
+        if xdiffSbarin[i] < 0.
+            tmax = min(tmax, -(M+asviews.xSbarin[i])/xdiffSbarin[i])
+        else
+            tmax = min(tmax, (M-asviews.xSbarin[i])/xdiffSbarin[i])
+        end
+        push!(tflip, -asviews.xSbarin[i]/xdiffSbarin[i])
+    end
+    for i in 1:length(node.support.S1in)
+        if xdiffS1in[i] < 0.
+            tmax = min(tmax, -(M+asviews.xS1in[i])/xdiffS1in[i])
+        else
+            tmax = min(tmax, (M-asviews.xS1in[i])/xdiffS1in[i])
+        end
+        push!(tflip, -asviews.xS1in[i]/xdiffS1in[i])
+    end
+
+    filter!(ti -> 0. < ti < tmax, sort!(unique!(tflip)))
+    pushfirst!(tflip,0.)
+    push!(tflip,tmax)
+
+    topt = 0.
+    uopt = u
+    vopt = Inf
+    
+    for i in 1:(length(tflip)-1)
+
+        tl   = tflip[i]
+        tu   = tflip[i+1]
+        tmed = (tl+tu)/2.
+
+        thetaSbarin = sign.(asviews.xSbarin + tmed .* xdiffSbarin)
+
+        b = b_base - λ_M * thetaSbarin' * xdiffSbarin
+
+        tinf = max(tl,min(tu,b/a))
+        uinf = u - tinf .* Axdiff
+        vinf = 0.5 * norm(uinf,2)^2 + λ_M * norm(asviews.xSbarin + tinf .* xdiffSbarin,1)
+
+        if vinf < vopt
+            topt = tinf
+            uopt = uinf
+            vopt = vinf
+        end
+    end
+
+    asviews.xSbarin += topt .* xdiffSbarin
+    asviews.xS1in += topt .* xdiffS1in
+    asviews.sSbarin = tolsign.(asviews.xSbarin,bnbparams.tol)
+    asviews.sS1in = tolsign.(asviews.xS1in,bnbparams.tol)
+    Atuopt = A' * uopt
+    lb = vopt + (λ_S1 + λ * sum(node.support.Sbarbox))
+
+    return uopt, Atuopt, lb
+end
+
+function detect_blocking_behaviour(swapped::Vector{Int}, imax::Int)
+    return (length(swapped)==1) & (imax in swapped)
+end
+
+function active_optimality(
+    asviews::ActivesetViews,
+    M::Float64,
+    bnbparams::BnbParams
+    )
+    cond = (
+        all(asviews.sSbarin .== tolsign.(asviews.xnewSbarin,bnbparams.tol)) & 
+        all(asviews.sS1in .== tolsign.(asviews.xnewS1in,bnbparams.tol)) &
+        (norm(asviews.xnewSbarin,Inf) - M <= bnbparams.tol) &
+        (norm(asviews.xnewS1in,Inf) - M <= bnbparams.tol) 
+    )
+    return cond 
+end
+
+function inactive_optimality(
+    node::Node,
+    asviews::ActivesetViews,
+    Atu::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    bnbparams::BnbParams
+    )
+    vmax = 0.
+    imax = nothing
+    smax = ""
+    λ_M = λ / M
+    λ_M_m_tol =  λ_M - bnbparams.tol
+    λ_M_p_tol =  λ_M + bnbparams.tol
+    m_tol = -bnbparams.tol
+    for i in node.support.Sbar0
+        v = abs(Atu[i]) - λ_M_p_tol
+        if v > vmax
+            vmax = v
+            imax = i
+            smax = "Sbar0"
+        end
+    end
+    for (iSbarbox,i) in enumerate(node.support.Sbarbox)
+        v = λ_M_m_tol - Atu[i] * asviews.sSbarbox[iSbarbox]
+        if v > vmax
+            vmax = v
+            imax = i
+            smax = "Sbarbox"
+        end
+    end
+    for (iS1box,i) in enumerate(node.support.S1box)
+        v = m_tol - Atu[i] * asviews.sS1box[iS1box]
+        if v > vmax
+            vmax = v
+            imax = i
+            smax = "S1box"
+        end
+    end
+    return imax, smax
+end
+
+function new_tentative_sign!(
+    node::Node, 
+    asviews::ActivesetViews, 
+    imax::Int, 
+    smax::String, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64}
+    )
+    if smax == "Sbar0"
+        swap_Sbar0_to_Sbarin!(node,asviews,A,B,imax)
+    elseif smax == "Sbarbox"
+        swap_Sbarbox_to_Sbarin!(node,asviews,A,B,imax)
+    elseif smax == "S1box"
+        swap_S1box_to_Sbarin!(node,asviews,A,B,imax)
+    end
+    return nothing
+end
+
+function update_sets!(
+    node::Node,
+    asviews::ActivesetViews, 
+    A::Matrix{Float64}, 
+    B::Matrix{Float64},
+    M::Float64,
+    bnbparams::BnbParams
+    )
+
+    swapped = Vector{Int}()
+    for (iS1in,i) in enumerate(node.support.S1in)
+        absxi = abs(asviews.xS1in[iS1in])
+        if abs(M - absxi) <= bnbparams.tol
+            swap_S1in_to_S1box!(node,asviews,A,B,i)
+            push!(swapped,i)
+        end
+    end
+    for (iSbarin,i) in enumerate(node.support.Sbarin)
+        absxi = abs(asviews.xSbarin[iSbarin])
+        if absxi <= bnbparams.tol
+            swap_Sbarin_to_Sbar0!(node,asviews,A,i)
+            push!(swapped,i)
+        elseif abs(M - absxi) <= bnbparams.tol
+            swap_Sbarin_to_Sbarbox!(node,asviews,A,i)
+            push!(swapped,i)
+        end
+    end
+    return swapped
+end
+
+function solve_relax_activeset!(
+    tree::Tree,
+    node::Node,
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    bnbparams::BnbParams,
+    )
+
+    asviews = ActivesetViews(node,A,bnbparams)
+    B = I(size(A)[1]) - BLAS.symm('R','U',node.GS1in_inv,asviews.AS1in) * asviews.AS1in'
+    node.branch_on[2] == 0 && swap_Sbar_to_S0!(node,asviews,A,node.branch_on[1])
+    node.branch_on[2] == 1 && swap_Sbar_to_S1!(node,asviews,A,B,node.branch_on[1])
+
+    it = 0
+    lb = Inf
+
+    while true
+        it += 1
+        it >= bnbparams.maxiter && error("activetset : iteration limit")
+
+        Axbox = solve_kkt_system!(node,asviews,B,y,λ,M)
+        u, Atu, lb = discrete_line_search!(node,asviews,A,Axbox,y,λ,M,bnbparams)
+        swapped = update_sets!(node,asviews,A,B,M,bnbparams)
+
+        if bnbparams.screening
+            prune = screen!(tree,node,asviews,u,Atu,B,A,y,λ,M)
+            prune && break
+        end
+
+        if active_optimality(asviews,M,bnbparams)
+            imax, smax = inactive_optimality(node,asviews,Atu,λ,M,bnbparams)
+            isa(imax,Nothing) && break
+            new_tentative_sign!(node,asviews,imax,smax,A,B)
+            detect_blocking_behaviour(swapped,imax) && break
+        end
+    end
+
+    node.lb = lb
+    node.x[node.support.Sbar0] .= 0.
+    node.x[node.support.Sbarin] = asviews.xSbarin
+    node.x[node.support.Sbarbox] = asviews.xSbarbox
+    node.x[node.support.S1in] = asviews.xS1in
+    node.x[node.support.S1box] = asviews.xS1box
+    
+    return nothing
+end
+
diff --git a/src/bnb.jl b/src/bnb.jl
new file mode 100644
index 0000000000000000000000000000000000000000..ec8819f20831f879dcf801adbb6fbdffdea8833a
--- /dev/null
+++ b/src/bnb.jl
@@ -0,0 +1,191 @@
+function best_lb(tree::Tree)
+    isempty(tree.active) && return 0.
+    return maximum([node.lb for node in tree.active])
+end
+
+function branch!(tree::Tree, node::Node)
+    prune(tree,node) && return nothing
+    j = branching_index(node)
+    isa(j,Nothing) && return nothing
+    node_j0 = Node(node,j,0)
+    node_j1 = Node(node,j,1)
+    push!(tree.queue,node_j0)
+    push!(tree.queue,node_j1)
+end
+
+function branching_index(node::Node)
+    isempty(node.branch.Sbar) && return nothing
+    return node.branch.Sbar[argmax(abs.(node.x[node.branch.Sbar]))]
+end
+
+function depth(node::Node)
+    depth = 0
+    while !isa(node,Nothing)
+        depth += 1
+        node = node.parent
+    end
+    return depth
+end
+
+function elapsed_time(tree::Tree)
+    return (Dates.now() - tree.start_time).value / 1000.
+end
+
+function fixto!(node::Node, j::Int, jval::Int)
+    jpos = findfirst(i->i==j,node.branch.Sbar)
+    isa(jpos,Nothing) && error("Branching index is already fixed")
+    if jval == 0
+        push!(node.branch.S0,popat!(node.branch.Sbar,jpos))
+        node.x[j] = 0.
+    elseif jval == 1
+        push!(node.branch.S1,popat!(node.branch.Sbar,jpos))
+    end
+    return nothing
+end
+
+function global_gap(tree::Tree)
+    return (tree.ub - best_lb(tree)) / tree.ub
+end
+
+function has_perfect_relaxation(node::Node, M::Float64, bnbparams::BnbParams)
+    return !(any(bnbparams.tol .< abs.(node.x[node.branch.Sbar]) .< M - bnbparams.tol))
+end
+
+function lower_bound!(
+    tree::Tree,
+    node::Node,
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    bnbparams::BnbParams
+    )
+    if bnbparams.lb_method == ACTIVESET
+        solve_relax_activeset!(tree,node,A,y,λ,M,bnbparams)
+    elseif bnbparams.lb_method == LBMIP
+        solve_relax_mip!(node,A,y,λ,M)
+    else
+        error("Paramter `bnbparams.lb_method` not understood.")
+    end
+    return nothing
+end
+
+function next_node!(tree::Tree)
+    return pop!(tree.queue)
+end
+
+function processed!(tree::Tree, node::Node)
+    if !prune(tree,node)
+        push!(tree.active,node)
+    end
+    tree.nexpl += 1
+    return nothing
+end
+
+function prune(tree::Tree, node::Node)
+    return (tree.ub < node.lb)
+end
+
+function terminated!(tree::Tree, bnbparams::BnbParams)
+    if isempty(tree.queue)
+        tree.termination_status = MOI.OPTIMAL
+    elseif elapsed_time(tree) > bnbparams.maxtime
+        tree.termination_status = MOI.TIME_LIMIT
+    end
+    return (tree.termination_status != MOI.OPTIMIZE_NOT_CALLED)
+end
+
+function update_upper_bound!(tree::Tree, ub::Float64, inc::Vector{Float64})
+    if ub < tree.ub
+        tree.ub = ub
+        tree.inc = inc
+        return true
+    end
+    return false
+end
+
+function update_active_nodes!(tree::Tree)
+    filter!(node->!prune(tree,node),tree.active)
+    return nothing
+end
+
+function upper_bound!(
+    tree::Tree,
+    node::Node,
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    bnbparams::BnbParams
+    )
+
+    perfect = false
+
+    if isempty(node.branch.S1)
+        ub = 0.5*y'*y
+        inc = zeros(Float64,size(A)[2])
+    elseif has_perfect_relaxation(node,M,bnbparams)
+        ub = node.lb
+        inc = node.x
+        perfect = true
+    else
+        if bnbparams.ub_method == BVLS
+            ub, inc = solve_heuristic_bvls(node,A,y,λ,M,bnbparams)
+        elseif bnbparams.ub_method == UBMIP
+            ub, inc = solve_heuristic_mip(node,A,y,λ,M)
+        else
+            error("Paramter `bnbparams.ub_method` not understood.")
+        end
+    end
+    updated = update_upper_bound!(tree,ub,inc)
+    updated && update_active_nodes!(tree)
+    return perfect
+end
+
+# ============================================================================ #
+
+"""
+    solve_bnb(
+        A::Matrix{Float64}, 
+        y::Vector{Float64}, 
+        λ::Float64, 
+        M::Float64;
+        bnbparams::BnbParams=BnbParams(),
+        )
+
+Solve the L0-penalized least-square problem 
+
+    min_x   (1/2) norm(y-Ax,2)^2 + λ norm(x,0)  
+    st.     norm(x,Inf) <= M
+
+using a Branch-and-Bound algorithm. This algorithm can use branch-screening 
+tests to enhance its efficiency. Returns a [`@BnbResults`](@ref) struct 
+containing the optimization results and statistics. See [`@BnbParams`](@ref) for 
+informations about additional method parameters.
+"""
+function solve_bnb(
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64;
+    bnbparams::BnbParams=BnbParams(),
+    )
+
+    validate_data(A,y,λ,M)
+    tree = Tree(A,y)
+    bnbparams.verbosity && display_head()
+
+    while !terminated!(tree,bnbparams)
+        node = next_node!(tree)
+        lower_bound!(tree,node,A,y,λ,M,bnbparams)
+        perfect = upper_bound!(tree,node,A,y,λ,M,bnbparams)
+        processed!(tree,node)
+        perfect || branch!(tree,node)
+        bnbparams.verbosity && display_node(tree,node)
+    end
+
+    results = BnbResults(tree)
+    bnbparams.verbosity && display_tail(results)
+    
+    return results
+end
\ No newline at end of file
diff --git a/src/bvls.jl b/src/bvls.jl
new file mode 100644
index 0000000000000000000000000000000000000000..b7a3aaad14758555c36746ac0f44cbf40e4c7091
--- /dev/null
+++ b/src/bvls.jl
@@ -0,0 +1,132 @@
+function compute_kkt_optimality(g::Vector{Float64}, on_bound::Vector{Int})
+    length(g) == 0 && return 0.
+    @views begin
+        g_kkt = g .* on_bound
+        free_set = (on_bound .== 0)
+        g_kkt[free_set] = abs.(g[free_set])
+    end
+    return maximum(g_kkt)
+end
+
+function solve_heuristic_bvls(
+    node::Node,
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    bnbparams::BnbParams,
+    )
+
+    AS1 = A[:,node.branch.S1]
+    n = size(AS1)[2]
+    lb = fill(-M,(n,))
+    ub = fill(M,(n,))
+
+    x = zeros(Float64,n)
+    on_bound = zeros(Int,n)
+    free_set = (on_bound .== 0)
+    active_set = .!free_set
+    free_set = findall(free_set)
+
+    r = AS1 * x - y
+    cost = r' * r
+    g = AS1' * r
+    cost_change = Inf
+
+    # ----- Initialization loop ----- #
+
+    while !isempty(free_set)
+
+        AS1_free = AS1[:, free_set]
+        y_free = y - AS1 * (x .* active_set)
+        z = AS1_free \ y_free
+        lbv = (z .< lb[free_set])
+        ubv = (z .> ub[free_set])
+        v = lbv .| ubv
+
+        if any(lbv)
+            ind = free_set[lbv]
+            x[ind] = lb[ind]
+            active_set[ind] .= true
+            on_bound[ind] .= -1
+        end
+        if any(ubv)
+            ind = free_set[ubv]
+            x[ind] = ub[ind]
+            active_set[ind] .= true
+            on_bound[ind] .= 1
+        end
+
+        ind = free_set[.!v]
+        x[ind] = z[.!v]
+        r = AS1 * x - y
+        g = AS1' * r
+        any(v) || break
+        free_set = free_set[.!v]
+    end
+
+    # ----- Main loop ----- #
+
+    optimality = compute_kkt_optimality(g,on_bound)
+
+    while true
+
+        (optimality < bnbparams.tol) && break
+        move_to_free = argmax(g .* on_bound)
+        on_bound[move_to_free] = 0
+        
+        while true
+
+            free_set = (on_bound .== 0)
+            active_set = .!free_set
+            free_set = findall(free_set)
+            x_free = x[free_set]
+            lb_free = lb[free_set]
+            ub_free = ub[free_set]
+            AS1_free = AS1[:,free_set]
+            y_free = y - AS1 * (x .* active_set)
+            z = AS1_free \ y_free
+            lbv = findall(z .< lb_free)
+            ubv = findall(z .> ub_free)
+            v = vcat(lbv,ubv)
+
+            if !isempty(v)
+                
+                alphas = vcat(
+                    lb_free[lbv] - x_free[lbv], 
+                    ub_free[ubv] - x_free[ubv]
+                ) ./ (z[v] - x_free[v])
+                i = argmin(alphas)
+                i_free = v[i]
+                alpha  = alphas[i]
+                x_free *= 1. - alpha
+                x_free += alpha * z
+                x[free_set] = x_free
+
+                if i < length(lbv)
+                    on_bound[free_set[i_free]] = -1
+                else
+                    on_bound[free_set[i_free]] = 1
+                end
+            else
+                x_free = z
+                x[free_set] = x_free
+                break
+            end
+        end
+
+        r = AS1 * x - y
+        cost_new = 0.5 * (r' * r)
+        cost_change = cost - cost_new
+
+        (cost_change < bnbparams.tol * cost) && break
+
+        cost = cost_new
+        g = AS1' * r
+        optimality = compute_kkt_optimality(g,on_bound)
+    end
+
+    obj = 0.5 * norm(r,2)^2 + λ * n
+    
+    return obj, x
+end
\ No newline at end of file
diff --git a/src/data.jl b/src/data.jl
new file mode 100644
index 0000000000000000000000000000000000000000..d1d1f55a3957e4a0cdfcab86d61134aecdee8505
--- /dev/null
+++ b/src/data.jl
@@ -0,0 +1,87 @@
+function validate_data(
+    A::Array{Float64,2}, 
+    y::Array{Float64,1}, 
+    λ::Float64, 
+    M::Float64
+    )
+    size(A)[1] == size(y)[1] || throw(ArgumentError("Dimension missmatch"))
+    λ > 0 || throw(ArgumentError("λ must be positive"))
+    M > 0 || throw(ArgumentError("M must be positive"))
+end
+
+"""
+    data_gaussian(
+        k::Int=5, 
+        m::Int=500, 
+        n::Int=1000, 
+        snr::Float64=10.
+        )
+
+Generate a Gaussian setup.
+"""
+function data_gaussian(
+    k::Int=5, 
+    m::Int=500, 
+    n::Int=1000, 
+    snr::Float64=10.
+    )
+
+    x = zeros(n)
+    a = rand(Normal(0,1),k)
+    a += sign.(a)
+    x[randperm(n)[1:k]] = a
+
+    A = rand(Normal(0,1),m,n)
+    for i in 1:n
+        A[:,i] /= norm(A[:,i],2)
+    end
+
+    σ = norm(A*x,2) / sqrt(m * 10^(snr/10))
+    ϵ = rand(Normal(0,σ),m)
+    y = A * x .+ ϵ
+    λ = 2 * σ * log(n/k - 1)
+    M = 1.5 * norm(A'*y,Inf)
+
+    return A, y, λ, M
+end
+
+"""
+    data_gaussian(
+        k::Int=5, 
+        m::Int=500, 
+        n::Int=1000, 
+        snr::Float64=10.
+        )
+
+Generate a Toeplitz setup.
+"""
+function data_toeplitz(
+    k::Int=5, 
+    m::Int=500, 
+    n::Int=300, 
+    snr::Float64=10.
+    )
+
+    x = zeros(n)
+    a = rand(Normal(0,1),k)
+    a += sign.(a)
+    x[randperm(n)[1:k]] = a
+
+    tmax = n/2
+    t = collect(1:m)
+    A = zeros(m,n)
+    offset = (n-m)/2
+    for i in 1:n
+        A[:,i] = @. sinc((offset + (t - i)) * (tmax/m))
+        A[:,i] /= norm(A[:,i],2)
+    end
+    A += rand(m,n)./10000
+
+    σ = norm(A*x,2) / sqrt(m * 10^(snr/10))
+    ϵ = rand(Normal(0,σ),m)
+    y = A * x .+ ϵ
+    λ = 2 * σ * log(n/k - 1)
+    M = 1.5 * norm(A'*y,Inf)
+
+    return A, y, λ, M
+end
\ No newline at end of file
diff --git a/src/displays.jl b/src/displays.jl
new file mode 100644
index 0000000000000000000000000000000000000000..391dde8faacd9380d404ba754c5bc0467358bca8
--- /dev/null
+++ b/src/displays.jl
@@ -0,0 +1,53 @@
+WIDTH = 67
+
+function display_head()
+    println(repeat("*",WIDTH))
+    println("B&B-screening solver for the L0-penalized least-square problem")
+    println("Version : $(version())")
+    println("Author  : $(author())")
+    println("Contact : $(contact())")
+    println("License : $(license())")
+    println()
+    println(repeat("-",WIDTH))
+    println("|        Node         |        Bounds       |        Branch       |")
+    println("|  nexpl  depth  queue|     lb     ub    gap|     S0     S1   Sbar|")
+    println(repeat("-",WIDTH))
+    return nothing
+end
+    
+function display_node(tree::Tree, node::Node)
+
+    @printf(
+        "|%7d%7d%7d|%7.3f%7.3f%7.0e|%7d%7d%7d|",
+        tree.nexpl,
+        depth(node),
+        length(tree.queue),
+        best_lb(tree),
+        tree.ub,
+        global_gap(tree),
+        length(node.branch.S0),
+        length(node.branch.S1),
+        length(node.branch.Sbar),
+    )
+    println()
+    return nothing
+end
+
+function diplay_results(results::BnbResults)
+    println("B&B results")
+    println("  Status     : $(results.termination_status)")
+    println("  Solve time : $(results.solve_time)")
+    println("  Nodes      : $(results.node_count)")
+    println("  Objective  : $(results.objective_value)")
+    return nothing
+end
+
+function display_tail(results::BnbResults)
+    println(repeat("-",WIDTH))
+    println()
+    diplay_results(results)
+    println()
+    println(repeat("*",WIDTH))
+    return nothing
+end
+
diff --git a/src/mips.jl b/src/mips.jl
new file mode 100644
index 0000000000000000000000000000000000000000..7199214ade0ae0923b596283cfcbfd19eb6f5697
--- /dev/null
+++ b/src/mips.jl
@@ -0,0 +1,117 @@
+function solve_relax_mip!(
+    node::Node,
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    )
+    
+    relax = Model(CPLEX.Optimizer)
+    set_silent(relax)
+
+    gram = A'*A
+    Aty = A'*y
+    
+    n = size(A)[2]
+    @variable(relax, x[1:n])
+    @variable(relax, xabs[1:n])
+    @objective(relax, Min, 
+        0.5*(y'*y) - Aty'*x + (0.5*x)'*(gram*x) + 
+        (λ/M)*sum(xabs[node.branch.Sbar]) + 
+        λ*length(node.branch.S1)
+    )
+    @constraint(relax, box, -M .<= x .<= M)
+    @constraint(relax, abs_pos, xabs .>= x)
+    @constraint(relax, abs_neg, xabs .>= -x)
+    @constraint(relax, setzero, x[node.branch.S0] .== 0)
+    @constraint(relax, abs_setzero, xabs[node.branch.S0] .== 0)
+
+    for i in 1:n
+        set_start_value(x[i], node.x[i])
+        set_start_value(xabs[i], abs(node.x[i]))
+    end
+
+    optimize!(relax)
+
+    node.lb = objective_value(relax)
+    node.x = value.(x)
+end
+
+function solve_heuristic_mip(
+    node::Node,
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    )
+
+    heur = Model(CPLEX.Optimizer)
+    set_silent(heur)
+
+    nS1 = length(node.branch.S1)
+    AS1 = A[:,node.branch.S1]
+    xS1 = node.x[node.branch.S1]
+    
+    @variable(heur, x[1:nS1])
+    @objective(heur, Min, 0.5*(y-AS1*x)'*(y-AS1*x)+ λ*length(node.branch.S1))
+    @constraint(heur, box, -M .<= x .<= M)
+
+    for i in 1:nS1
+        set_start_value(x[i], xS1[i])
+    end
+
+    optimize!(heur)
+
+    inc = zeros(Float64,size(A)[2])
+    inc[node.branch.S1] = value.(x)
+
+    return objective_value(heur), inc
+end
+
+# ============================================================================ #
+
+"""
+    solve_mip(
+        A::Matrix{Float64}, 
+        y::Vector{Float64}, 
+        λ::Float64, 
+        M::Float64;
+        mipparams::MipParams=MipParams(),
+        )
+
+Solve the L0-penalized least-square problem 
+
+    min_x   (1/2) norm(y-Ax,2)^2 + λ norm(x,0)  
+    st.     norm(x,Inf) <= M
+
+using a MIP solver (CPLEX). Returns a [`@MipResults`](@ref) struct containing
+the optimization results and statistics. See [`@MipParams`](@ref) for 
+informations about additional method parameters.
+"""
+function solve_mip(
+    A::Matrix{Float64}, 
+    y::Vector{Float64}, 
+    λ::Float64, 
+    M::Float64;
+    mipparams::MipParams=MipParams(),
+    )
+
+    problem = Model(CPLEX.Optimizer)
+    set_optimizer_attribute(problem,"CPX_PARAM_TILIM",mipparams.maxtime)
+    mipparams.silent && set_silent(problem)
+
+    gram = A'*A
+    Aty  = A'*y
+    
+    n = size(A)[2]
+    @variable(problem, x[1:n])
+    @variable(problem, z[1:n], Bin)
+    @objective(problem, Min, 0.5*(y'*y) - Aty'*x + (0.5*x)'*(gram*x) + λ*sum(z))
+    @constraint(problem, box_lower, -M.*z .<= x)
+    @constraint(problem, box_upper, x .<= M.*z)
+
+    optimize!(problem)
+    result = MipResults(problem)
+
+    return result
+end
diff --git a/src/screen.jl b/src/screen.jl
new file mode 100644
index 0000000000000000000000000000000000000000..569c17dda60efb119b9debce1edf826cbc48464d
--- /dev/null
+++ b/src/screen.jl
@@ -0,0 +1,52 @@
+function screen!(
+    tree::Tree,
+    node::Node,
+    asviews::ActivesetViews,
+    u::Vector{Float64},
+    Atu::Vector{Float64},
+    B::Matrix{Float64},
+    A::Matrix{Float64},
+    y::Vector{Float64},
+    λ::Float64,
+    M::Float64,
+    )
+
+    @views begin
+        pivotSbar = @. M * (abs(Atu[node.branch.Sbar]) - λ/M)
+        pivotSbar0 = max.(pivotSbar,0)
+        pivotSbar1 = max.(-pivotSbar,0)
+        pivotS1 = @. M * (abs(Atu[node.branch.S1]) - λ/M)
+        dobj = 0.5 * (y'y - norm(y-u,2)^2) - sum(pivotSbar0) - sum(pivotS1) 
+    end
+
+    Sbar_to_S0 = Vector{Int}()
+    Sbar_to_S1 = Vector{Int}()
+    for (iSbar,i) in enumerate(node.branch.Sbar)
+        if pivotSbar1[iSbar] > tree.ub - dobj
+            push!(Sbar_to_S0,i)
+            tree.scr0 += 1
+        end
+        if pivotSbar0[iSbar] > tree.ub - dobj
+            push!(Sbar_to_S1,i)
+            tree.scr1 += 1
+        end
+    end
+
+    if !isempty(intersect(Sbar_to_S0,Sbar_to_S1))
+        node.lb = Inf
+        node.x = rand(size(A)[2])
+        return true
+    end
+
+    for i in Sbar_to_S0
+        fixto!(node,i,0)
+        swap_Sbar_to_S0!(node,asviews,A,i)
+    end
+
+    for i in Sbar_to_S1
+        fixto!(node,i,1)
+        swap_Sbar_to_S1!(node,asviews,A,B,i)
+    end
+
+    return false
+end
\ No newline at end of file
diff --git a/src/types.jl b/src/types.jl
new file mode 100644
index 0000000000000000000000000000000000000000..16e48d4a6dbc4243770455188e8a4d647aeeadf3
--- /dev/null
+++ b/src/types.jl
@@ -0,0 +1,295 @@
+# ========================================== #
+# Active-Set support configuration and views #
+# ========================================== #
+
+mutable struct Support
+    Sbar0::Vector{Int}      # Indices verifying 0 = xi      with i in Sbar
+    Sbarin::Vector{Int}     # Indices verifying 0 < xi < M  with i in Sbar
+    Sbarbox::Vector{Int}    # Indices verifying     xi = M  with i in Sbar
+    S1in::Vector{Int}       # Indices verifying     xi < M  with i in S1
+    S1box::Vector{Int}      # Indices verifying     xi = M  with i in S1
+end
+
+mutable struct ActivesetViews
+    # Current iterate
+    xSbarin::Vector{Float64}
+    xSbarbox::Vector{Float64}
+    xS1in::Vector{Float64}
+    xS1box::Vector{Float64}
+
+    # Sign of x
+    sSbarin::Vector{Float64}
+    sSbarbox::Vector{Float64}
+    sS1in::Vector{Float64}
+    sS1box::Vector{Float64}
+
+    # New iterate
+    xnewSbarin::Vector{Float64}
+    xnewS1in::Vector{Float64}
+
+    # Submatrices
+    ASbarin::Matrix{Float64}
+    ASbarbox::Matrix{Float64}
+    AS1in::Matrix{Float64}
+    AS1box::Matrix{Float64}
+end
+
+
+# =========================== #
+# Branch-and-Bound components #
+# =========================== #
+
+mutable struct Branch
+    S0::Vector{Int}     # Indices fixed to zero
+    S1::Vector{Int}     # Indices fixed to non-zero
+    Sbar::Vector{Int}   # Indices not fixed
+end
+
+mutable struct Node
+    parent::Union{Nothing,Node}     # Parent node
+    branch::Branch                  # Current branch (fixed variables)
+    branch_on::Tuple{Int,Int}       # Branching index above node
+    x::Vector{Float64}              # Relaxation solution
+    lb::Float64                     # Relaxation objective value
+
+    # Active set data cashed in each node
+    support::Support
+    GS1in_inv::Matrix{Float64}      
+    GSbarin_inv::Matrix{Float64}
+end
+
+mutable struct Tree
+    termination_status::MOI.TerminationStatusCode   # Optimization status
+    start_time::DateTime                            # Start time
+    nexpl::Int                                      # Number of nodes explored
+    queue::Vector{Node}                             # Nodes to explore
+    active::Vector{Node}                            # Active nodes (with node.lb < tree.ub)
+    ub::Float64                                     # Best upper bound
+    inc::Vector{Float64}                            # Incumbent solution
+    scr0::Int                                       # Indices fixed to 0 with screening tests
+    scr1::Int                                       # Indices fixed to 1 with screening tests
+end
+
+
+# ======================================= #
+# Branch-and-Bound parameters and outputs #
+# ======================================= #
+
+# Available solving methods for the relaxed problem (lower bounding step)
+@enum LbMethod begin
+    LBMIP       # MIP solver
+    ACTIVESET   # Active-Set algorihtm
+end
+
+# Available heuristic methods (upper bounding step)
+@enum UbMethod begin
+    UBMIP   # MIP solver
+    BVLS    # Bounded Variable Least-Square algorithm
+end
+
+struct BnbParams
+    lb_method::LbMethod     # Method used to solve node relaxations
+    ub_method::UbMethod     # Heuristic to compute node uppper bounds
+    maxtime::Int            # Maximum solving time in seconds
+    tol::Float64            # Integer tolerance
+    maxiter::Int            # Max. iter. of the relaxation solution algorithm
+    screening::Bool         # Toogle screening
+    verbosity::Bool         # Whether to enable iterations displays
+    showevery::Int          # Node display interval
+end
+
+struct BnbResults
+    termination_status::MOI.TerminationStatusCode   # Optimization status
+    solve_time::Float64                             # Solution time in seconds
+    node_count::Int                                 # Number of nodes explored
+    objective_value::Float64                        # Objective value (if optimal)
+    x::Vector{Float64}                              # Optimizer (if optimal)
+    scr0::Int                                       # Number of screning tests passed (xi = 0)
+    scr1::Int                                       # Number of screning tests passed (xi != 0)
+end
+
+
+# ================================= #
+# MIP solver parameters and outputs #
+# ================================= #
+
+struct MipParams
+    maxtime::Int    # Maximum solution time in seconds
+    silent::Bool    # Set solver silent
+end
+
+struct MipResults
+    termination_status::MOI.TerminationStatusCode   # Optimization status
+    solve_time::Float64                             # Solution time in seconds
+    node_count::Int                                 # Number of nodes explored
+    objective_value::Float64                        # Objective value (if optimal)
+    x::Vector{Float64}                              # Optimizer (if optimal)
+end
+
+
+# ============ #
+# Constructors #
+# ============ #
+
+function Support(n::Int)
+    return Support(
+        collect(1:n), 
+        Vector{Int}(), 
+        Vector{Int}(), 
+        Vector{Int}(), 
+        Vector{Int}()
+    )
+end
+
+function ActivesetViews(node::Node, A::Matrix{Float64}, bnbparams::BnbParams)
+    xSbarin = node.x[node.support.Sbarin]
+    xSbarbox = node.x[node.support.Sbarbox]
+    xS1in = node.x[node.support.S1in]
+    xS1box = node.x[node.support.S1box]
+    sSbarin = tolsign.(xSbarin, bnbparams.tol)
+    sSbarbox = tolsign.(xSbarbox, bnbparams.tol)
+    sS1in = tolsign.(xS1in, bnbparams.tol)
+    sS1box = tolsign.(xS1box, bnbparams.tol)
+    xnewSbarin = copy(xSbarin)
+    xnewS1in = copy(xS1in)
+    ASbarin = A[:,node.support.Sbarin]
+    ASbarbox = A[:,node.support.Sbarbox]
+    AS1in = A[:,node.support.S1in]
+    AS1box = A[:,node.support.S1box]
+    return ActivesetViews(
+        xSbarin,
+        xSbarbox,
+        xS1in,
+        xS1box,
+        sSbarin,
+        sSbarbox,
+        sS1in,
+        sS1box,
+        xnewSbarin,
+        xnewS1in,
+        ASbarin,
+        ASbarbox,
+        AS1in,
+        AS1box,
+    )
+end
+
+function Branch(n::Int)
+    return Branch(Vector{Int}(), Vector{Int}(), collect(1:n))
+end
+
+function Node(A::Matrix{Float64})
+    m, n = size(A)
+    return Node(
+        nothing, 
+        Branch(n), 
+        (-1,-1), 
+        zeros(Float64,n),
+        0.,
+        Support(n),
+        zeros(Float64,0,0),
+        zeros(Float64,0,0),
+        )
+end
+
+function Node(parent::Node, j::Int, jval::Int)
+    node = Node(
+        parent,
+        copy(parent.branch),
+        (j,jval),
+        copy(parent.x),
+        parent.lb,
+        copy(parent.support),
+        copy(parent.GS1in_inv),
+        copy(parent.GSbarin_inv),
+        )
+    fixto!(node,j,jval)
+    return node
+end
+
+function Tree(A::Array{Float64,2}, y::Vector{Float64})
+    return Tree(
+        MOI.OPTIMIZE_NOT_CALLED,
+        Dates.now(),
+        0,
+        Vector{Node}([Node(A)]),
+        Vector{Node}(),
+        0.5 * norm(y, 2)^2,
+        zeros(Float64,size(A)[2]),
+        0,
+        0,
+    )
+end
+
+function BnbParams(;
+    lb_method=ACTIVESET,
+    ub_method=BVLS,
+    maxtime=1000,
+    tol=1e-8,
+    maxiter=1000,
+    screening=false,
+    verbosity=true,
+    showevery=1,
+    )
+    maxtime > 0 || error("Parameter `maxtime` must be positive")
+    tol > 0 || error("Parameter `tol` must be positive")
+    maxiter > 0 || error("Parameter `maxiter` must be positive")
+    showevery > 0 || error("Parameter `showevery` must be positive")
+    return BnbParams(
+        lb_method,
+        ub_method,
+        maxtime,
+        tol,
+        maxiter,
+        screening,
+        verbosity,
+        showevery
+        )
+end
+
+function BnbResults(tree::Tree)
+    return BnbResults(
+        tree.termination_status,
+        elapsed_time(tree),
+        tree.nexpl,
+        tree.ub,
+        tree.inc,
+        tree.scr0,
+        tree.scr1,
+    )
+end
+
+function MipParams(;maxtime=1000,silent=true)
+    maxtime > 0 || error("Parameter `maxtime` must be positive")
+    return MipParams(maxtime, silent)
+end
+
+function MipResults(problem::JuMP.Model)
+    return MipResults(
+        termination_status(problem),
+        solve_time(problem),
+        convert(Int,node_count(problem)),
+        objective_value(problem),
+        value.(problem[:x])
+    )
+end
+
+
+# ====================== #
+# Base module extensions #
+# ====================== #
+
+
+function Base.copy(support::Support)
+    return Support(
+        copy(support.Sbar0), 
+        copy(support.Sbarin), 
+        copy(support.Sbarbox), 
+        copy(support.S1in), 
+        copy(support.S1box)
+    )
+end
+
+function Base.copy(branch::Branch)
+    return Branch(copy(branch.S0), copy(branch.S1), copy(branch.Sbar))
+end
diff --git a/tests/test.jl b/tests/test.jl
new file mode 100644
index 0000000000000000000000000000000000000000..b204ae1817c4350e9d82e6f74e2db72f90f32257
--- /dev/null
+++ b/tests/test.jl
@@ -0,0 +1,31 @@
+include("../src/BnbScreening.jl")
+
+k, m, n, snr = 5, 500, 300, 10*log10(10)
+A, y, λ, M = data_toeplitz(k,m,n,snr)
+#k, m, n, snr = 5, 500, 1000, 10*log10(10)
+#A, y, λ, M = data_gaussian(k,m,n,snr)
+
+result_mip = solve_mip(A,y,λ,M)
+println("MIP")
+println("  Status    : $(result_mip.termination_status)")
+println("  Objective : $(result_mip.objective_value)")
+println("  Nodes     : $(result_mip.node_count)")
+println("  Timer     : $(result_mip.solve_time)")
+
+result_bnb = solve_bnb(A,y,λ,M,bnbparams=BnbParams(verbosity=false))
+println("BnB")
+println("  Status    : $(result_bnb.termination_status)")
+println("  Objective : $(result_bnb.objective_value)")
+println("  Nodes     : $(result_bnb.node_count)")
+println("  Timer     : $(result_bnb.solve_time)")
+println("Error : $(abs(result_mip.objective_value - result_bnb.objective_value))")
+
+result_bnb_scr = solve_bnb(A,y,λ,M,bnbparams=BnbParams(verbosity=false,screening=true))
+println("BnB + scr")
+println("  Status    : $(result_bnb_scr.termination_status)")
+println("  Objective : $(result_bnb_scr.objective_value)")
+println("  Nodes     : $(result_bnb_scr.node_count)")
+println("  Timer     : $(result_bnb_scr.solve_time)")
+println("  Scr-0     : $(result_bnb_scr.scr0)")
+println("  Scr-1     : $(result_bnb_scr.scr1)")
+println("Error : $(abs(result_mip.objective_value - result_bnb_scr.objective_value))")
\ No newline at end of file