Add variable-length genome search space and objective function (Stage 6 pt.1)

- 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>
This commit is contained in:
jze9
2026-07-06 20:26:16 +05:00
parent 069ea848e2
commit 56c7ab9c1d
4 changed files with 548 additions and 0 deletions

60
tests/test_objective.py Normal file
View File

@@ -0,0 +1,60 @@
import random
from gausse.components.database import ComponentDatabase
from gausse.optim.objective import compute_cost_rub, decoded_summary, evaluate
from gausse.optim.search_space import SearchBounds, decode, sample_genome
DB = ComponentDatabase.load()
BOUNDS = SearchBounds()
def test_evaluate_feasible_genome_has_fitness_equal_efficiency():
rng = random.Random(2)
found_feasible = False
for _ in range(50):
genome = sample_genome(DB, BOUNDS, rng)
result = evaluate(genome, DB, BOUNDS)
if result.feasible:
found_feasible = True
assert result.fitness == result.efficiency
assert 0.0 <= result.efficiency <= 1.0
assert result.cost_rub > 0
break
assert found_feasible, "ни один из 50 случайных геномов не оказался реализуемым"
def test_evaluate_infeasible_genome_has_negative_fitness_and_reason():
rng = random.Random(9)
genome = sample_genome(DB, BOUNDS, rng)
# индукционный датчик требует скорости >= порог/чувствительность = 5 м/с,
# а старт по умолчанию 3 м/с -- первая ступень обязана провалиться
inductive_idx = next(i for i, s in enumerate(DB.sensors) if s.kind == "inductive")
genome.stages[0].sensor_idx = inductive_idx
result = evaluate(genome, DB, BOUNDS)
assert not result.feasible
assert result.fitness < 0
assert result.reason is not None
assert result.failed_stage_index == 0
def test_compute_cost_rub_is_positive_and_scales_with_stage_count():
rng = random.Random(4)
genome = sample_genome(DB, BOUNDS, rng)
config, _, _ = decode(genome, DB, BOUNDS)
cost = compute_cost_rub(config, DB)
assert cost > 0
genome.stages.append(genome.stages[0])
genome.inter_stage_gaps_m.append(0.05)
config2, _, _ = decode(genome, DB, BOUNDS)
cost2 = compute_cost_rub(config2, DB)
assert cost2 > cost
def test_decoded_summary_uses_real_part_numbers():
rng = random.Random(6)
genome = sample_genome(DB, BOUNDS, rng)
summary = decoded_summary(genome, DB)
assert len(summary["stages"]) == len(genome.stages)
real_wire_ids = {w.part_id for w in DB.wires}
assert all(s["wire"] in real_wire_ids for s in summary["stages"])