Tension/compression spring
The problem
A helical coil spring is wound from wire and loaded along its axis. The design has three variables: the diameter d of the wire, the mean diameter D of the coils, both in inches, and the number N of active coils, those that deflect under the load. The goal is the lightest spring. The weight is taken as
(N + 2) D d²
which is proportional to the volume of the wire: N + 2 coils (the active ones and one closed coil at each end), each πD long, of cross-section πd²/4.
Four constraints, written g(x) ≤ 0, keep the spring usable:
g1 = 1 − D³ N / (71785 d⁴) the deflection
g2 = (4D² − dD) / (12566 (D d³ − d⁴)) + 1 / (5108 d²) − 1 the shear stress
g3 = 1 − 140.45 d / (D² N) the surge frequency
g4 = (D + d) / 1.5 − 1 the outer diameter
- g1: under its load, the spring must deflect at least a given amount. The deflection grows with D³ N / d⁴: a thin wire, wide coils and many of them make a soft spring.
- g2: the shear stress in the wire must stay below the allowed stress.
- g3: the surge frequency, the frequency of waves along the spring, must stay above a limit, to avoid resonance.
- g4: the outer diameter, D + d, can't exceed 1.5 inches.
The problem comes from Belegundu (1982) and Arora (1989, Introduction to Optimum Design,
McGraw-Hill). genoxide's TensionCompressionSpring uses the definition and bounds that Coello
Coello restates (2000, Computers in Industry 41(2): 113-127): d in [0.05, 2], D in [0.25, 1.3] and
N in [2, 15]. N is continuous, as in the restatement. Cagnina, Esquivel and Coello Coello (2008,
Informatica 32: 319-326) report a weight of 0.012665 at d = 0.051690, D = 0.356750 and N =
11.287126; that design, printed to 6 digits, exceeds g2 by 2e-5. A local solver (SLSQP) started
from it reaches their minimum to full precision, 0.01266523278831971, at d = 0.0516891, D =
0.3567176 and N = 11.288971, with every constraint met: genoxide's best known weight. It isn't
proven optimal.
What makes it hard
The constraints pull against each other. A lighter spring needs less wire: a thinner wire, smaller coils or fewer of them. But a thinner wire raises the shear stress (g2), and smaller or fewer coils make the spring too stiff to deflect enough (g1). At the best designs found, both g1 and g2 hold with equality: the lightest spring lies where their two curved boundaries meet. g3 and g4 have slack.
The feasible region is small. Of 2 million designs drawn at random within the bounds, 0.75 % are feasible; g1 alone holds for 1.7 %. The bounds are also far wider than the good designs: the best d is 0.0517, in the lowest 0.1 % of its range [0.05, 2].
Near the minimum, the weight hardly changes along the curve where g1 and g2 meet: two feasible designs with D = 0.3567 and D = 0.3581 differ in weight by 6e-8, in the sixth significant digit. A search gets close fast and settles the last digits slowly.
Representation
A Real genome of 3 genes, (d, D, N), within the bounds above. The fitness is the weight and the
total constraint violation, the sum of max(0, g) over the four constraints, 0 for a feasible
design. genoxide compares fitnesses with Deb's feasibility rules (Deb, 2000, Computer Methods in
Applied Mechanics and Engineering 186: 311-338): a feasible design beats an infeasible one, two
feasible ones compare by weight, and two infeasible ones by violation. The rules need no penalty
weights.
Algorithm
SHADE (Tanabe and Fukunaga, 2013, IEEE CEC 2013: 71-78), a differential evolution that adapts its scale factor and crossover rate from successful trials, with genoxide's defaults: its published population of 100, and a restart after 200 generations without progress. Differential evolution builds each trial design from differences between designs of the population, so its steps shrink as the population gathers along the curve of the minimum. It stops once the weight is within 1e-10 of the best known, relative to its size, or after 100,000 evaluations.
Output
The first line gives the weight of the best design, the evaluations the run took and the best known
weight. The second gives its
constraint violation; 0 means it's feasible. The third gives the design: d, D and N. The last names
the constraints at their limit, within 1e-6 of 0. In Python, run evaluates the problem in Rust,
so both versions print the same.
The progress curve is the weight's error to the best known weight, on a log axis. The plot shows each variable on its range, and each constraint's value g: satisfied with its slack, active (within 1e-6 of its limit) or violated. The best design is feasible from the first generation on: the best of the 100 random designs of the first population is feasible. The weight falls to 0.0127 after about 14,000 evaluations and to 0.012666 after about 37,000. From about 26,000 evaluations, g1 and g2 come within 1e-6 of their limits, first in turn and from about 40,000 on together; g3 and g4 keep their slack throughout. The error falls below 1e-7 after about 48,000 evaluations and below 1e-9 after about 65,000, and from about 56,000 the median weight matches the best to 6 significant digits. The population keeps moving along the curve of the minimum, without a restart, until the run meets its target after 86,900 evaluations.
The project page plays this run back.
Good results
The best known weight is 0.01266523278831971. The run finds a feasible spring of weight 0.0126652 (0.012665232789 to 11 significant digits, 1.0e-12 above the best known), at d = 0.051689, D = 0.356723 and N = 11.288641, after 86,900 evaluations. Cagnina et al.'s published design weighs 0.0126651, a little less, but it exceeds g2 by 2e-5: it's slightly infeasible.
In the run's design, the deflection (g1) and the shear stress (g2) are at their limits. The surge frequency is five times its limit, and the outer diameter is 0.41 inch, far below 1.5.
With seeds 1 to 20, every run meets the target, after 42,000 to 91,400 evaluations, 81,400 at the median.
Reference: Belegundu, A. D. (1982). A Study of Mathematical Programming Methods for Structural Optimization. PhD thesis, University of Iowa.
Known optimum: 0.01266523278831971 (weight), best known
Source: examples/tension_compression_spring
Interactive run: tachsin.gr/projects/genoxide/examples/tension-compression-spring
cargo run --release --example tension_compression_spring
//! The tension/compression spring (Belegundu, 1982; Arora, 1989): the lightest coil spring whose
//! deflection, shear stress, surge frequency and outer diameter stay within their limits. A
//! constrained continuous problem, whose best known weight is 0.01266523.
//!
//! The variables are the wire diameter d, the mean coil diameter D and the number of active coils
//! N. genoxide's `TensionCompressionSpring` gives the weight and the violation of the four
//! constraints, which Deb's feasibility rules compare. SHADE, a differential evolution, searches
//! the genes, and the example prints the best design and the constraints at their limits.
//!
//! With `GENOXIDE_TRACE=<file>`, it also writes a trace of its run for the plot on the example's
//! page, with `trace.rs`.
//!
//! ```text
//! cargo run --release --example tension_compression_spring
//! ```
mod trace;
use genoxide::prelude::*;
use genoxide::problems::Problem;
use genoxide::problems::engineering::TensionCompressionSpring;
// a constraint within this of 0 is at its limit: active
const ACTIVE: f64 = 1e-6;
fn main() -> Result<()> {
let problem = TensionCompressionSpring;
let best_known = problem.optimum().expect("known").value();
let de = De::builder(problem.representation())
.minimize()
.seed(1)
.build()?;
// with GENOXIDE_TRACE=<file>, a trace of the run for the plot on the example's page
let mut trace = trace::Trace::from_env();
let outcome = Engine::new(de, problem)
.stop_when(Stop::target(best_known * (1.0 + 1e-10)).or(Stop::evaluations(100_000)))
.on_generation(|snapshot| trace.record(snapshot))
.run()?;
let best = outcome.best_fitness();
let x = outcome.best_genome();
let constraints = problem.constraints(x);
let active: Vec<String> = (constraints.inequalities().iter().enumerate())
.filter(|(_, g)| g.abs() <= ACTIVE)
.map(|(i, _)| format!("g{}", i + 1))
.collect();
println!(
"weight {:.7} after {} evaluations (the best known: {best_known})",
best.score().unwrap_or(f64::NAN),
outcome.evaluations()
);
println!("violation {:.6}", best.violation());
println!("d {:.6}, D {:.6}, N {:.6}", x[0], x[1], x[2]);
println!("active constraints: {}", active.join(", "));
trace.write();
Ok(())
}
python examples/tension_compression_spring/main.py
"""The tension/compression spring (Belegundu, 1982; Arora, 1989): the lightest coil spring whose
deflection, shear stress, surge frequency and outer diameter stay within their limits. A
constrained continuous problem, whose best known weight is 0.01266523.
The variables are the wire diameter d, the mean coil diameter D and the number of active coils N.
genoxide's ``TensionCompressionSpring`` gives the weight and the violation of the four
constraints, which Deb's feasibility rules compare. SHADE, a differential evolution, searches the
genes, and the example prints the best design and the constraints at their limits.
With ``GENOXIDE_TRACE=<file>``, it also writes a trace of its run for the plot on the example's
page, with trace.py.
python examples/tension_compression_spring/main.py
"""
import genoxide as gx
from trace import Trace
# a constraint within this of 0 is at its limit: active
ACTIVE = 1e-6
problem = gx.problems.engineering.TensionCompressionSpring()
best_known = problem.optimum.value
de = gx.De(problem.genome, objective=problem.objective, seed=1)
# with GENOXIDE_TRACE=<file>, a trace of the run for the plot on the example's page
trace = Trace(problem)
result = de.run(
problem,
target=best_known * (1.0 + 1e-10),
evaluations=100_000,
on_generation=trace.on_generation,
)
d, coil, n = result.best_genome.tolist()
constraints = problem.constraints(result.best_genome).tolist()
active = [f"g{i + 1}" for i, g in enumerate(constraints) if abs(g) <= ACTIVE]
print(
f"weight {result.best_fitness:.7f} after {result.evaluations} evaluations "
f"(the best known: {best_known})"
)
print(f"violation {result.violation:.6f}")
print(f"d {d:.6f}, D {coil:.6f}, N {n:.6f}")
print(f"active constraints: {', '.join(active)}")
trace.write()
What it prints, from a seeded run:
weight 0.0126652 after 86900 evaluations (the best known: 0.01266523278831971)
violation 0.000000
d 0.051689, D 0.356723, N 11.288641
active constraints: g1, g2