Skip to content

Commit b237603

Browse files
Add and fix multimaterial example (#18)
* First sketch * ignore derivatives * solved assertion issue * zygote "foreign call" error * fix TopOpt example and use Zygote-over-FDM Co-authored-by: LucasMSpereira <lucas.lmspereira@gmail.com>
1 parent 95db7da commit b237603

File tree

3 files changed

+158
-13
lines changed

3 files changed

+158
-13
lines changed

Project.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ version = "0.1.0"
77
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
88
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
99
DistributionsAD = "ced4e74d-a319-5a8a-b0ac-84af2272839c"
10+
FiniteDifferences = "26cc04aa-876d-5657-8c51-4c34ba976000"
11+
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
1012
ImplicitDifferentiation = "57b37032-215b-411a-8a7c-41a003a55207"
1113
JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899"
1214
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1315
NonconvexIpopt = "bf347577-a06d-49ad-a669-8c0e005493b8"
16+
NonconvexTOBS = "6c0b5230-d4c9-466e-bfd4-b31e6272ab65"
1417
Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
1518
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
19+
StaticArraysCore = "1e83bf80-4336-4d27-bf5d-d5a4f845583c"
1620
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
21+
TopOpt = "53a1e1a5-51bb-58a9-8a02-02056cc81109"
1722
UnPack = "3a884ed6-31ef-47d7-9d2a-63182c4928ed"
1823
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
1924

src/ReliabilityOptimization.jl

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module ReliabilityOptimization
22

33
using ImplicitDifferentiation, Zygote, LinearAlgebra, ChainRulesCore, SparseArrays
44
using UnPack, NonconvexIpopt, Statistics, Distributions, Reexport, DistributionsAD
5+
using FiniteDifferences, StaticArraysCore
56
@reexport using LinearAlgebra
67
@reexport using Statistics
78
export RandomFunction, FORM, RIA, MvNormal
@@ -15,9 +16,14 @@ end
1516
struct FORM{M}
1617
method::M
1718
end
18-
struct RIA end
19+
struct RIA{A, O}
20+
optim_alg::A
21+
optim_options::O
22+
end
23+
RIA() = RIA(IpoptAlg(), IpoptOptions(print_level = 0, max_wall_time = 1.0))
1924

20-
function get_forward(f, p, ::FORM{<:RIA})
25+
function get_forward(f, p, method::FORM{<:RIA})
26+
alg, options = method.method.optim_alg, method.method.optim_options
2127
function forward(x)
2228
# gets an objective function of p
2329
obj = pc -> begin
@@ -39,9 +45,9 @@ function get_forward(f, p, ::FORM{<:RIA})
3945
add_eq_constraint!(innerOptModel, constr)
4046
result = optimize(
4147
innerOptModel,
42-
IpoptAlg(),
48+
alg,
4349
[mean(p); 0.0],
44-
options = IpoptOptions(print_level = 0),
50+
options = options,
4551
)
4652
return vcat(result.minimizer, result.problem.mult_g[1])
4753
end
@@ -54,7 +60,7 @@ function get_conditions(f, ::FORM{<:RIA})
5460
c = pcmult[end-1]
5561
mult = pcmult[end]
5662
return vcat(
57-
2 * p + Zygote.pullback(p -> f(x, p), p)[2](mult)[1],
63+
2 * p + vec(_jacobian(f, x, p)) * mult,
5864
2c - mult,
5965
f(x, p) .- c,
6066
)
@@ -79,23 +85,103 @@ end
7985
_vec(x::Real) = [x]
8086
_vec(x) = x
8187

82-
function _jacobian(f, x)
83-
val, pb = Zygote.pullback(f, x)
84-
M = length(val)
85-
vecs = [Vector(sparsevec([i], [true], M)) for i in 1:M]
86-
Jt = reduce(hcat, first.(pb.(vecs)))
87-
return copy(Jt')
88+
function get_identity_vecs(M)
89+
return [Vector(sparsevec([i], [1.0], M)) for i in 1:M]
90+
end
91+
function ChainRulesCore.rrule(::typeof(get_identity_vecs), M::Int)
92+
get_identity_vecs(M), _ -> (NoTangent(), NoTangent())
93+
end
94+
reduce_hcat(vs) = reduce(hcat, vs)
95+
# function ChainRulesCore.rrule(::typeof(reduce_hcat), vs::Vector{<:Vector})
96+
# return reduce_hcat(vs), Δ -> begin
97+
# return NoTangent(), [Δ[:, i] for i in 1:size(Δ, 2)]
98+
# end
99+
# end
100+
101+
const fdm = FiniteDifferences.central_fdm(5, 1)
102+
103+
function _jacobian(f, x1, x2)
104+
# val, pb = Zygote.pullback(f, x1, x2)
105+
# if val isa Vector
106+
# M = length(val)
107+
# vecs = get_identity_vecs(M)
108+
# cotangents = map(pb, vecs)
109+
# Jt = reduce_hcat(map(last, cotangents))
110+
# return copy(Jt')
111+
# elseif val isa Real
112+
# Jt = last(pb(1.0))
113+
# return copy(Jt')
114+
# else
115+
# throw(ArgumentError("Output type not supported."))
116+
# end
117+
ẏs = map(eachindex(x2)) do n
118+
return fdm(zero(eltype(x2))) do ε
119+
xn = x2[n]
120+
xcopy = vcat(x2[1:n-1], xn + ε, x2[n+1:end])
121+
ret = copy(f(x1, xcopy)) # copy required incase `f(x)` returns something that aliases `x`
122+
return ret
123+
end
124+
end
125+
return reduce(hcat, ẏs)
88126
end
127+
# function ChainRulesCore.rrule(::typeof(_jacobian), f, x1, x2)
128+
# (val, pb), _pb_pb = Zygote.pullback(Zygote.pullback, f, x1, x2)
129+
# M = length(val)
130+
# if val isa Vector
131+
# vecs = get_identity_vecs(M)
132+
# _pb = (pb, v) -> last(pb(v))
133+
# co1, pb_pb = Zygote.pullback(_pb, pb, first(vecs))
134+
# cotangents = vcat([co1], last.(map(pb, @view(vecs[2:end]))))
135+
# Jt, hcat_pb = Zygote.pullback(reduce_hcat, cotangents)
136+
# return copy(Jt'), Δ -> begin
137+
# temp = hcat_pb(Δ')[1]
138+
# co_pb = map(temp) do t
139+
# first(pb_pb(t))
140+
# end
141+
# co_f_x = _pb_pb.(tuple.(Ref(nothing), co_pb))
142+
# co_f = sum(getindex.(co_f_x, 1))
143+
# co_x1 = sum(getindex.(co_f_x, 2))
144+
# co_x2 = sum(getindex.(co_f_x, 3))
145+
# return NoTangent(), co_f, co_x1, co_x2
146+
# end
147+
# elseif val isa Real
148+
# println(1)
149+
# _pb = (pb, v) -> pb(v)[end]
150+
# println(2)
151+
# @show _pb(pb, 1.0)
152+
# Jt, pb_pb = Zygote.pullback(_pb, pb, 1.0)
153+
# println(3)
154+
# return copy(Jt'), Δ -> begin
155+
# println(4)
156+
# @show vec(Δ)
157+
# @show Δ
158+
# @show pb_pb(Δ')
159+
# co_pb = first(pb_pb(vec(Δ)))
160+
# co_f_x = _pb_pb((nothing, co_pb))
161+
# co_f = co_f_x[1]
162+
# co_x1 = co_f_x[2]
163+
# co_x2 = co_f_x[3]
164+
# return NoTangent(), co_f, co_x1, co_x2
165+
# end
166+
# else
167+
# throw(ArgumentError("Output type not supported."))
168+
# end
169+
# end
89170

90171
function (f::RandomFunction)(x)
91172
mup = mean(f.p)
92173
covp = cov(f.p)
93174
p0 = getp0(f.f, x, f.p, f.method)
94-
dfdp0 = _jacobian(p -> f.f(x, p), p0)
175+
dfdp0 = _jacobian(f.f, x, p0)
95176
fp0 = f.f(x, p0)
96-
muf = _vec(fp0) + dfdp0 * (mup - p0)
177+
muf = _vec(fp0) .+ dfdp0 * (mup - p0)
97178
covf = dfdp0 * covp * dfdp0'
98179
return MvNormal(muf, covf)
99180
end
100181

182+
# necessary type piracy FiniteDifferences._estimate_magnitudes uses this constructor which Zygote struggles to differentiate on its own
183+
function ChainRulesCore.rrule(::typeof(StaticArraysCore.SVector{3}), x1::T, x2::T, x3::T) where {T}
184+
StaticArraysCore.SVector{3}(x1, x2, x3), Δ -> (NoTangent(), Δ[1], Δ[2], Δ[3])
101185
end
186+
187+
end

test/example.jl

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
using ReliabilityOptimization, Test, NonconvexTOBS, ChainRulesCore, TopOpt, Zygote, FiniteDifferences
2+
3+
const densities = [0.0, 0.5, 1.0] # for mass calculation
4+
const nmats = 3 # number of materials
5+
const v = 0.3 # Poisson’s ratio
6+
const f = 1.0 # downward force
7+
const problemSize = (4, 4) # size of rectangular mesh
8+
const elSize = (1.0, 1.0) # size of QUAD4 elements
9+
# Point load cantilever problem to be solved
10+
problem = PointLoadCantilever(Val{:Linear}, problemSize, elSize, 1.0, v, f)
11+
ncells = TopOpt.getncells(problem) # Number of elements in mesh
12+
solver = FEASolver(Direct, problem; xmin = 0.0)
13+
filter = DensityFilter(solver; rmin = 3.0) # filter to avoid checkerboarding
14+
M = 1 / nmats / 2 # mass fraction
15+
comp = Compliance(solver) # function that returns compliance
16+
penalty = TopOpt.PowerPenalty(3.0) # SIMP penalty
17+
# Young’s modulii of air (void) + 2 materials
18+
const avgEs = [1e-6, 0.5, 2.0]
19+
# since first E is close to zero,
20+
# can the deviation make the final value negative?
21+
logEs = MvNormal(log.(avgEs), Matrix(Diagonal(0.1 .* abs.(log.(avgEs)))))
22+
# 'Original' function. At least one input is random.
23+
# In this example, Es is the random input.
24+
function uncertainComp(x, logEs)
25+
Es = exp.(logEs)
26+
# interpolation of properties between materials
27+
interp = MaterialInterpolation(Es, penalty)
28+
MultiMaterialVariables(x, nmats) |> interp |> filter |> comp
29+
# return sum(x) + sum(Es)
30+
end
31+
# wrap original function in RandomFunction struct
32+
rf = RandomFunction(uncertainComp, logEs, FORM(RIA()))
33+
# initial homogeneous distribution of pseudo-densities
34+
x0 = fill(M, ncells * (length(logEs) - 1))
35+
# call wrapper with example input
36+
# (Returns propability distribution of the objective for current point)
37+
d = rf(x0)
38+
# mass constraint
39+
constr = x -> begin
40+
ρs = PseudoDensities(MultiMaterialVariables(x, nmats))
41+
return sum(element_densities(ρs, densities)) / ncells - 0.3 # unit element volume
42+
end
43+
function obj(x) # objective for TO problem
44+
dist = rf(x)
45+
mean(dist)[1] + 2 * sqrt(cov(dist)[1, 1])
46+
end
47+
obj(x0)
48+
Zygote.gradient(obj, x0)
49+
FiniteDifferences.grad(central_fdm(5, 1), obj, x0)[1]
50+
51+
m = Model(obj) # create optimization model
52+
addvar!(m, zeros(length(x0)), ones(length(x0))) # setup optimization variables
53+
Nonconvex.add_ineq_constraint!(m, constr) # setup volume inequality constraint
54+
@time r = Nonconvex.optimize(m, TOBSAlg(), x0; options = TOBSOptions())

0 commit comments

Comments
 (0)