Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Bridges/Constraint/det.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ function bridge_constraint(::Type{LogDetBridge{T}}, model,
end

MOI.supports_constraint(::Type{LogDetBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.LogDetConeTriangle}) where T = true
MOIB.added_constrained_variable_types(::Type{<:LogDetBridge}) = Tuple{DataType}[]
MOIB.added_constraint_types(::Type{LogDetBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.LogDetConeTriangle}) where T = [(MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle), (MOI.VectorAffineFunction{T}, MOI.ExponentialCone), (MOI.ScalarAffineFunction{T}, MOI.LessThan{T})]

"""
Expand Down Expand Up @@ -125,6 +126,7 @@ end

# Attributes, Bridge acting as a model
MOI.get(b::LogDetBridge, ::MOI.NumberOfVariables) = length(b.Δ) + length(b.l)
MOI.get(b::LogDetBridge, ::MOI.ListOfVariableIndices) = [b.Δ; b.l]
MOI.get(b::LogDetBridge{T}, ::MOI.NumberOfConstraints{MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle}) where T = 1
MOI.get(b::LogDetBridge{T}, ::MOI.NumberOfConstraints{MOI.VectorAffineFunction{T}, MOI.ExponentialCone}) where T = length(b.lcindex)
MOI.get(b::LogDetBridge{T}, ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{T}, MOI.LessThan{T}}) where T = 1
Expand Down Expand Up @@ -194,10 +196,12 @@ function bridge_constraint(::Type{RootDetBridge{T}}, model,
end

MOI.supports_constraint(::Type{RootDetBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.RootDetConeTriangle}) where T = true
MOIB.added_constrained_variable_types(::Type{<:RootDetBridge}) = Tuple{DataType}[]
MOIB.added_constraint_types(::Type{RootDetBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.RootDetConeTriangle}) where T = [(MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle), (MOI.VectorAffineFunction{T}, MOI.GeometricMeanCone)]

# Attributes, Bridge acting as a model
MOI.get(b::RootDetBridge, ::MOI.NumberOfVariables) = length(b.Δ)
MOI.get(b::RootDetBridge, ::MOI.ListOfVariableIndices) = b.Δ
MOI.get(b::RootDetBridge{T}, ::MOI.NumberOfConstraints{MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle}) where T = 1
MOI.get(b::RootDetBridge{T}, ::MOI.NumberOfConstraints{MOI.VectorAffineFunction{T}, MOI.GeometricMeanCone}) where T = 1
MOI.get(b::RootDetBridge{T}, ::MOI.ListOfConstraintIndices{MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle}) where T = [b.sdindex]
Expand Down
12 changes: 9 additions & 3 deletions src/Bridges/Constraint/flip_sign.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,17 @@ abstract type FlipSignBridge{
T, S1<:MOI.AbstractSet, S2<:MOI.AbstractSet,
F<:MOI.AbstractFunction, G<:MOI.AbstractFunction} <: AbstractBridge end

function MOI.supports_constraint(::Type{<:FlipSignBridge{T, S1}},
::Type{<:MOI.AbstractFunction},
::Type{S1}) where {T, S1<:MOI.AbstractSet}
function MOI.supports_constraint(
::Type{<:FlipSignBridge{T, S1}}, ::Type{<:MOI.AbstractScalarFunction},
::Type{S1}) where {T, S1<:MOI.AbstractScalarSet}
return true
end
function MOI.supports_constraint(
::Type{<:FlipSignBridge{T, S1}}, ::Type{<:MOI.AbstractVectorFunction},
::Type{S1}) where {T, S1<:MOI.AbstractVectorSet}
return true
end
MOIB.added_constrained_variable_types(::Type{<:FlipSignBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(
::Type{<:FlipSignBridge{T, S1, S2, F}}) where {T, S1, S2, F}
return [(F, S2)]
Expand Down
2 changes: 2 additions & 0 deletions src/Bridges/Constraint/functionize.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ end
MOI.supports_constraint(::Type{ScalarFunctionizeBridge{T}},
::Type{<:MOI.SingleVariable},
::Type{<:MOI.AbstractScalarSet}) where {T} = true
MOIB.added_constrained_variable_types(::Type{<:ScalarFunctionizeBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{ScalarFunctionizeBridge{T, S}}) where {T, S}
return [(MOI.ScalarAffineFunction{T}, S)]
end
Expand Down Expand Up @@ -91,6 +92,7 @@ end
MOI.supports_constraint(::Type{VectorFunctionizeBridge{T}},
::Type{MOI.VectorOfVariables},
::Type{<:MOI.AbstractVectorSet}) where {T} = true
MOIB.added_constrained_variable_types(::Type{<:VectorFunctionizeBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{VectorFunctionizeBridge{T, S}}) where {T, S}
return [(MOI.VectorAffineFunction{T}, S)]
end
Expand Down
2 changes: 2 additions & 0 deletions src/Bridges/Constraint/geomean.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ function MOI.supports_constraint(::Type{GeoMeanBridge{T}},
::Type{MOI.GeometricMeanCone}) where T
return true
end
MOIB.added_constrained_variable_types(::Type{<:GeoMeanBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{GeoMeanBridge{T, F, G}}) where {T, F, G}
return [(F, MOI.LessThan{T}), (G, MOI.RotatedSecondOrderCone)]
end
Expand All @@ -116,6 +117,7 @@ end

# Attributes, Bridge acting as a model
MOI.get(b::GeoMeanBridge, ::MOI.NumberOfVariables) = length(b.xij)
MOI.get(b::GeoMeanBridge, ::MOI.ListOfVariableIndices) = b.xij
function MOI.get(b::GeoMeanBridge{T, F},
::MOI.NumberOfConstraints{F, MOI.LessThan{T}}) where {T, F}
return 1 # t ≤ x_{l1}/sqrt(N)
Expand Down
29 changes: 10 additions & 19 deletions src/Bridges/Constraint/indicator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,12 @@ struct IndicatorActiveOnFalseBridge{T, F <: MOI.AbstractVectorFunction, S <: MOI
end

function bridge_constraint(::Type{IndicatorActiveOnFalseBridge{T,F,S}}, model::MOI.ModelLike, f::MOI.VectorAffineFunction{T}, s::IS) where {S <: MOI.AbstractScalarSet, T <: Real, F, IS <: MOI.IndicatorSet{MOI.ACTIVATE_ON_ZERO, S}}
z1 = f.terms[1].scalar_term.variable_index
z2 = MOI.add_variable(model)
zo_cons = MOI.add_constraint(model, MOI.SingleVariable(z2), MOI.ZeroOne())

f_scalars = MOIU.eachscalar(f)
z2, zo_cons = MOI.add_constrained_variable(model, MOI.ZeroOne())
# z1 + z2 == 1
dcons = MOI.add_constraint(model,
MOI.ScalarAffineFunction(
[MOI.ScalarAffineTerm(one(T), z1), MOI.ScalarAffineTerm(one(T), z2)], zero(T),
),
MOI.EqualTo(one(T)),
)
vec_terms2 = [t for t in f.terms if t.output_index == 2]
f2 = MOI.VectorAffineFunction(
vcat(
[MOI.VectorAffineTerm(1, MOI.ScalarAffineTerm(1.0, z2))],
vec_terms2,
),
f.constants
)
z1_z2 = MOIU.operate(+, T, f_scalars[1], MOI.SingleVariable(z2))
dcons = MOIU.normalize_and_add_constraint(model, z1_z2, MOI.EqualTo(one(T)))
f2 = MOIU.operate(vcat, T, MOI.SingleVariable(z2), f_scalars[2])
ci = MOI.add_constraint(model, f2, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE}(s.set))
return IndicatorActiveOnFalseBridge{T,F,S}(z2, zo_cons, dcons, ci)
end
Expand All @@ -45,8 +32,12 @@ function MOI.supports_constraint(::Type{<:IndicatorActiveOnFalseBridge{T}},
return true
end

function MOIB.added_constrained_variable_types(::Type{<:IndicatorActiveOnFalseBridge})
return [(MOI.ZeroOne,)]
end
function MOIB.added_constraint_types(::Type{IndicatorActiveOnFalseBridge{T, F, S}}) where {T, F, S}
return [(F, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, S})]
return [(MOI.ScalarAffineFunction{T}, MOI.EqualTo{T}),
(F, MOI.IndicatorSet{MOI.ACTIVATE_ON_ONE, S})]
end

function concrete_bridge_type(::Type{<:IndicatorActiveOnFalseBridge{T}},
Expand Down
1 change: 1 addition & 0 deletions src/Bridges/Constraint/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function bridge_constraint(::Type{SplitIntervalBridge{T, F}}, model, f::F,
end

MOI.supports_constraint(::Type{SplitIntervalBridge{T}}, ::Type{<:MOI.AbstractScalarFunction}, ::Type{MOI.Interval{T}}) where T = true
MOIB.added_constrained_variable_types(::Type{<:SplitIntervalBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{SplitIntervalBridge{T, F}}) where {T, F}
return [(F, MOI.GreaterThan{T}), (F, MOI.LessThan{T})]
end
Expand Down
1 change: 1 addition & 0 deletions src/Bridges/Constraint/quad_to_soc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ function MOI.supports_constraint(::Type{QuadtoSOCBridge{T}},
MOI.GreaterThan{T}}}) where T
return true
end
MOIB.added_constrained_variable_types(::Type{<:QuadtoSOCBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{QuadtoSOCBridge{T}}) where T
return [(MOI.VectorAffineFunction{T}, MOI.RotatedSecondOrderCone)]
end
Expand Down
1 change: 1 addition & 0 deletions src/Bridges/Constraint/rsoc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function MOI.supports_constraint(::Type{RSOCBridge{T}},
::Type{MOI.RotatedSecondOrderCone}) where T
return true
end
MOIB.added_constrained_variable_types(::Type{<:RSOCBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{<:RSOCBridge{T, F}}) where {T, F}
return [(F, MOI.SecondOrderCone)]
end
Expand Down
19 changes: 7 additions & 12 deletions src/Bridges/Constraint/scalarize.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
const VectorLinearSet = Union{MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives}

scalar_set_type(::Type{<:MOI.Zeros}, T::Type) = MOI.EqualTo{T}
scalar_set_type(::Type{<:MOI.Nonpositives}, T::Type) = MOI.LessThan{T}
scalar_set_type(::Type{<:MOI.Nonnegatives}, T::Type) = MOI.GreaterThan{T}

"""
ScalarizeBridge{T, F, S}

Transforms a constraint `AbstractVectorFunction`-in-`vector_set(S)` where
Transforms a constraint `AbstractVectorFunction`-in-`vector_set_type(S)` where
`S <: LPCone{T}` to `F`-in-`S`.
"""
mutable struct ScalarizeBridge{T, F, S} <: AbstractBridge
Expand All @@ -17,7 +11,7 @@ end
function bridge_constraint(::Type{ScalarizeBridge{T, F, S}},
model::MOI.ModelLike,
f::MOI.AbstractVectorFunction,
set::VectorLinearSet) where {T, F, S}
set::MOIU.VectorLinearSet) where {T, F, S}
dimension = MOI.output_dimension(f)
constants = MOI.constant(f, T)
new_f = MOIU.scalarize(f, true)
Expand All @@ -30,16 +24,17 @@ end

function MOI.supports_constraint(::Type{ScalarizeBridge{T}},
::Type{<:MOI.AbstractVectorFunction},
::Type{<:VectorLinearSet}) where T
::Type{<:MOIU.VectorLinearSet}) where T
return true
end
MOIB.added_constrained_variable_types(::Type{<:ScalarizeBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{ScalarizeBridge{T, F, S}}) where {T, F, S}
return [(F, S)]
end
function concrete_bridge_type(::Type{<:ScalarizeBridge{T}},
F::Type{<:MOI.AbstractVectorFunction},
S::Type{<:VectorLinearSet}) where T
return ScalarizeBridge{T, MOIU.scalar_type(F), scalar_set_type(S, T)}
S::Type{<:MOIU.VectorLinearSet}) where T
return ScalarizeBridge{T, MOIU.scalar_type(F), MOIU.scalar_set_type(S, T)}
end

# Attributes, Bridge acting as a model
Expand Down Expand Up @@ -69,7 +64,7 @@ function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction,
end
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
bridge::ScalarizeBridge{T, F, S}) where {T, F, S}
return vector_set_type(S)(length(bridge.constants))
return MOIU.vector_set_type(S)(length(bridge.constants))
end

function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal,
Expand Down
2 changes: 2 additions & 0 deletions src/Bridges/Constraint/soc_to_psd.jl
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ _SOCtoPSDaff(f::MOI.VectorOfVariables, ::Type{T}) where T = _SOCtoPSDaff(MOI.Vec
_SOCtoPSDaff(f::MOI.VectorAffineFunction, ::Type) = _SOCtoPSDaff(f, MOIU.eachscalar(f)[1])

MOI.supports_constraint(::Type{SOCtoPSDBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.SecondOrderCone}) where T = true
MOIB.added_constrained_variable_types(::Type{<:SOCtoPSDBridge}) = Tuple{DataType}[]
MOIB.added_constraint_types(::Type{SOCtoPSDBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.SecondOrderCone}) where T = [(MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle)]

function MOI.get(model::MOI.ModelLike, a::MOI.ConstraintPrimal, c::SOCtoPSDBridge)
Expand Down Expand Up @@ -113,6 +114,7 @@ struct RSOCtoPSDBridge{T, G} <: AbstractBridge
end

MOI.supports_constraint(::Type{RSOCtoPSDBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.RotatedSecondOrderCone}) where T = true
MOIB.added_constrained_variable_types(::Type{<:RSOCtoPSDBridge}) = Tuple{DataType}[]
MOIB.added_constraint_types(::Type{<:RSOCtoPSDBridge{T}}, ::Type{<:Union{MOI.VectorOfVariables, MOI.VectorAffineFunction{T}}}, ::Type{MOI.RotatedSecondOrderCone}) where T = [(MOI.VectorAffineFunction{T}, MOI.PositiveSemidefiniteConeTriangle)]
function concrete_bridge_type(::Type{<:RSOCtoPSDBridge{T}},
G::Type{<:MOI.AbstractVectorFunction},
Expand Down
1 change: 1 addition & 0 deletions src/Bridges/Constraint/square.jl
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ function MOI.supports_constraint(::Type{SquareBridge{T}},
::Type{<:MOI.AbstractSymmetricMatrixSetSquare}) where T
return true
end
MOIB.added_constrained_variable_types(::Type{<:SquareBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{SquareBridge{T, F, G, TT, ST}}) where {T, F, G, TT, ST}
return [(F, TT), (G, MOI.EqualTo{T})]
end
Expand Down
28 changes: 9 additions & 19 deletions src/Bridges/Constraint/vectorize.jl
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
const ScalarLinearSet{T} = Union{MOI.EqualTo{T}, MOI.LessThan{T}, MOI.GreaterThan{T}}

vector_set(::MOI.EqualTo) = MOI.Zeros(1)
vector_set(::MOI.LessThan) = MOI.Nonpositives(1)
vector_set(::MOI.GreaterThan) = MOI.Nonnegatives(1)

vector_set_type(::Type{<:MOI.EqualTo}) = MOI.Zeros
vector_set_type(::Type{<:MOI.LessThan}) = MOI.Nonpositives
vector_set_type(::Type{<:MOI.GreaterThan}) = MOI.Nonnegatives

"""
VectorizeBridge{T, F, S, G}

Expand All @@ -20,7 +10,7 @@ mutable struct VectorizeBridge{T, F, S, G} <: AbstractBridge
end
function bridge_constraint(::Type{VectorizeBridge{T, F, S, G}},
model::MOI.ModelLike, g::G,
set::MOI.AbstractScalarSet) where {T, F, S, G}
set::MOIU.ScalarLinearSet{T}) where {T, F, S, G}
set_constant = MOI.constant(set)
h = MOIU.operate(-, T, g, set_constant)
if -set_constant != MOI.constant(h)[1]
Expand All @@ -30,24 +20,25 @@ function bridge_constraint(::Type{VectorizeBridge{T, F, S, G}},
typeof(set)}(constant))
end
f = MOIU.operate(vcat, T, h)
vector_constraint = MOI.add_constraint(model, f, vector_set(set))
vector_constraint = MOI.add_constraint(model, f, S(1))
return VectorizeBridge{T, F, S, G}(vector_constraint, set_constant)
end

function MOI.supports_constraint(::Type{VectorizeBridge{T}},
::Type{<:MOI.AbstractScalarFunction},
::Type{<:ScalarLinearSet{T}}) where T
::Type{<:MOIU.ScalarLinearSet{T}}) where T
return true
end
MOIB.added_constrained_variable_types(::Type{<:VectorizeBridge}) = Tuple{DataType}[]
function MOIB.added_constraint_types(::Type{<:VectorizeBridge{T, F, S}}) where {T, F, S}
return [(F, S)]
end
function concrete_bridge_type(::Type{<:VectorizeBridge{T}},
G::Type{<:MOI.AbstractScalarFunction},
S::Type{<:ScalarLinearSet{T}}) where T
S::Type{<:MOIU.ScalarLinearSet{T}}) where T
H = MOIU.promote_operation(-, T, G, T)
F = MOIU.promote_operation(vcat, T, H)
return VectorizeBridge{T, F, vector_set_type(S), G}
return VectorizeBridge{T, F, MOIU.vector_set_type(S), G}
end

# Attributes, Bridge acting as a model
Expand All @@ -73,8 +64,7 @@ function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintPrimal,
@assert length(x) == 1
y = x[1]
status = MOI.get(model, MOI.PrimalStatus(attr.N))
if status != MOI.INFEASIBILITY_CERTIFICATE &&
status != MOI.NEARLY_INFEASIBILITY_CERTIFICATE
if !MOIU.is_ray(status)
# If it is an infeasibility certificate, it is a ray and satisfies the
# homogenized problem, see https://github.com/JuliaOpt/MathOptInterface.jl/issues/433
# Otherwise, we need to add the set constant since the ConstraintPrimal
Expand All @@ -97,7 +87,7 @@ function MOI.modify(model::MOI.ModelLike, bridge::VectorizeBridge,
[(1, change.new_coefficient)]))
end
function MOI.set(model::MOI.ModelLike, ::MOI.ConstraintSet,
bridge::VectorizeBridge, new_set::ScalarLinearSet)
bridge::VectorizeBridge, new_set::MOIU.ScalarLinearSet)
bridge.set_constant = MOI.constant(new_set)
MOI.modify(model, bridge.vector_constraint,
MOI.VectorConstantChange([-bridge.set_constant]))
Expand All @@ -111,5 +101,5 @@ function MOI.get(model::MOI.ModelLike, attr::MOI.ConstraintFunction,
end
function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintSet,
bridge::VectorizeBridge{T, F, S}) where {T, F, S}
return scalar_set_type(S, T)(bridge.set_constant)
return MOIU.scalar_set_type(S, T)(bridge.set_constant)
end
11 changes: 11 additions & 0 deletions src/Utilities/sets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,14 @@ end
function shift_constant(set::MOI.Interval, offset)
return MOI.Interval(set.lower + offset, set.upper + offset)
end

const ScalarLinearSet{T} = Union{MOI.EqualTo{T}, MOI.LessThan{T}, MOI.GreaterThan{T}}
const VectorLinearSet = Union{MOI.Zeros, MOI.Nonnegatives, MOI.Nonpositives}

vector_set_type(::Type{<:MOI.EqualTo}) = MOI.Zeros
vector_set_type(::Type{<:MOI.LessThan}) = MOI.Nonpositives
vector_set_type(::Type{<:MOI.GreaterThan}) = MOI.Nonnegatives

scalar_set_type(::Type{<:MOI.Zeros}, T::Type) = MOI.EqualTo{T}
scalar_set_type(::Type{<:MOI.Nonpositives}, T::Type) = MOI.LessThan{T}
scalar_set_type(::Type{<:MOI.Nonnegatives}, T::Type) = MOI.GreaterThan{T}
13 changes: 13 additions & 0 deletions test/Bridges/Constraint/det.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
using Test

using MathOptInterface
const MOI = MathOptInterface
const MOIT = MathOptInterface.Test
const MOIU = MathOptInterface.Utilities
const MOIB = MathOptInterface.Bridges

include("../utilities.jl")

mock = MOIU.MockOptimizer(MOIU.Model{Float64}())
config = MOIT.TestConfig()

@testset "LogDet" begin
bridged_mock = MOIB.Constraint.LogDet{Float64}(mock)
mock.optimize! = (mock::MOIU.MockOptimizer) -> MOIU.mock_optimize!(mock, [0, 1, 0, 1, 1, 0, 1, 0, 0, 1])
Expand Down