Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions experiment_runner_3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import subprocess
import os
import yaml # Wymaga: pip install pyyaml
from pathlib import Path

# --- KONFIGURACJA ŚCIEŻEK ---
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
RESULTS_DIR = os.path.join(PROJECT_ROOT, "experiment_results_mutations")
GENERATED_CONFIGS_DIR = os.path.join(PROJECT_ROOT, "generated_configs")

TEMPLATE_FILE = os.path.join(PROJECT_ROOT, "config_template.yaml")

USER_HOME = str(Path.home())
GUROBI_LICENSE = os.path.join(USER_HOME, "development", "gurobi.lic")

ITERATIONS = [100, 500, 1000]
HEURISTIC = "avg"

# Definicje scenariuszy mutacji.
# Klucz: Nazwa scenariusza (używana w nazwach plików).
# Wartość: Lista ciągów znaków. Jeśli typ mutacji (z pola 'type' w yaml) zawiera
# którykolwiek z tych ciągów, mutacja zostaje włączona.
MUTATION_SETS = {
# 1. Wszystkie mutacje dostępne w szablonie
"All": ["ALL"],

# 2. Tylko generacja i selekcja (brak mutacji w sensie modyfikacji istniejących)
"None": [],

# 3. Przykład: Tylko mutacje liczbowe (IntMutation)
"Only_Int": ["IntMutation"],

# 4. Przykład: Wszystko OPRÓCZ krzyżowania (zakładając nazwę klasy SmartCrossover)
# Tu musimy wymienić wszystkie INNE, które chcemy zachować.
# To podejście "whitelist" jest bezpieczniejsze.
"No_Crossover": ["IntMutation", "BoolMutation", "TypeAwareMutation", "ReplaceTokenMutation"]
}

PROBLEMS = {
"Accap": {
"model": "models/mznc2024_probs/accap/accap.mzn",
"data": "models/accap_a10_f80_t50.json",
"sols": "models/accap_sols_a10.csv"
},
"Community": {
"model": "models/mznc2024_probs/community-detection/community-detection.mzn",
"data": "models/mznc2024_probs/community-detection/Zakhary.s12.k3.dzn",
"sols": "models/community.s12.k3.csv"
},
}

def ensure_dir(directory):
if not os.path.exists(directory):
os.makedirs(directory)

def generate_config_file(scenario_name, allowed_keys):
ensure_dir(GENERATED_CONFIGS_DIR)

if not os.path.exists(TEMPLATE_FILE):
print(f"CRITICAL ERROR: Nie znaleziono pliku {TEMPLATE_FILE}!")
print("Skopiuj swój 'config.yaml' jako 'config_template.yaml' przed uruchomieniem.")
exit(1)

with open(TEMPLATE_FILE, 'r') as f:
config_data = yaml.safe_load(f)

if "ALL" in allowed_keys:
pass
else:
original_mutations = config_data.get("mutations", [])
filtered_mutations = []

for m in original_mutations:
m_type = m.get("type", "")

if any(key in m_type for key in allowed_keys):
filtered_mutations.append(m)

config_data["mutations"] = filtered_mutations

output_filename = f"config_{scenario_name}.yaml"
output_path = os.path.join(GENERATED_CONFIGS_DIR, output_filename)

with open(output_path, 'w') as f:
yaml.dump(config_data, f, default_flow_style=False)

return output_path, len(config_data.get("mutations", []))

def run_experiment():
ensure_dir(RESULTS_DIR)

total_runs = len(PROBLEMS) * len(ITERATIONS) * len(MUTATION_SETS)
current_run = 0

print(f"#### rozpoczynam eksperyment 3: {total_runs} przebiegów ####")

for prob_name, paths in PROBLEMS.items():

for mut_set_name, allowed_keys in MUTATION_SETS.items():

config_path, mut_count = generate_config_file(mut_set_name, allowed_keys)

for iter_count in ITERATIONS:
current_run += 1

base_name = f"{prob_name}_{mut_set_name}_i{iter_count}"
output_csv = os.path.join(RESULTS_DIR, f"{base_name}.csv")
picked_csv = os.path.join(RESULTS_DIR, f"{base_name}_picked.csv")
checkpoint = os.path.join(RESULTS_DIR, f"{base_name}.bin")

print(f"\n[{current_run}/{total_runs}] {prob_name} | Mut: {mut_set_name} (n={mut_count}) | Iter: {iter_count}")
print(f" Config: {config_path}")

sbt_args = (
f'run {paths["model"]} '
f'-D {paths["data"]} '
f'-s {paths["sols"]} '
f'-i {iter_count} '
f'-e {HEURISTIC} '
f'-o "{output_csv}" '
f'-p "{picked_csv}" '
f'-c "{checkpoint}" '
f'--config "{config_path}" '
f'--gurobi-license "{GUROBI_LICENSE}"'
)

full_command = f'sbt "{sbt_args}"'

try:
subprocess.run(full_command, shell=True, cwd=PROJECT_ROOT, check=True)
print("#### pomyślne zakończenie eksperymentu ####")
except subprocess.CalledProcessError:
print(f"błąd: eksperyment {base_name} zakończony niepowodzeniem")

if __name__ == "__main__":
run_experiment()
2 changes: 2 additions & 0 deletions src/main/scala/com/beepboop/app/MainApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ object MainApp extends LogTrait {
if (configOpt.isEmpty) return
val config = configOpt.get

com.beepboop.app.dataprovider.ConfigLoader.initialize(config.configPath)

info("--- Step 1: Configuration ---")
info(s"Model: ${config.modelPath}")
info(s"Max Iterations: ${config.maxIterations}")
Expand Down
4 changes: 4 additions & 0 deletions src/main/scala/com/beepboop/app/components/Expression.scala
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ abstract class Expression[ReturnT](implicit val ct: ClassTag[ReturnT]) extends L
case c: ComposableExpression => 1 + c.children.map(_.exprDepth).sum
case _ => 1
}
def symbolCount: Int = this match {
case c: ComposableExpression => c.children.map(_.symbolCount).sum
case _ => 1
}
}

case class Variable[ReturnT : ClassTag ](name: String) extends Expression[ReturnT] {
Expand Down
29 changes: 20 additions & 9 deletions src/main/scala/com/beepboop/app/cpicker/ConstraintPicker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ case class ConstraintData(
constraint: List[Expression[?]],
heuristics: Double,
solCount: Long,
exprDepth: Int,
nestingDepth: Int,
symbolCount: Int,
distributionScore: Double
)

Expand All @@ -29,30 +30,36 @@ object ConstraintPicker extends LogTrait {
this.config = config
}

private def order(item: ConstraintData): Double = item.distributionScore
private def order(item: ConstraintData): Double = {
item.solCount.toDouble * item.distributionScore
}

var queue: mutable.PriorityQueue[ConstraintData] = mutable.PriorityQueue[ConstraintData]()(Ordering.by(order))

def runInitial(nodes: mutable.Set[ModelNodeTMP], maxRounds: Int = 3, keepRatio: Double = 0.5, threshold: Int = 500): Unit = {
val (distMean, distStd) = DistributionScorer.getParams(config.modelPath)
info(s"Distribution Params for '${config.modelPath}': Mean=$distMean, Std=$distStd")

val initialWorkload = nodes.par
val validSingles = new mutable.ListBuffer[Expression[?]]()
val round1Results = new mutable.ListBuffer[ConstraintData]()

info("Starting single evaluation using minizinc with Normal Distribution Scoring")
info("Starting single evaluation using minizinc with Symbol Count Scoring")

initialWorkload.foreach { tmpNode =>
val expr = tmpNode.constraint

val size = expr.exprDepth
val distScore = DistributionScorer.scoreNormal(size)
val depth = expr.exprDepth
val symbols = expr.symbolCount
val distScore = DistributionScorer.scoreNormal(symbols, distMean, distStd)

val tmpFile = ConstraintSaver.save(expr)
val runner = new Runner(this.config)
val solCount = runner.run(tmpFile)

solCount match {
case Some(value) if (value >= threshold) => {
val data = ConstraintData(List(expr), tmpNode.f, value, size, distScore)
val data = ConstraintData(List(expr), tmpNode.f, value, depth, symbols, distScore)

queue.synchronized {
queue.enqueue(data)
Expand Down Expand Up @@ -92,16 +99,20 @@ object ConstraintPicker extends LogTrait {

batchWorkload.foreach { group =>

val totalSize = group.map(_.exprDepth).sum
val distScore = DistributionScorer.scoreNormal(totalSize)
val totalDepth = group.map(_.exprDepth).sum
val totalSymbols = group.map(_.symbolCount).sum

val avgDistScore = group.map(expr =>
DistributionScorer.scoreNormal(expr.symbolCount, distMean, distStd)
).sum / group.size

val tmpFile = ConstraintSaver.save(group: _*)
val runner = new Runner(this.config)
val solCount = runner.run(tmpFile)

solCount match {
case Some(value) if (value >= threshold) => {
val data = ConstraintData(group, 0.0, value, totalSize, distScore)
val data = ConstraintData(group, 0.0, value, totalDepth, totalSymbols, avgDistScore)
queue.synchronized {
queue.enqueue(data)
}
Expand Down
26 changes: 21 additions & 5 deletions src/main/scala/com/beepboop/app/cpicker/DistributionScorer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,29 @@ package com.beepboop.app.cpicker
import scala.math._

object DistributionScorer {
val DefaultMean: Double = 20.0
val DefaultStd: Double = 10.0
val DefaultMean: Double = 8.0
val DefaultStd: Double = 4.0

def scoreNormal(length: Int, mean: Double = DefaultMean, stdDev: Double = DefaultStd): Double = {
if (stdDev == 0) return if (length == mean) 1.0 else 0.0
private val ProblemStats = Map(
"accap" -> (7.75, 3.77),
"community" -> (8.60, 3.20),
"concert" -> (7.00, 1.90),
"hoist" -> (10.22, 7.73),
"network" -> (5.80, 3.06),
"efm" -> (5.80, 3.06)
)

def getParams(modelPath: String): (Double, Double) = {
val lowerPath = modelPath.toLowerCase
ProblemStats.find { case (key, _) => lowerPath.contains(key) }
.map(_._2)
.getOrElse((DefaultMean, DefaultStd))
}

def scoreNormal(symbolCount: Int, mean: Double = DefaultMean, stdDev: Double = DefaultStd): Double = {
if (stdDev == 0) return if (symbolCount == mean) 1.0 else 0.0

val variance = stdDev * stdDev
exp(-pow(length - mean, 2) / (2 * variance))
exp(-pow(symbolCount - mean, 2) / (2 * variance))
}
}
38 changes: 25 additions & 13 deletions src/main/scala/com/beepboop/app/dataprovider/ConfigLoader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,45 @@ package com.beepboop.app.dataprovider

import com.beepboop.app.mutations.Mutation
import pureconfig.ConfigReader

import pureconfig.ConfigSource

case class ClassLogConfig(
level: String = "INFO",
enabled: Boolean = false
)derives ConfigReader

) derives ConfigReader

case class LogConfig(
enabled: Boolean = true,
logDebug: Boolean = false,
classes: Map[String, ClassLogConfig]
)derives ConfigReader


case class AlgorithmConfig(
expressionWeights: Map[String, Double],
mutations: List[Mutation],
logging: LogConfig
) derives ConfigReader

case class AlgorithmConfig(
expressionWeights: Map[String, Double],
mutations: List[Mutation],
logging: LogConfig
) derives ConfigReader

object ConfigLoader {
import pureconfig.module.yaml._
import pureconfig.module.yaml.*

private var _settings: Option[AlgorithmConfig] = None

val settings: AlgorithmConfig = YamlConfigSource.file("config.yaml")
.loadOrThrow[AlgorithmConfig]
def initialize(path: String): Unit = {
val loaded = YamlConfigSource.file(path).loadOrThrow[AlgorithmConfig]
_settings = Some(loaded)
println(s"Configuration loaded from: $path")
}

def settings: AlgorithmConfig = {
_settings match {
case Some(s) => s
case None =>
println("WARNING: ConfigLoader not initialized explicitly. Loading default 'config.yaml'.")
initialize("config.yaml")
_settings.get
}
}

def getWeight(componentName: String): Double = {
settings.expressionWeights.getOrElse(componentName, 0.0)
Expand Down
5 changes: 5 additions & 0 deletions src/main/scala/com/beepboop/app/utils/ArgumentParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ case class GeneratorConfig(
@arg(short = 'r', doc = "Resume from checkpoint if exists")
resume: Flag,

@arg(name = "config", short = 'C', doc = "Path to the configuration YAML file")
configPath: String = "config.yaml",

@arg(name = "debug", short = 'd', doc = "Launch Visual Debugger GUI instead of running search")
debug: Flag,

Expand All @@ -57,6 +60,7 @@ case class AppConfig(
pickedOutputCsv: String,
checkpointFile: String,
resume: Boolean,
configPath: String,
debug: Boolean,
gurobiLicense: String,
analyze: Boolean,
Expand Down Expand Up @@ -114,6 +118,7 @@ object ArgumentParser {
pickedOutputCsv = cli.pickedOutputCsv,
checkpointFile = cli.checkpointFile,
resume = cli.resume.value,
configPath = cli.configPath,
debug = cli.debug.value,
gurobiLicense = cli.gurobiLicense,
analyze = cli.analyze.value,
Expand Down