Загрузка данных
from dataclasses import dataclass
import math
import random
import time
import numpy as np
import matplotlib.pyplot as plt
# ============================================================
# 1. Параметры задачи и ГА
# ============================================================
SEED = 42
POPULATION_SIZE = 80
GENERATIONS = 200
ELITISM = 4
TOURNAMENT_SIZE = 3
CROSSOVER_RATE = 0.90
MUTATION_RATE = 0.10
STOP_WITHOUT_IMPROVEMENT = 60
E = 2.1e11 # Па
DENSITY = 7850.0 # кг/м³
SIGMA_ALLOW = 160e6 # Па
DISPLACEMENT_ALLOW = 0.020 # м (20 мм)
# Геометрические ограничения
MIN_MEMBER_LENGTH = 0.10
MIN_NODE_DISTANCE = 0.10
MU_BUCKLING = 1.0
BUCKLING_SAFETY_FACTOR = 1.0
# Сортамент: КРУГЛЫЕ СТЕРЖНИ СПЛОШНОГО СЕЧЕНИЯ, диаметр 12–100 мм.
# A = πd²/4, I = πd⁴/64 — формулы сплошного круга.
DIAMETERS_MM = np.array([
12, 14, 16, 18, 20, 22, 25, 28, 30, 32,
35, 38, 40, 45, 50, 55, 60, 70, 80, 90, 100
], dtype=float)
AREAS = np.pi * (DIAMETERS_MM / 1000.0) ** 2 / 4.0
INERTIAS = np.pi * (DIAMETERS_MM / 1000.0) ** 4 / 64.0
# ============================================================
# 2. Тестовая ферма
# ============================================================
NODES = np.array([
[0.0, 0.0],
[1.0, 0.0],
[2.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
[2.0, 1.0],
], dtype=float)
MEMBERS = np.array([
[0, 1], [1, 2],
[3, 4], [4, 5],
[0, 3], [1, 4], [2, 5],
[0, 4], [1, 3], [1, 5],
], dtype=int)
SUPPORTS = {
0: (True, True),
2: (False, True),
}
LOADS = np.zeros((len(NODES), 2), dtype=float)
LOADS[1, 1] = -30e3
LOADS[4, 1] = -30e3
SHAPE_BOUNDS = {
3: ((0.0, 0.0), (0.6, 1.5)),
4: ((0.6, 1.4), (0.6, 1.5)),
5: ((2.0, 2.0), (0.6, 1.5)),
}
# ============================================================
# 3. Структура результата
# ============================================================
@dataclass
class Result:
fitness: float
mass: float
feasible: bool
violations: float
coords: np.ndarray
diameters_mm: np.ndarray
active: np.ndarray
displacements: np.ndarray
axial_forces: np.ndarray
stresses: np.ndarray
buckling_ratios: np.ndarray
reason: str = ""
# ============================================================
# 4. МКЭ
# ============================================================
def verify_rigidity(coords=None):
"""Проверка геометрической неизменяемости исходной схемы."""
c = NODES if coords is None else coords
n = len(c)
ndof = 2 * n
K = np.zeros((ndof, ndof))
areas = np.ones(len(MEMBERS)) * 1e-4
for e, (i, j) in enumerate(MEMBERS):
d = c[j] - c[i]
L = float(np.linalg.norm(d))
if L < 1e-9:
raise ValueError(f"Стержень {e} нулевой длины.")
cx, cy = d / L
k = (E * areas[e] / L) * np.array([
[ cx*cx, cx*cy, -cx*cx, -cx*cy],
[ cx*cy, cy*cy, -cx*cy, -cy*cy],
[-cx*cx, -cx*cy, cx*cx, cx*cy],
[-cx*cy, -cy*cy, cx*cy, cy*cy],
])
dofs = [2*i, 2*i+1, 2*j, 2*j+1]
K[np.ix_(dofs, dofs)] += k
fixed = set()
for node, (fx, fy) in SUPPORTS.items():
if fx: fixed.add(2 * node)
if fy: fixed.add(2 * node + 1)
free = np.array([d for d in range(ndof) if d not in fixed], dtype=int)
K_ff = K[np.ix_(free, free)]
rank = np.linalg.matrix_rank(K_ff)
cond = np.linalg.cond(K_ff)
return rank, K_ff.shape[0], cond
def assemble_and_solve(coords, areas, active):
n = len(coords)
ndof = 2 * n
K = np.zeros((ndof, ndof), dtype=float)
lengths = np.zeros(len(MEMBERS), dtype=float)
dirs = np.zeros((len(MEMBERS), 2), dtype=float)
for e, (i, j) in enumerate(MEMBERS):
if not active[e]:
continue
d = coords[j] - coords[i]
L = float(np.linalg.norm(d))
if L < 1e-9:
raise ValueError(f"Стержень {e} нулевой длины.")
cx, cy = d / L
lengths[e] = L
dirs[e] = (cx, cy)
k = (E * areas[e] / L) * np.array([
[ cx*cx, cx*cy, -cx*cx, -cx*cy],
[ cx*cy, cy*cy, -cx*cy, -cy*cy],
[-cx*cx, -cx*cy, cx*cx, cx*cy],
[-cx*cy, -cy*cy, cx*cy, cy*cy],
])
dofs = [2*i, 2*i+1, 2*j, 2*j+1]
K[np.ix_(dofs, dofs)] += k
F = LOADS.reshape(-1)
fixed = set()
for node, (fx, fy) in SUPPORTS.items():
if fx: fixed.add(2 * node)
if fy: fixed.add(2 * node + 1)
free = np.array([d for d in range(ndof) if d not in fixed], dtype=int)
if len(free) == 0:
raise ValueError("Нет свободных DOF.")
K_ff = K[np.ix_(free, free)]
F_f = F[free]
try:
u_free = np.linalg.solve(K_ff, F_f)
except np.linalg.LinAlgError as exc:
raise ValueError("Сингулярная матрица.") from exc
if not np.all(np.isfinite(u_free)):
raise ValueError("NaN/Inf в решении.")
# Защита от катастрофически больших чисел (почти-сингулярная матрица)
if np.max(np.abs(u_free)) > 1e3:
raise ValueError("Решение аномально большое (почти-сингулярная матрица).")
u = np.zeros(ndof)
u[free] = u_free
forces = np.zeros(len(MEMBERS))
stresses = np.zeros(len(MEMBERS))
for e, (i, j) in enumerate(MEMBERS):
if not active[e]:
continue
L = lengths[e]
cx, cy = dirs[e]
ext = np.dot(np.array([-cx, -cy, cx, cy]),
u[[2*i, 2*i+1, 2*j, 2*j+1]])
forces[e] = E * areas[e] * ext / L
stresses[e] = forces[e] / areas[e]
return u.reshape((-1, 2)), forces, stresses, lengths
# ============================================================
# 5. Кодирование и оценка
# ============================================================
def random_chromosome():
genes = []
for _ in MEMBERS:
genes.append(random.randrange(len(DIAMETERS_MM)))
for node, bounds in SHAPE_BOUNDS.items():
for low, high in bounds:
genes.append(random.uniform(low, high))
return genes
def initial_chromosome():
genes = []
idx_25 = int(np.argmin(np.abs(DIAMETERS_MM - 25.0)))
for _ in MEMBERS:
genes.append(idx_25)
for node, bounds in SHAPE_BOUNDS.items():
base = NODES[node]
for axis, (low, high) in enumerate(bounds):
genes.append(float(np.clip(base[axis], low, high)))
return genes
def decode(chromosome):
n = len(MEMBERS)
idx = np.asarray(chromosome[:n], dtype=int)
diameters = DIAMETERS_MM[np.clip(idx, 0, len(DIAMETERS_MM) - 1)]
coords = NODES.copy()
offset = n
for node, bounds in SHAPE_BOUNDS.items():
for axis, (low, high) in enumerate(bounds):
coords[node, axis] = float(np.clip(chromosome[offset], low, high))
offset += 1
active = np.ones(n, dtype=bool)
return coords, diameters, active
def evaluate(chromosome):
coords, diameters, active = decode(chromosome)
areas = np.zeros(len(MEMBERS))
inertias = np.zeros(len(MEMBERS))
for e, d in enumerate(diameters):
k = int(np.argmin(np.abs(DIAMETERS_MM - d)))
areas[e] = AREAS[k]
inertias[e] = INERTIAS[k]
# Геометрические проверки
for e, (i, j) in enumerate(MEMBERS):
L = float(np.linalg.norm(coords[j] - coords[i]))
if L < MIN_MEMBER_LENGTH:
return _invalid(coords, diameters, active,
f"Стержень {e} короче {MIN_MEMBER_LENGTH} м.")
for i in range(len(coords)):
for j in range(i + 1, len(coords)):
d = float(np.linalg.norm(coords[j] - coords[i]))
if d < MIN_NODE_DISTANCE:
return _invalid(coords, diameters, active,
f"Узлы {i} и {j} слишком близко.")
# МКЭ
try:
u, forces, stresses, lengths = assemble_and_solve(coords, areas, active)
except (ValueError, FloatingPointError) as exc:
return _invalid(coords, diameters, active, str(exc))
# Масса
mass = float(np.sum(DENSITY * areas[active] * lengths[active]))
ratios = []
# 1) Прочность
for e in range(len(MEMBERS)):
if not active[e]:
continue
r = max(0.0, abs(stresses[e]) / SIGMA_ALLOW - 1.0)
ratios.append(r)
# 2) Результирующие перемещения sqrt(ux² + uy²) для свободных узлов
for node in range(len(NODES)):
free_x = node not in SUPPORTS or not SUPPORTS[node][0]
free_y = node not in SUPPORTS or not SUPPORTS[node][1]
if free_x or free_y:
mag = math.hypot(u[node, 0], u[node, 1])
r = max(0.0, mag / DISPLACEMENT_ALLOW - 1.0)
ratios.append(r)
# 3) Устойчивость сжатых стержней
buckling = np.zeros(len(MEMBERS))
for e in range(len(MEMBERS)):
if not active[e] or forces[e] >= 0:
continue
L_eff = MU_BUCKLING * lengths[e]
p_cr = math.pi**2 * E * inertias[e] / max(L_eff**2, 1e-20)
p_dem = abs(forces[e]) * BUCKLING_SAFETY_FACTOR
buckling[e] = p_dem / max(p_cr, 1e-20)
ratios.append(max(0.0, buckling[e] - 1.0))
# 4) Эвристика: штраф за недогрузку.
# НЕ входит в violation (не влияет на допустимость),
# только мягко увеличивает fitness.
underload = 0.0
for e in range(len(MEMBERS)):
if not active[e]:
continue
N_max = areas[e] * SIGMA_ALLOW
util = abs(forces[e]) / max(N_max, 1e-12)
if util < 0.05:
underload += (0.05 - util) / 0.05
violation = float(sum(ratios))
feasible = violation <= 1e-8
fitness = (mass
* (1.0 + 1e4 * violation + 1e4 * violation ** 2)
* (1.0 + 0.2 * underload))
return Result(
fitness=fitness, mass=mass, feasible=feasible,
violations=violation, coords=coords,
diameters_mm=diameters, active=active,
displacements=u, axial_forces=forces,
stresses=stresses, buckling_ratios=buckling, reason=""
)
def _invalid(coords, diameters, active, reason):
return Result(
fitness=1e12, mass=1e9, feasible=False, violations=1e6,
coords=coords, diameters_mm=diameters, active=active,
displacements=np.zeros_like(coords),
axial_forces=np.zeros(len(MEMBERS)),
stresses=np.zeros(len(MEMBERS)),
buckling_ratios=np.zeros(len(MEMBERS)),
reason=reason
)
# ============================================================
# 6. Генетические операторы
# ============================================================
def tournament(population, scores):
k = min(TOURNAMENT_SIZE, len(population))
idx = random.sample(range(len(population)), k)
return population[min(idx, key=lambda i: scores[i])][:]
def crossover(p1, p2):
if random.random() > CROSSOVER_RATE or len(p1) < 2:
return p1[:], p2[:]
a, b = p1[:], p2[:]
l, r = sorted(random.sample(range(len(a) + 1), 2))
a[l:r], b[l:r] = b[l:r], a[l:r]
return a, b
def mutate(chrom, gen):
child = chrom[:]
n = len(MEMBERS)
progress = gen / max(GENERATIONS - 1, 1)
step = 0.20 * (1.0 - progress) + 0.02
for g in range(len(child)):
if random.random() >= MUTATION_RATE:
continue
if g < n:
child[g] = random.randrange(len(DIAMETERS_MM))
else:
si = g - n
node_pos = si // 2
axis = si % 2
node = list(SHAPE_BOUNDS.keys())[node_pos]
low, high = SHAPE_BOUNDS[node][axis]
if high > low:
child[g] = float(np.clip(
child[g] + random.gauss(0, step * (high - low)),
low, high))
return child
def invert(chrom):
child = chrom[:]
if len(child) >= 4 and random.random() < 0.10:
l, r = sorted(random.sample(range(len(child)), 2))
child[l:r + 1] = child[l:r + 1][::-1]
return child
# ============================================================
# 7. Основной цикл
# ============================================================
def optimize(verbose=True):
random.seed(SEED)
np.random.seed(SEED)
population = [initial_chromosome()] + \
[random_chromosome() for _ in range(POPULATION_SIZE - 1)]
best_feasible_chrom = None
best_feasible_mass = float("inf")
best_seen = None
best_score = float("inf")
stagnant = 0
history_best = []
history_mean = []
history_best_mass = []
history_feasible_frac = []
t0 = time.time()
for gen in range(GENERATIONS):
results = [evaluate(ind) for ind in population]
scores = [r.fitness for r in results]
order = sorted(range(len(population)), key=lambda i: scores[i])
population = [population[i] for i in order]
results = [results[i] for i in order]
scores = [scores[i] for i in order]
history_best.append(scores[0])
history_mean.append(float(np.mean(scores)))
feasible_frac = float(np.mean([r.feasible for r in results]))
history_feasible_frac.append(feasible_frac)
for ind, r in zip(population, results):
if r.feasible and r.mass < best_feasible_mass:
best_feasible_mass = r.mass
best_feasible_chrom = ind[:]
history_best_mass.append(best_feasible_mass
if best_feasible_chrom is not None
else float("nan"))
current = results[0]
if current.fitness < best_score:
best_score = current.fitness
best_seen = population[0][:]
stagnant = 0
else:
stagnant += 1
if verbose and (gen % 20 == 0 or gen == GENERATIONS - 1):
if best_feasible_chrom is not None:
print(f"Пок.{gen:4d} | допустимых "
f"{int(feasible_frac*POPULATION_SIZE):2d}/{POPULATION_SIZE} "
f"| лучшая допустимая масса = {best_feasible_mass:7.3f} кг")
else:
print(f"Пок.{gen:4d} | допустимых 0/{POPULATION_SIZE} "
f"| f* = {current.fitness:.3e} | "
f"наруш. = {current.violations:.3e}")
if stagnant >= STOP_WITHOUT_IMPROVEMENT:
if verbose:
print(f"Остановка: нет улучшения {stagnant} поколений.")
break
next_pop = population[:ELITISM]
while len(next_pop) < POPULATION_SIZE:
p1 = tournament(population, scores)
p2 = tournament(population, scores)
c1, c2 = crossover(p1, p2)
c1 = invert(mutate(c1, gen))
c2 = invert(mutate(c2, gen))
next_pop.append(c1)
if len(next_pop) < POPULATION_SIZE:
next_pop.append(c2)
population = next_pop
elapsed = time.time() - t0
if best_feasible_chrom is not None:
best = evaluate(best_feasible_chrom)
else:
best = evaluate(best_seen)
return best, elapsed, history_best, history_mean, history_best_mass, history_feasible_frac
# ============================================================
# 8. Визуализация
# ============================================================
def plot_truss(coords, members, forces=None, ax=None, title="Ферма"):
if ax is None:
_, ax = plt.subplots(figsize=(8, 5))
for e, (i, j) in enumerate(members):
x = [coords[i, 0], coords[j, 0]]
y = [coords[i, 1], coords[j, 1]]
if forces is not None and np.isfinite(forces[e]):
color = 'red' if forces[e] > 0 else 'blue'
lw = 1.0 + 3.0 * min(abs(forces[e]) / 1e5, 1.0)
else:
color = 'gray'; lw = 1.5
ax.plot(x, y, color=color, lw=lw, zorder=1)
ax.scatter(coords[:, 0], coords[:, 1], c='black', s=30, zorder=3)
for i, (x, y) in enumerate(coords):
ax.annotate(str(i), (x, y), textcoords="offset points",
xytext=(5, 5), fontsize=9)
for node, (fx, fy) in SUPPORTS.items():
x, y = coords[node]
marker = '^' if (fx and fy) else 'o'
ax.plot(x, y, marker=marker, markersize=12, color='green', zorder=4)
for node in range(len(coords)):
Fx, Fy = LOADS[node]
if Fx == 0 and Fy == 0:
continue
x, y = coords[node]
s = 5e-5
ax.arrow(x, y, Fx*s, Fy*s, head_width=0.05,
head_length=0.05, fc='orange', ec='orange', zorder=5)
ax.set_aspect('equal')
ax.set_title(title)
ax.grid(True, alpha=0.3)
def plot_convergence(hist_best, hist_mean, hist_mass, hist_frac):
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ax = axes[0]
ax.plot(hist_best, label='fitness (лучшее)', lw=2)
ax.set_yscale('log')
ax.set_xlabel('Поколение')
ax.set_ylabel('Fitness')
ax.set_title('Сходимость fitness')
ax.legend()
ax.grid(True, alpha=0.3)
ax = axes[1]
ax.plot(hist_mass, label='Лучшая допустимая масса, кг', lw=2, color='green')
ax.set_xlabel('Поколение')
ax.set_ylabel('Масса, кг')
ax.set_title('Лучшая допустимая масса')
ax2 = ax.twinx()
ax2.plot(hist_frac, label='Доля допустимых', color='orange',
lw=1.2, alpha=0.7)
ax2.set_ylabel('Доля допустимых', color='orange')
ax.legend(loc='upper right')
ax2.legend(loc='lower right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
return fig
# ============================================================
# 9. Вывод результата
# ============================================================
def print_result(r, elapsed, mass0=None):
print("\n" + "=" * 72)
print(" РЕЗУЛЬТАТ ОПТИМИЗАЦИИ")
print("=" * 72)
if mass0 is not None:
print(f" Масса исходная: {mass0:.3f} кг")
print(f" Масса оптимальная: {r.mass:.3f} кг")
if mass0 is not None and mass0 > 0:
print(f" Снижение массы: {(1 - r.mass/mass0)*100:.1f}%")
print(f" Допустимое решение: {'ДА' if r.feasible else 'НЕТ'}")
print(f" Нарушения: {r.violations:.4e}")
print(f" Время: {elapsed:.2f} с")
print(f"\n Координаты узлов, м:")
for i, xy in enumerate(r.coords):
print(f" Узел {i}: x = {xy[0]:.4f}, y = {xy[1]:.4f}")
print(f"\n Стержни:")
print(f" {'№':>3s} {'(i,j)':>8s} {'d, мм':>7s} "
f"{'N, кН':>9s} {'σ, МПа':>9s} {'Эйлер':>8s}")
for e, (i, j) in enumerate(MEMBERS):
print(f" {e:>3d} {f'({i},{j})':>8s} {r.diameters_mm[e]:7.2f} "
f"{r.axial_forces[e]/1e3:+9.3f} "
f"{r.stresses[e]/1e6:+9.2f} "
f"{r.buckling_ratios[e]:8.4f}")
print(f"\n Перемещения узлов, мм (результирующее):")
for i, uv in enumerate(r.displacements):
mag = math.hypot(uv[0], uv[1]) * 1000
print(f" Узел {i}: ux = {uv[0]*1000:+8.3f}, "
f"uy = {uv[1]*1000:+8.3f}, |u| = {mag:7.3f}")
print(f"\n Проверка ограничений:")
print(f" Max |σ| / σ_доп = {np.max(np.abs(r.stresses))/SIGMA_ALLOW:.3f}")
u_mag = np.max(np.hypot(r.displacements[:, 0], r.displacements[:, 1]))
print(f" Max |u| / u_доп = {u_mag/DISPLACEMENT_ALLOW:.3f}")
print(f" Max Эйлер = {np.max(r.buckling_ratios):.3f}")
if not r.feasible:
print(f"\n ВНИМАНИЕ: решение недопустимо. {r.reason}")
# ============================================================
# 10. Главная функция
# ============================================================
def main():
print("=" * 72)
print(" ОПТИМИЗАЦИЯ ПЛОСКОЙ ФЕРМЫ: МКЭ + ГА (v16)")
print("=" * 72)
# Проверка жёсткости исходной схемы
print("\n Проверка геометрической неизменяемости исходной схемы:")
try:
rank, n_dof, cond = verify_rigidity(NODES)
print(f" Свободных DOF: {n_dof}, ранг K_ff: {rank}, "
f"cond = {cond:.2e}")
if rank < n_dof:
print(" !!! СХЕМА ГЕОМЕТРИЧЕСКИ ИЗМЕНЯЕМА !!!")
return
print(" Схема геометрически неизменяема.")
except ValueError as exc:
print(f" Ошибка проверки: {exc}")
return
print(f"\n Материал: сталь С245")
print(f" E = {E/1e9:.0f} ГПа, ρ = {DENSITY} кг/м³, "
f"σ_доп = {SIGMA_ALLOW/1e6:.0f} МПа")
print(f" Сортамент: круглые стержни сплошного сечения, "
f"d = {DIAMETERS_MM[0]:.0f}–{DIAMETERS_MM[-1]:.0f} мм")
print(f" Узлов: {len(NODES)}, стержней: {len(MEMBERS)}, "
f"опорных связей: {sum(sum(v) for v in SUPPORTS.values())}")
m = len(MEMBERS); r_ = sum(sum(v) for v in SUPPORTS.values())
j = len(NODES)
print(f" W = m + r - 2j = {m} + {r_} - {2*j} = {m + r_ - 2*j} "
f"(степень статической неопределимости)")
# Исходная ферма: d = 25 мм
d0 = 25.0
k0 = int(np.argmin(np.abs(DIAMETERS_MM - d0)))
areas0 = AREAS[k0] * np.ones(len(MEMBERS))
active0 = np.ones(len(MEMBERS), dtype=bool)
try:
u0, f0, s0, L0 = assemble_and_solve(NODES, areas0, active0)
mass0 = float(np.sum(DENSITY * areas0 * L0))
print(f"\n Исходная ферма (все d = {d0:.0f} мм):")
print(f" Масса: {mass0:.3f} кг")
u0_mag = np.max(np.hypot(u0[:, 0], u0[:, 1]))
print(f" Max |u| = {u0_mag*1000:.3f} мм")
print(f" Max |σ| = {np.max(np.abs(s0))/1e6:.2f} МПа")
ratios = []
for e in range(len(MEMBERS)):
if f0[e] < 0:
L_eff = MU_BUCKLING * L0[e]
p_cr = math.pi**2 * E * INERTIAS[k0] / L_eff**2
ratios.append(abs(f0[e]) / p_cr)
print(f" Max Эйлер = {max(ratios) if ratios else 0:.3f}")
except ValueError as exc:
print(f"\n Исходная ферма не решается: {exc}")
return
print("\n Запуск генетического алгоритма...")
(result, elapsed, hb, hm, hbm, hbf) = optimize(verbose=True)
print_result(result, elapsed, mass0)
# Визуализация
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
plot_truss(NODES, MEMBERS, forces=f0,
ax=axes[0], title=f"Исходная ферма (M = {mass0:.2f} кг)")
plot_truss(result.coords, MEMBERS, forces=result.axial_forces,
ax=axes[1],
title=f"Оптимальная ферма (M = {result.mass:.2f} кг)")
plt.tight_layout()
plt.savefig('truss_result.png', dpi=120)
plot_convergence(hb, hm, hbm, hbf)
plt.savefig('convergence.png', dpi=120)
print("\n Графики: truss_result.png, convergence.png")
plt.show()
if __name__ == "__main__":
main()