- optim/search_space.py: Genome encodes only indices into the real component database plus continuous/discrete geometry parameters; number of stages is itself mutable via add/remove/duplicate-stage operators. repair() clips everything back into bounds after mutation/crossover so decode() never sees an invalid genome. - optim/objective.py: evaluate() decodes a genome, computes real BOM cost from wire length/price and component prices, runs the full chain, and scores fitness = efficiency for feasible runs or a soft penalty scaled by how many stages it got through for infeasible ones (so the GA gets gradient instead of a wall). Tested against the real component database (not synthetic fixtures) — including an explicit infeasible case using the real inductive sensor's actual sensitivity/threshold values. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
298 lines
12 KiB
Python
298 lines
12 KiB
Python
"""Пространство поиска: геном переменной длины (число ступеней эволюционирует).
|
||
|
||
Геном кодирует только ИНДЕКСЫ в базу реальных компонентов (не сами
|
||
параметры) + непрерывные величины (расстояния, напряжение, геометрия
|
||
снаряда). Это гарантирует, что что бы ни нашёл поиск, оно собрано из
|
||
реально продающихся деталей, а не из выдуманных чисел.
|
||
"""
|
||
|
||
import copy
|
||
import math
|
||
import random
|
||
from dataclasses import dataclass, field
|
||
|
||
from gausse.components.database import ComponentDatabase
|
||
from gausse.sim.coilgun import CoilgunConfig
|
||
from gausse.sim.stage import ProjectileConfig, StageConfig
|
||
|
||
TUBE_WALL_CLEARANCE_M = 0.001
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class SearchBounds:
|
||
min_stages: int = 1
|
||
max_stages: int = 4
|
||
turns_per_layer_min: int = 5
|
||
turns_per_layer_max: int = 100
|
||
layers_min: int = 1
|
||
layers_max: int = 8
|
||
sensor_to_coil_distance_m_min: float = 0.005
|
||
sensor_to_coil_distance_m_max: float = 0.05
|
||
inter_stage_gap_m_min: float = 0.01
|
||
inter_stage_gap_m_max: float = 0.10
|
||
tube_od_m_min: float = 0.009
|
||
tube_od_m_max: float = 0.02
|
||
projectile_diameter_m_min: float = 0.004
|
||
projectile_diameter_m_max: float = 0.008
|
||
projectile_length_m_min: float = 0.01
|
||
projectile_length_m_max: float = 0.03
|
||
charge_voltage_fraction_min: float = 0.5
|
||
charge_voltage_fraction_max: float = 1.0
|
||
initial_launch_velocity_mps: float = 3.0
|
||
initial_approach_margin_m: float = 0.02
|
||
|
||
|
||
@dataclass
|
||
class StageGene:
|
||
wire_idx: int
|
||
capacitor_idx: int
|
||
switch_idx: int
|
||
sensor_idx: int
|
||
turns_per_layer: int
|
||
layers: int
|
||
sensor_to_coil_distance_m: float
|
||
charge_voltage_fraction: float
|
||
|
||
|
||
@dataclass
|
||
class ProjectileGene:
|
||
material_idx: int
|
||
diameter_m: float
|
||
length_m: float
|
||
|
||
|
||
@dataclass
|
||
class Genome:
|
||
tube_od_m: float
|
||
stages: list[StageGene]
|
||
inter_stage_gaps_m: list[float]
|
||
projectile: ProjectileGene
|
||
|
||
|
||
def _clip(value: float, lo: float, hi: float) -> float:
|
||
return max(lo, min(hi, value))
|
||
|
||
|
||
def sample_stage_gene(db: ComponentDatabase, bounds: SearchBounds, rng: random.Random) -> StageGene:
|
||
return StageGene(
|
||
wire_idx=rng.randrange(len(db.wires)),
|
||
capacitor_idx=rng.randrange(len(db.capacitors)),
|
||
switch_idx=rng.randrange(len(db.switches)),
|
||
sensor_idx=rng.randrange(len(db.sensors)),
|
||
turns_per_layer=rng.randint(bounds.turns_per_layer_min, bounds.turns_per_layer_max),
|
||
layers=rng.randint(bounds.layers_min, bounds.layers_max),
|
||
sensor_to_coil_distance_m=rng.uniform(
|
||
bounds.sensor_to_coil_distance_m_min, bounds.sensor_to_coil_distance_m_max
|
||
),
|
||
charge_voltage_fraction=rng.uniform(
|
||
bounds.charge_voltage_fraction_min, bounds.charge_voltage_fraction_max
|
||
),
|
||
)
|
||
|
||
|
||
def sample_genome(db: ComponentDatabase, bounds: SearchBounds, rng: random.Random) -> Genome:
|
||
tube_od_m = rng.uniform(bounds.tube_od_m_min, bounds.tube_od_m_max)
|
||
max_diameter = min(bounds.projectile_diameter_m_max, tube_od_m - TUBE_WALL_CLEARANCE_M)
|
||
diameter_m = rng.uniform(bounds.projectile_diameter_m_min, max(max_diameter, bounds.projectile_diameter_m_min))
|
||
|
||
n_stages = rng.randint(bounds.min_stages, bounds.max_stages)
|
||
stages = [sample_stage_gene(db, bounds, rng) for _ in range(n_stages)]
|
||
gaps = [
|
||
rng.uniform(bounds.inter_stage_gap_m_min, bounds.inter_stage_gap_m_max)
|
||
for _ in range(n_stages - 1)
|
||
]
|
||
projectile = ProjectileGene(
|
||
material_idx=rng.randrange(len(db.projectile_materials)),
|
||
diameter_m=diameter_m,
|
||
length_m=rng.uniform(bounds.projectile_length_m_min, bounds.projectile_length_m_max),
|
||
)
|
||
return Genome(tube_od_m=tube_od_m, stages=stages, inter_stage_gaps_m=gaps, projectile=projectile)
|
||
|
||
|
||
def repair(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> Genome:
|
||
"""Приводит геном в границы после мутации/скрещивания (клэмп, не отбраковка)."""
|
||
genome.tube_od_m = _clip(genome.tube_od_m, bounds.tube_od_m_min, bounds.tube_od_m_max)
|
||
max_diameter = max(genome.tube_od_m - TUBE_WALL_CLEARANCE_M, bounds.projectile_diameter_m_min)
|
||
genome.projectile.diameter_m = _clip(
|
||
genome.projectile.diameter_m, bounds.projectile_diameter_m_min, max_diameter
|
||
)
|
||
genome.projectile.length_m = _clip(
|
||
genome.projectile.length_m, bounds.projectile_length_m_min, bounds.projectile_length_m_max
|
||
)
|
||
genome.projectile.material_idx = genome.projectile.material_idx % len(db.projectile_materials)
|
||
|
||
for stage in genome.stages:
|
||
stage.wire_idx %= len(db.wires)
|
||
stage.capacitor_idx %= len(db.capacitors)
|
||
stage.switch_idx %= len(db.switches)
|
||
stage.sensor_idx %= len(db.sensors)
|
||
stage.turns_per_layer = int(_clip(stage.turns_per_layer, bounds.turns_per_layer_min, bounds.turns_per_layer_max))
|
||
stage.layers = int(_clip(stage.layers, bounds.layers_min, bounds.layers_max))
|
||
stage.sensor_to_coil_distance_m = _clip(
|
||
stage.sensor_to_coil_distance_m,
|
||
bounds.sensor_to_coil_distance_m_min,
|
||
bounds.sensor_to_coil_distance_m_max,
|
||
)
|
||
stage.charge_voltage_fraction = _clip(
|
||
stage.charge_voltage_fraction,
|
||
bounds.charge_voltage_fraction_min,
|
||
bounds.charge_voltage_fraction_max,
|
||
)
|
||
genome.inter_stage_gaps_m = [
|
||
_clip(g, bounds.inter_stage_gap_m_min, bounds.inter_stage_gap_m_max)
|
||
for g in genome.inter_stage_gaps_m[: len(genome.stages) - 1]
|
||
]
|
||
while len(genome.inter_stage_gaps_m) < len(genome.stages) - 1:
|
||
genome.inter_stage_gaps_m.append(
|
||
(bounds.inter_stage_gap_m_min + bounds.inter_stage_gap_m_max) / 2
|
||
)
|
||
return genome
|
||
|
||
|
||
def decode(genome: Genome, db: ComponentDatabase, bounds: SearchBounds) -> tuple[CoilgunConfig, float, float]:
|
||
"""Возвращает (CoilgunConfig, initial_x_m, initial_v_mps)."""
|
||
material = db.projectile_materials[genome.projectile.material_idx % len(db.projectile_materials)]
|
||
projectile = ProjectileConfig(
|
||
material=material,
|
||
diameter_m=genome.projectile.diameter_m,
|
||
length_m=genome.projectile.length_m,
|
||
)
|
||
|
||
stage_configs = []
|
||
for gene in genome.stages:
|
||
wire = db.wires[gene.wire_idx % len(db.wires)]
|
||
capacitor = db.capacitors[gene.capacitor_idx % len(db.capacitors)]
|
||
switch = db.switches[gene.switch_idx % len(db.switches)]
|
||
sensor = db.sensors[gene.sensor_idx % len(db.sensors)]
|
||
max_voltage = min(capacitor.voltage_v, switch.max_voltage_v)
|
||
charge_voltage_v = gene.charge_voltage_fraction * max_voltage
|
||
stage_configs.append(
|
||
StageConfig(
|
||
wire=wire,
|
||
capacitor=capacitor,
|
||
switch=switch,
|
||
sensor=sensor,
|
||
tube_od_m=genome.tube_od_m,
|
||
turns_per_layer=gene.turns_per_layer,
|
||
layers=gene.layers,
|
||
sensor_to_coil_distance_m=gene.sensor_to_coil_distance_m,
|
||
charge_voltage_v=charge_voltage_v,
|
||
)
|
||
)
|
||
|
||
config = CoilgunConfig(
|
||
stages=stage_configs,
|
||
inter_stage_gaps_m=list(genome.inter_stage_gaps_m),
|
||
projectile=projectile,
|
||
initial_x_m=-(stage_configs[0].sensor_to_coil_distance_m + bounds.initial_approach_margin_m),
|
||
initial_v_mps=bounds.initial_launch_velocity_mps,
|
||
)
|
||
return config, config.initial_x_m, config.initial_v_mps
|
||
|
||
|
||
def mutate(genome: Genome, db: ComponentDatabase, bounds: SearchBounds, rng: random.Random, rate: float = 0.2) -> Genome:
|
||
child = copy.deepcopy(genome)
|
||
|
||
if rng.random() < rate:
|
||
child.tube_od_m = rng.uniform(bounds.tube_od_m_min, bounds.tube_od_m_max)
|
||
|
||
for stage in child.stages:
|
||
if rng.random() < rate:
|
||
stage.wire_idx = rng.randrange(len(db.wires))
|
||
if rng.random() < rate:
|
||
stage.capacitor_idx = rng.randrange(len(db.capacitors))
|
||
if rng.random() < rate:
|
||
stage.switch_idx = rng.randrange(len(db.switches))
|
||
if rng.random() < rate:
|
||
stage.sensor_idx = rng.randrange(len(db.sensors))
|
||
if rng.random() < rate:
|
||
stage.turns_per_layer += rng.randint(-10, 10)
|
||
if rng.random() < rate:
|
||
stage.layers += rng.randint(-1, 1)
|
||
if rng.random() < rate:
|
||
stage.sensor_to_coil_distance_m *= rng.uniform(0.7, 1.3)
|
||
if rng.random() < rate:
|
||
stage.charge_voltage_fraction *= rng.uniform(0.9, 1.1)
|
||
|
||
for i in range(len(child.inter_stage_gaps_m)):
|
||
if rng.random() < rate:
|
||
child.inter_stage_gaps_m[i] *= rng.uniform(0.7, 1.3)
|
||
|
||
if rng.random() < rate:
|
||
child.projectile.material_idx = rng.randrange(len(db.projectile_materials))
|
||
if rng.random() < rate:
|
||
child.projectile.diameter_m *= rng.uniform(0.8, 1.2)
|
||
if rng.random() < rate:
|
||
child.projectile.length_m *= rng.uniform(0.8, 1.2)
|
||
|
||
# структурные операторы: число ступеней тоже эволюционирует
|
||
structural_roll = rng.random()
|
||
if structural_roll < rate / 3 and len(child.stages) < bounds.max_stages:
|
||
new_stage = sample_stage_gene(db, bounds, rng)
|
||
child.stages.append(new_stage)
|
||
child.inter_stage_gaps_m.append(
|
||
rng.uniform(bounds.inter_stage_gap_m_min, bounds.inter_stage_gap_m_max)
|
||
)
|
||
elif structural_roll < 2 * rate / 3 and len(child.stages) > bounds.min_stages:
|
||
idx = rng.randrange(len(child.stages))
|
||
child.stages.pop(idx)
|
||
if child.inter_stage_gaps_m:
|
||
child.inter_stage_gaps_m.pop(min(idx, len(child.inter_stage_gaps_m) - 1))
|
||
elif structural_roll < rate and len(child.stages) < bounds.max_stages:
|
||
idx = rng.randrange(len(child.stages))
|
||
child.stages.insert(idx, copy.deepcopy(child.stages[idx]))
|
||
child.inter_stage_gaps_m.append(
|
||
rng.uniform(bounds.inter_stage_gap_m_min, bounds.inter_stage_gap_m_max)
|
||
)
|
||
|
||
return repair(child, db, bounds)
|
||
|
||
|
||
def crossover(parent_a: Genome, parent_b: Genome, rng: random.Random) -> Genome:
|
||
"""Обмен целыми ступенями между родителями — ступень физически цельная единица."""
|
||
n = rng.choice([len(parent_a.stages), len(parent_b.stages)])
|
||
stages = []
|
||
for i in range(n):
|
||
source = parent_a if rng.random() < 0.5 else parent_b
|
||
if i < len(source.stages):
|
||
stages.append(copy.deepcopy(source.stages[i]))
|
||
else:
|
||
fallback = parent_a if source is parent_b else parent_b
|
||
stages.append(copy.deepcopy(fallback.stages[i % len(fallback.stages)]))
|
||
|
||
gaps = []
|
||
for i in range(n - 1):
|
||
source = parent_a if rng.random() < 0.5 else parent_b
|
||
if i < len(source.inter_stage_gaps_m):
|
||
gaps.append(source.inter_stage_gaps_m[i])
|
||
else:
|
||
gaps.append(parent_a.inter_stage_gaps_m[i % max(len(parent_a.inter_stage_gaps_m), 1)])
|
||
|
||
projectile_source = parent_a if rng.random() < 0.5 else parent_b
|
||
tube_source = parent_a if rng.random() < 0.5 else parent_b
|
||
|
||
return Genome(
|
||
tube_od_m=tube_source.tube_od_m,
|
||
stages=stages,
|
||
inter_stage_gaps_m=gaps,
|
||
projectile=copy.deepcopy(projectile_source.projectile),
|
||
)
|
||
|
||
|
||
def genome_to_dict(genome: Genome) -> dict:
|
||
return {
|
||
"tube_od_m": genome.tube_od_m,
|
||
"stages": [vars(s) for s in genome.stages],
|
||
"inter_stage_gaps_m": genome.inter_stage_gaps_m,
|
||
"projectile": vars(genome.projectile),
|
||
}
|
||
|
||
|
||
def genome_from_dict(data: dict) -> Genome:
|
||
return Genome(
|
||
tube_od_m=data["tube_od_m"],
|
||
stages=[StageGene(**s) for s in data["stages"]],
|
||
inter_stage_gaps_m=list(data["inter_stage_gaps_m"]),
|
||
projectile=ProjectileGene(**data["projectile"]),
|
||
)
|