From f68bcb0c3a242545af0400a19e8f406a3c7bf65c Mon Sep 17 00:00:00 2001 From: Dika Date: Thu, 20 Nov 2025 16:36:47 +0700 Subject: [PATCH] chore(refactor): modularize GA - add utils, ga package, main runner, tests; refactor selection/mutation --- __pycache__/mutation.cpython-312.pyc | Bin 0 -> 11372 bytes __pycache__/utils.cpython-312.pyc | Bin 0 -> 9360 bytes __pycache__/utils.cpython-313.pyc | Bin 0 -> 9428 bytes ga/__init__.py | 9 + ga/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 630 bytes ga/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 630 bytes ga/__pycache__/evaluator.cpython-312.pyc | Bin 0 -> 2624 bytes ga/__pycache__/evaluator.cpython-313.pyc | Bin 0 -> 2641 bytes ga/__pycache__/operators.cpython-312.pyc | Bin 0 -> 7329 bytes ga/__pycache__/operators.cpython-313.pyc | Bin 0 -> 7252 bytes ga/__pycache__/population.cpython-312.pyc | Bin 0 -> 2025 bytes ga/__pycache__/population.cpython-313.pyc | Bin 0 -> 2033 bytes ga/evaluator.py | 59 ++++++ ga/operators.py | 129 +++++++++++++ ga/population.py | 32 ++++ main.py | 143 ++++++++++++++ mutation.py | 31 +-- populasi_awal_final.py | 15 ++ selection.py | 134 +++---------- tests/test_utils_and_ops.py | 41 ++++ utils.py | 218 ++++++++++++++++++++++ utlis.py | 27 ++- 22 files changed, 706 insertions(+), 132 deletions(-) create mode 100644 __pycache__/mutation.cpython-312.pyc create mode 100644 __pycache__/utils.cpython-312.pyc create mode 100644 __pycache__/utils.cpython-313.pyc create mode 100644 ga/__init__.py create mode 100644 ga/__pycache__/__init__.cpython-312.pyc create mode 100644 ga/__pycache__/__init__.cpython-313.pyc create mode 100644 ga/__pycache__/evaluator.cpython-312.pyc create mode 100644 ga/__pycache__/evaluator.cpython-313.pyc create mode 100644 ga/__pycache__/operators.cpython-312.pyc create mode 100644 ga/__pycache__/operators.cpython-313.pyc create mode 100644 ga/__pycache__/population.cpython-312.pyc create mode 100644 ga/__pycache__/population.cpython-313.pyc create mode 100644 ga/evaluator.py create mode 100644 ga/operators.py create mode 100644 ga/population.py create mode 100644 main.py create mode 100644 tests/test_utils_and_ops.py create mode 100644 utils.py diff --git a/__pycache__/mutation.cpython-312.pyc b/__pycache__/mutation.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..228de8fee9fc112ac24223f59c829294b9b5ccf6 GIT binary patch literal 11372 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 literal 0 HcmV?d00001 diff --git a/__pycache__/utils.cpython-313.pyc b/__pycache__/utils.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d83a8eddb4de37ad52c12ac8c50092b7949c6683 GIT binary patch literal 9428 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 H)t>(YBt~DK literal 0 HcmV?d00001 diff --git a/ga/__pycache__/population.cpython-312.pyc b/ga/__pycache__/population.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..004bd32b17fcc3683e8271533565fdb11c942c0a GIT binary patch literal 2025 zcmZuyUu;uV7(cgvZ*RNd*g9NQa$%qp*%A{$L84h05`mS)!2?aD+}wNG-R=IV=bVBg zts%-oNlX||BY}sK_%JgcO!NVx4?JpoX)WM#oQa9N%=l0;6BC~N&TTK9$k+6o-~I0Q zedqgo?oWMv3Ie+N^LF#oFhYNb2cP0yu<;3iC1jv1G9)8lNp>I`u!Gs49m<9z!3$d9 zY#8>C6|tk)DDc8qHX=BH6)}*TK8f6Qsy6Z5_bf~JS z)YdFZC7NTnwp#k1G0JT-&9t-94#Hv1&SC)SbsF zDLZsreHToW2%M`6C-P3^yf^7^w5v*#)1%@)C9re8;}A9T+C|OE z7feU9#!Cz5L0l^_Y^cobd8A@TcMa1ijw|b;ROuiOoir)qp-EF`JUHPjWYQ8J6pl0O z-oX^?@UVePF5@x1Oc?}2D31`#%EZwF!c><84#Iz97?veeK}D$|T>>nS*k@2$ z%6x&+0TP8LBEv$3lBzyFbL3F&3^@1Uk#vT&ZbWrjN;d7Vx_dRuV9Gi)7x7VVuY`Sj651pw^7<4R67%O1 zCXfj)Jz`sYR6)M2O+3lxt#s@B^j?kp$8OKs6U|EG66pGR)hJ{{?q$RXJnOVdbfudW z$kQRnQ>+rM#NZbdU6evqWmB}z`4RN?UM$skMV=e%z3~xC`lu4GD86@ov=aL4a0pd` z&(z%ZL{s+V{T=lF5_-_*!OXfSH4%ABCwLfckRs+G-G!!%bgmN~rL*Ni!NNQ$7^VY5 zXlozAVmdr%VTVWbS=ZDtk7%V5b_}moiRgoLf{0Xm6GTJ;xYBD%F6DWQU>j~Qn0Br> zWxLb=l*5HocILgV1uw)*P%fD~?6JL}q+%4*O+*BPr@HqJOp4yuFJ>Q`nE2<^Pq3ECan&=w3NA?X$|eF#n$BhYyDUI7Z>iyJ02!qSUPw8Tz%qm zy8POg`ZuFr6~2D;dwTcacOSRLGp$!nwUW~{8mk>$8%ixrU7xDI+!{*Pj;H$i39qa-o&le_uY(9zp%P+9{M6 zZiJGgA0kUj>*C`F#f;|jhO6iEJi-=AkYXa5m4w71#+8WP;$iWPpiZ4tZbt%AdU>Xe zV7qhr?&!B?J@~`xjp>;m4{iwdBV~&;TxZKiZZEVEKwk`C<801jH41Z$dACub&3V== Ll>ZiDo=^V*^o;*o literal 0 HcmV?d00001 diff --git a/ga/__pycache__/population.cpython-313.pyc b/ga/__pycache__/population.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7ed259bcf475861c3236919694da97ed1f27b0b7 GIT binary patch literal 2033 zcmZuyO>7%Q6rT0^XZ@FwCQhmO(Kbn;hB_5eQwT+%s4a!0aih2^HHuYtwH_y%tY^*4 zZc8|zN?cqn4V9`?Y7ef&AxIpM;81=JJ;DhaS0NgLghVerAQA<^l{af|9ZE;qdEdT$ z@6G$2-AN{62-ek~o;N=VA@sX=@mIV9c4`1_pgf`|FXaQ1(nNp;CS)p41ZhweJb5xi zLqLO*;fV;1054Qf^5H_Dpny@7kND_lIIpD9Vvt7jiG0-O?vjiZVxDwIQ)nMm@^PWF zDAD-5oJr~_3887b>RP&E+C-hl1k*Nj9n&%$6En4JQ}y)tNGx`4!DMR1F1Z#~4VyT+ zNtmjutfE_%N_A4QD{A$B#z5Oy&>hvFSa-l&30r2xbTG}9;nbM1K<$dn>`Lb(mtoZ; zVANj1Ds>4PQQrm=RSM@C!pXxb#Z^l;uzJ~a7Qi+JpD0%K(j}c3xD@LI0ZL%!V%s5R z7WK=zRVcWx-lzf_6H2PeHRJVy#=vp`3PF~p^-|$1!6&JC2^UVzm{DBY&YSPSZ?T*}Nhmz1-umjOd*hgJDh_sMzQAN9CMP?sc zgD`({{-g>lJoVPx9v^AQx3-HX`Ml9itskc({2#krYmYRwa29lZJraSeDBY~&15Y|l zi;Q+M1GyT5Tt&~%o?p#^t3gC!yJCIH_r6OZFOMQ2aZQ=&>wfdmT*^o1zwtfuBURDV zafISXMVkCX-F?2UruaHO1>HY=UFdf?_fV9airi%qJOnq%Jmx{ehUN^nuN59)3vRh= zVIC0-lfW=m^!H&i370KQc-UC5O#}0=UaewM@>-UPe#j)KNVhjhR3w7Oy3NU@B9BsB zftw7bp2vt=Y0m-Ua51@L$?IP5Ld*u^lF36J+Z#?MMnc`Ss9^AP=l+3-@~*^U_c(eG z1bX3NKf-neZ43@ynb=J3U3qIgIaHH2`k!0PeUfYS57(lbO3$^Pk9$^@?<)r$q@G#5 zaQ#Ak{8P5}%IC&cLtm7?eEB00bT>gk)kw@=igw{mNX&1YY0 zrN(Npjo!Xmc(Zq4<-OZOH_clkYcH+$j@HKEq`Gt`(df_CPHpz5SEsH|)n91!XKJT5 z5__*rU7fn0K7RL9EB)?zVq!aj_8!|(P_*x7Wq(uIe?OgFE4R|4>&mh1{itVnD~%EZ zjbMuQKuj6wq4)_x86(AF$u^2b9(I%s4jHm37GjQu6p+p#^;K;2n8ac6l80rbTHHf9SRt5IHTEZL1JYc4s> Lvir9X^L+XjH23Mg literal 0 HcmV?d00001 diff --git a/ga/evaluator.py b/ga/evaluator.py new file mode 100644 index 0000000..ed9c1e4 --- /dev/null +++ b/ga/evaluator.py @@ -0,0 +1,59 @@ +"""Fitness evaluator: simulates battery and charging for delimiter-flat chromosomes.""" +from typing import List, Dict, Tuple, Any +from utils import split_routes, get_distance, INFEASIBLE_PENALTY + + +def fitness_function(chromosome: List[str], distance_matrix: Dict[Tuple[str, str], float], + battery_capacity: float = 100.0, consumption_rate: float = 1.0, + charging_rate: float = 1.0, w_distance: float = 0.6, w_charging: float = 0.4) -> float: + """Compute fitness: weighted sum of total distance and charging time. + + Returns a smaller-is-better fitness. Infeasible solutions get a large penalty. + Chromosome encoding: delimiter-flat list with 'S*' representing charging stations, + 'D*' depots and 'C*' customers. + """ + total_distance = 0.0 + total_charging_time = 0.0 + + routes = split_routes(chromosome) + + for route in routes: + battery = battery_capacity + for i in range(len(route) - 1): + a, b = route[i], route[i + 1] + dist = get_distance(a, b, distance_matrix) + if dist == float('inf'): + return INFEASIBLE_PENALTY + total_distance += dist + needed_energy = dist * consumption_rate + + # If current node is charging station, charge minimally to reach next station/depot + if a.startswith('S'): + # lookahead to next S or D + energy_needed_to_next = 0.0 + for j in range(i, len(route) - 1): + u, v = route[j], route[j + 1] + d2 = get_distance(u, v, distance_matrix) + if d2 == float('inf'): + return INFEASIBLE_PENALTY + energy_needed_to_next += d2 * consumption_rate + if route[j + 1].startswith('S') or route[j + 1].startswith('D'): + break + required_energy = max(0.0, energy_needed_to_next - battery) + charge_time = required_energy / charging_rate + total_charging_time += charge_time + battery += required_energy + if battery > battery_capacity: + battery = battery_capacity + + if battery < needed_energy: + # insufficient battery to travel (invalid) + return INFEASIBLE_PENALTY + + battery -= needed_energy + + # normalization + D = total_distance / 100.0 + C = total_charging_time / 100.0 + fitness = w_distance * D + w_charging * C + return fitness diff --git a/ga/operators.py b/ga/operators.py new file mode 100644 index 0000000..9578b7b --- /dev/null +++ b/ga/operators.py @@ -0,0 +1,129 @@ +"""Selection, crossover and mutation operators for GA. + +Functions expect delimiter-flat chromosome encoding (list[str]) where '|' separates routes. +""" +from typing import List, Tuple, Dict, Any +import random +import copy +from utils import split_routes, join_routes, get_distance + + +def get_elites(population: List[List[str]], fitness_scores: List[float], elite_size: int) -> List[List[str]]: + paired = list(zip(population, fitness_scores)) + paired.sort(key=lambda x: x[1]) + return [p for p, _ in paired[:elite_size]] + + +def tournament_selection(population: List[List[str]], fitness_scores: List[float], tournament_size: int) -> List[str]: + indices = random.sample(range(len(population)), k=min(tournament_size, len(population))) + best = None + best_score = float('inf') + for i in indices: + if fitness_scores[i] < best_score: + best_score = fitness_scores[i] + best = population[i] + return best + + +def selection(population: List[List[str]], fitness_scores: List[float], elite_size: int, tournament_size: int) -> List[List[str]]: + parents: List[List[str]] = [] + parents.extend(get_elites(population, fitness_scores, elite_size)) + remaining = len(population) - elite_size + for _ in range(remaining): + parents.append(tournament_selection(population, fitness_scores, tournament_size)) + return parents + + +# ---- BCRC crossover (from notebook) adapted ---- +def remove_customer_routes(routes: List[List[str]], customer: str) -> List[List[str]]: + return [[x for x in r if x != customer] for r in routes] + + +def insertion_cost(route: List[str], customer: str, dist: Dict[Tuple[str, str], float]) -> Tuple[float, int]: + if len(route) < 2: + return 0.0, 0 + best_cost = float('inf') + best_pos = 0 + for i in range(len(route) - 1): + a, b = route[i], route[i+1] + cost_before = get_distance(a, b, dist) + cost_after = get_distance(a, customer, dist) + get_distance(customer, b, dist) + delta = cost_after - cost_before + if delta < best_cost: + best_cost = delta + best_pos = i + 1 + return best_cost, best_pos + + +def bcrc_crossover(chrom1: List[str], chrom2: List[str], dist: Dict[Tuple[str, str], float]) -> List[str]: + p1 = copy.deepcopy(chrom1) + p2 = copy.deepcopy(chrom2) + routes1 = split_routes(p1) + routes2 = split_routes(p2) + all_customers = [x for r in routes1 for x in r if x.startswith('C')] + if not all_customers: + return chrom2.copy() + customer = random.choice(all_customers) + child_routes = remove_customer_routes(routes2, customer) + best_global_cost = float('inf') + best_route_idx = None + best_insert_pos = None + for idx, route in enumerate(child_routes): + if len(route) < 2: + continue + cost, pos = insertion_cost(route, customer, dist) + if cost < best_global_cost: + best_global_cost = cost + best_route_idx = idx + best_insert_pos = pos + if best_route_idx is not None: + child_routes[best_route_idx].insert(best_insert_pos, customer) + else: + for idx, route in enumerate(child_routes): + if len(route) >= 2: + child_routes[idx].insert(1, customer) + break + return join_routes(child_routes) + + +# ---- Mutation (simpler variant) ---- +def apply_mutation(chromosome: List[str], mutation_rate: float = 0.1) -> List[str]: + # simple swap mutation within a route + if random.random() > mutation_rate: + return chromosome + routes = split_routes(chromosome) + # pick a random route with at least two customers (excluding depots) + route_idx = None + for _ in range(5): + i = random.randrange(len(routes)) + customers = [x for x in routes[i] if x.startswith('C')] + if len(customers) >= 2: + route_idx = i + break + if route_idx is None: + return chromosome + # perform swap on customer positions within chosen route + route = routes[route_idx] + cust_positions = [i for i, g in enumerate(route) if g.startswith('C')] + a, b = random.sample(cust_positions, 2) + route[a], route[b] = route[b], route[a] + routes[route_idx] = route + return join_routes(routes) + + +# ---- Advanced mutation wrapper (uses legacy `mutation.py` implementation) ---- +def apply_mutation_advanced(chromosome: List[str], depot_locations: Dict[str, tuple], customer_locations: Dict[str, tuple], mutation_probability: float = 0.1, beta: float = 0.2) -> List[str]: + """Use existing sophisticated mutation logic from `mutation.py` if available. + + This function imports the original `apply_mutation` from `mutation.py` which + implements inter-depot and intra-depot strategies. It provides a thin wrapper + so callers can choose between the simple mutation above or the advanced one. + """ + try: + import mutation as mut + except Exception: + # fallback to simple mutation if module not importable + return apply_mutation(chromosome, mutation_rate=mutation_probability) + + # the legacy mutation expects depot/customer dicts and returns a new chromosome + return mut.apply_mutation(chromosome, depot_locations, customer_locations, mutation_probability=mutation_probability, beta=beta) diff --git a/ga/population.py b/ga/population.py new file mode 100644 index 0000000..689cd42 --- /dev/null +++ b/ga/population.py @@ -0,0 +1,32 @@ +"""Population generation utilities for GA. + +This module contains a small random population generator that creates delimiter-flat +chromosomes for use in smoke runs. For production, replace with the more advanced +generator in `populasi_awal_final.py` adapted to delimiter encoding. +""" +from typing import List, Dict, Any +import random +from utils import flatten_from_nested + + +def simple_route_from_depot_customers(depot: str, customers: List[str]) -> List[str]: + # simple route: depot -> customers in order -> depot + return [depot] + customers + [depot] + + +def generate_random_population(depots: List[str], customers: List[str], population_size: int = 10) -> List[List[str]]: + population: List[List[str]] = [] + for _ in range(population_size): + remaining = customers.copy() + random.shuffle(remaining) + # split into random chunks (random number of routes) + num_routes = random.randint(1, max(1, min(len(depots), len(customers)))) + # split customers roughly evenly + chunk_size = max(1, len(customers) // num_routes) + routes = [] + for i in range(0, len(customers), chunk_size): + chunk = remaining[i:i+chunk_size] + depot = random.choice(depots) + routes.append(simple_route_from_depot_customers(depot, chunk)) + population.append(flatten_from_nested(routes)) + return population diff --git a/main.py b/main.py new file mode 100644 index 0000000..4ec691e --- /dev/null +++ b/main.py @@ -0,0 +1,143 @@ +"""Minimal GA runner (smoke test) for Rute SPKLU problem. + +This script builds a tiny synthetic problem and runs a few GA generations to +demonstrate end-to-end wiring of utils, ga.population, ga.operators and ga.evaluator. +""" +from ga.population import generate_random_population +from ga.operators import selection, bcrc_crossover, apply_mutation +from ga.evaluator import fitness_function +from utils import build_distance_matrix, load_nodes_from_csv + +import argparse +import random +import os + + +def synthetic_nodes(): + # create small synthetic nodes. IDs: D1, S1, C1..C5 + nodes = { + 'D1': {'x': 0.0, 'y': 0.0, 'demand': 0}, + 'S1': {'x': 5.0, 'y': 0.0, 'demand': 0}, + 'C1': {'x': 2.0, 'y': 1.0, 'demand': 1}, + 'C2': {'x': 2.5, 'y': -1.0, 'demand': 1}, + 'C3': {'x': 4.0, 'y': 1.5, 'demand': 1}, + 'C4': {'x': -1.0, 'y': -0.5, 'demand': 1}, + 'C5': {'x': -2.0, 'y': 1.0, 'demand': 1}, + } + return nodes + + +def run_smoke(): + random.seed(1) + nodes = synthetic_nodes() + dist = build_distance_matrix(nodes) + + depots = ['D1'] + customers = ['C1', 'C2', 'C3', 'C4', 'C5'] + + pop = generate_random_population(depots, customers, population_size=8) + + generations = 20 + elite_size = 1 + tournament_size = 3 + + best = None + best_score = float('inf') + + for g in range(generations): + fitnesses = [fitness_function(ch, dist) for ch in pop] + # track best + for ch, f in zip(pop, fitnesses): + if f < best_score: + best_score = f + best = ch + + print(f"Gen {g}: best={best_score:.6f}") + + parents = selection(pop, fitnesses, elite_size=elite_size, tournament_size=tournament_size) + + # crossover + next_pop = [] + while len(next_pop) < len(pop): + a = random.choice(parents) + b = random.choice(parents) + child = bcrc_crossover(a, b, dist) + child = apply_mutation(child, mutation_rate=0.2) + next_pop.append(child) + + pop = next_pop + + print('\nBest chromosome:', best) + print('Best fitness:', best_score) + + +def run_from_csv(csv_path: str): + """Run GA using nodes loaded from a CSV file. + + The loader expects node IDs in the CSV that start with 'D' for depot, + 'S' for charging stations and 'C' for customers. If those prefixes are + not present, the function will attempt to infer depots/customers heuristically. + """ + nodes = load_nodes_from_csv(csv_path) + if not nodes: + print(f"No nodes loaded from {csv_path}; falling back to synthetic dataset.") + return run_smoke() + + dist = build_distance_matrix(nodes) + + # infer roles by prefix (D=depot, S=spklu, C=customer) + depots = [n for n in nodes.keys() if str(n).upper().startswith('D')] + spklus = [n for n in nodes.keys() if str(n).upper().startswith('S')] + customers = [n for n in nodes.keys() if str(n).upper().startswith('C')] + + # fallback heuristics + if not depots: + # if no D* found, take first node as depot + depots = [next(iter(nodes.keys()))] + if not customers: + # all non-depot/non-spklu nodes are customers + customers = [n for n in nodes.keys() if n not in depots + spklus] + + print(f"Using {len(depots)} depots, {len(spklus)} SPKLU, {len(customers)} customers from {csv_path}") + + pop = generate_random_population(depots, customers, population_size=20) + + generations = 50 + elite_size = 2 + tournament_size = 3 + + best = None + best_score = float('inf') + + for g in range(generations): + fitnesses = [fitness_function(ch, dist) for ch in pop] + for ch, f in zip(pop, fitnesses): + if f < best_score: + best_score = f + best = ch + print(f"Gen {g}: best={best_score:.6f}") + parents = selection(pop, fitnesses, elite_size=elite_size, tournament_size=tournament_size) + next_pop = [] + while len(next_pop) < len(pop): + a = random.choice(parents) + b = random.choice(parents) + child = bcrc_crossover(a, b, dist) + child = apply_mutation(child, mutation_rate=0.2) + next_pop.append(child) + pop = next_pop + + print('\nBest chromosome:', best) + print('Best fitness:', best_score) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='Run GA for Rute SPKLU (smoke or real CSV)') + parser.add_argument('--csv', '-c', help='Path to nodes CSV (optional). If omitted, runs synthetic smoke example.') + args = parser.parse_args() + + if args.csv and os.path.exists(args.csv): + run_from_csv(args.csv) + else: + if args.csv: + print(f"CSV path provided but not found: {args.csv}. Running synthetic smoke instead.") + run_smoke() diff --git a/mutation.py b/mutation.py index c21674b..e6a21f5 100644 --- a/mutation.py +++ b/mutation.py @@ -1,6 +1,10 @@ import random +import logging -def apply_mutation(chromosome, depot_locations, customer_locations, mutation_probability=0.1, beta=0.2): +logger = logging.getLogger(__name__) + + +def apply_mutation(chromosome, depot_locations, customer_locations, mutation_probability=0.1, beta=0.2, verbose: bool = False): """ Fungsi memilih antara inter-depot atau intra-depot parameter: @@ -14,10 +18,12 @@ def apply_mutation(chromosome, depot_locations, customer_locations, mutation_pro border_customers = find_border_customers(chromosome, depot_locations, customer_locations, beta) if border_customers and random.random() < 0.7: - print(" [MUTASI] Melakukan Inter-Depot Mutation") + if verbose: + logger.info("[MUTASI] Melakukan Inter-Depot Mutation") return inter_depot_mutation(chromosome, border_customers, depot_locations) else: - print(" [MUTASI] Melakukan Intra-Depot Mutation") + if verbose: + logger.info("[MUTASI] Melakukan Intra-Depot Mutation") return intra_depot_mutation(chromosome) def find_border_customers(chromosome, depot_locations, customer_locations, beta): @@ -59,15 +65,15 @@ def inter_depot_mutation(chromosome, border_customers, depot_locations): if not candidate_depots: return chromosome - + # Pilih depot tujuan secara random dari candidate depots new_depot = random.choice(candidate_depots) - - print(f" [INTER-DEPOT] Memindahkan {customer} dari {selected['current_depot']} ke {new_depot}") - + + logger.info("[INTER-DEPOT] Memindahkan %s dari %s ke %s", customer, selected['current_depot'], new_depot) + # Lakukan reassignment customer ke depot barucls new_chromosome = reassign_customer_to_depot(chromosome, customer, selected['current_depot'], new_depot) - + return new_chromosome def intra_depot_mutation(chromosome): @@ -94,13 +100,14 @@ def intra_depot_mutation(chromosome): # Swap mutation - tukar dua posisi random pos1, pos2 = random.sample(range(len(customers)), 2) customers[pos1], customers[pos2] = customers[pos2], customers[pos1] - print(f" [INTRA-DEPOT] Swap {customers[pos2]} dan {customers[pos1]} dalam {selected_route['depot']}") + logger.info("[INTRA-DEPOT] Swap %s dan %s dalam %s", customers[pos2], customers[pos1], selected_route['depot']) elif mutation_type == 'inversion' and len(customers) >= 2: # Inversion mutation - balik urutan segmen start, end = sorted(random.sample(range(len(customers)), 2)) - customers[start:end+1] = reversed(customers[start:end+1]) - print(f" [INTRA-DEPOT] Inversion posisi {start}-{end} dalam {selected_route['depot']}") + # reverse slice in place using slicing + customers[start:end+1] = customers[start:end+1][::-1] + logger.info("[INTRA-DEPOT] Inversion posisi %d-%d dalam %s", start, end, selected_route['depot']) elif mutation_type == 'insertion' and len(customers) >= 2: # Insertion mutation - pindah customer ke posisi lain @@ -112,7 +119,7 @@ def intra_depot_mutation(chromosome): customer_moved = customers.pop(from_pos) customers.insert(to_pos, customer_moved) - print(f" [INTRA-DEPOT] Insert {customer_moved} dari pos {from_pos} ke {to_pos} dalam {selected_route['depot']}") + logger.info("[INTRA-DEPOT] Insert %s dari pos %d ke %d dalam %s", customer_moved, from_pos, to_pos, selected_route['depot']) # Update full_route selected_route['full_route'] = [selected_route['depot']] + customers + [selected_route['depot']] diff --git a/populasi_awal_final.py b/populasi_awal_final.py index 5b87031..8e71f02 100644 --- a/populasi_awal_final.py +++ b/populasi_awal_final.py @@ -169,6 +169,21 @@ def generate_initial_population(config, pop_size=10, use_spklu=True): return population + + def generate_initial_population_flat(config, pop_size=10, use_spklu=True): + """Wrapper: returns delimiter-flat chromosomes by flattening nested routes. + + This preserves the original generator behavior while providing the + delimiter-flat format used by the GA modules (e.g. ['D1','C1','D1','|',...]). + """ + from utils import flatten_from_nested + + nested = generate_initial_population(config, pop_size=pop_size, use_spklu=use_spklu) + flat_pop = [] + for chrom in nested: + flat_pop.append(flatten_from_nested(chrom)) + return flat_pop + # ----- MAIN ----- if __name__ == "__main__": print("=" * 80) diff --git a/selection.py b/selection.py index 106e0c2..b831fe5 100644 --- a/selection.py +++ b/selection.py @@ -1,119 +1,29 @@ -import random +"""Compatibility wrapper for selection functions. -def get_elites(population, fitness_scores, elite_size): - """ - Membantu 'selection': Memilih individu terbaik (elites) dari populasi. - Berdasarkan skor fitness, di mana nilai yang lebih rendah lebih baik. - """ - # Pasangkan setiap individu dengan skor fitness-nya - population_with_fitness = list(zip(population, fitness_scores)) - - # Urutkan berdasarkan fitness (ascending, karena lebih rendah lebih baik) - sorted_population = sorted(population_with_fitness, key=lambda x: x[1]) - - # Ambil 'elite_size' individu terbaik - elites = [individual for individual, fitness in sorted_population[:elite_size]] - - return elites +This file preserves the original names `get_elites`, `run_tournament`, and +`selection` for scripts that imported them directly. Internally we delegate to +the new implementations in `ga.operators` to keep a single source of truth. +""" +from ga.operators import get_elites, tournament_selection as run_tournament, selection -def run_tournament(population, fitness_scores, tournament_size): - """ - Membantu 'selection': Menjalankan satu putaran tournament selection. - Memilih 'tournament_size' individu secara acak dan mengembalikan - yang terbaik (skor fitness terendah) dari kelompok tersebut. - """ - # Pilih indeks secara acak untuk turnamen - tournament_indices = random.sample(range(len(population)), tournament_size) - - # Siapkan variabel untuk melacak pemenang - best_individual = None - best_fitness = float('inf') # 'inf' karena kita mencari nilai minimum +__all__ = ['get_elites', 'run_tournament', 'selection'] - # Loop melalui peserta turnamen - for index in tournament_indices: - individual = population[index] - fitness = fitness_scores[index] - - # Jika individu ini lebih baik dari pemenang saat ini, jadikan dia pemenang - if fitness < best_fitness: - best_fitness = fitness - best_individual = individual - - return best_individual -# --- FUNGSI UTAMA SELECTION --- -def selection(population, fitness_scores, elite_size, tournament_size): - """ - Melakukan proses seleksi utama menggabungkan Elitism dan Tournament Selection - untuk memilih orang tua (parents) untuk generasi berikutnya. - - Args: - population (list): Daftar semua kromosom di populasi saat ini. - fitness_scores (list): Daftar skor fitness yang sesuai dengan 'population'. - elite_size (int): Jumlah individu terbaik yang akan lolos (Elitism). - tournament_size (int): Jumlah individu yang bertarung di setiap turnamen. - - Returns: - list: Daftar orang tua baru yang terpilih (new_parents). - """ - new_parents = [] - - # [cite_start]1. Elitism: Pertahankan individu terbaik secara langsung [cite: 208, 211] - # Fungsi ini mengambil 'elite_size' individu terbaik - elites = get_elites(population, fitness_scores, elite_size) - new_parents.extend(elites) - - # [cite_start]2. Tournament Selection: Isi sisa populasi [cite: 203, 211] - # Kita perlu memilih 'len(population) - elite_size' individu lagi - num_to_select = len(population) - elite_size - for _ in range(num_to_select): - # Jalankan turnamen untuk memilih satu orang tua - parent = run_tournament(population, fitness_scores, tournament_size) - new_parents.append(parent) - - return new_parents - -# --- CONTOH PENGGUNAAN --- if __name__ == "__main__": - - # Ini adalah POPULASI TIRUAN (DUMMY) + # minimal demo using the operators module + import random DUMMY_POPULATION = [ - ['D1', 'C1', 'S1', 'C2', 'D2'], # Kromosom 1 - ['D2', 'C3', 'C4', 'D1'], # Kromosom 2 - ['D1', 'C5', 'D1'], # Kromosom 3 - ['D2', 'C1', 'C3', 'S1', 'C2', 'C4', 'D2'], # Kromosom 4 - ['D1', 'C4', 'C2', 'S1', 'C1', 'C3', 'D1'] # Kromosom 5 - ] - - # Ini adalah SKOR FITNESS TIRUAN (DUMMY) - # [cite_start](Nilai LEBIH RENDAH = LEBIH BAIK, sesuai problem statement 'minimize' [cite: 34]) - DUMMY_FITNESS_SCORES = [ - 150.5, # Fitness untuk Kromosom 1 - 120.2, # Fitness untuk Kromosom 2 (Terbaik) - 210.0, # Fitness untuk Kromosom 3 - 185.7, # Fitness untuk Kromosom 4 - 135.9 # Fitness untuk Kromosom 5 (Kedua terbaik) + ['D1', 'C1', 'S1', 'C2', 'D2'], + ['D2', 'C3', 'C4', 'D1'], + ['D1', 'C5', 'D1'], + ['D2', 'C1', 'C3', 'S1', 'C2', 'C4', 'D2'], + ['D1', 'C4', 'C2', 'S1', 'C1', 'C3', 'D1'] ] - - # Parameter untuk GA - ELITE_SIZE = 1 # Jumlah individu terbaik yang langsung dipertahankan [cite: 208] - TOURNAMENT_SIZE = 2 # Jumlah individu yang bersaing di setiap turnamen [cite: 205] - - print("--- POPULASI AWAL & FITNESS ---") - for i in range(len(DUMMY_POPULATION)): - print(f"Fitness: {DUMMY_FITNESS_SCORES[i]:.1f} | Kromosom: {DUMMY_POPULATION[i]}") - - # === PANGGIL FUNGSI SELECTION === - selected_parents = selection( - DUMMY_POPULATION, - DUMMY_FITNESS_SCORES, - ELITE_SIZE, - TOURNAMENT_SIZE - ) - - print(f"\n--- ORANG TUA TERPILIH (Ukuran: {len(selected_parents)}) ---") - print(f"(Menggunakan Elitism: {ELITE_SIZE} & Tournament Size: {TOURNAMENT_SIZE})") - - # Tampilkan hasil - for i, parent in enumerate(selected_parents): - print(f"Orang Tua {i+1}: {parent}") \ No newline at end of file + DUMMY_FITNESS_SCORES = [150.5, 120.2, 210.0, 185.7, 135.9] + ELITE_SIZE = 1 + TOURNAMENT_SIZE = 2 + + selected_parents = selection(DUMMY_POPULATION, DUMMY_FITNESS_SCORES, ELITE_SIZE, TOURNAMENT_SIZE) + print(f"Selected parents (count={len(selected_parents)}):") + for p in selected_parents: + print(p) \ No newline at end of file diff --git a/tests/test_utils_and_ops.py b/tests/test_utils_and_ops.py new file mode 100644 index 0000000..14abd13 --- /dev/null +++ b/tests/test_utils_and_ops.py @@ -0,0 +1,41 @@ +import unittest +import random +import sys +import os + +# ensure project root is on sys.path for imports when running tests directly +PROJECT_ROOT = os.path.dirname(os.path.dirname(__file__)) +if PROJECT_ROOT not in sys.path: + sys.path.insert(0, PROJECT_ROOT) + +from utils import split_routes, join_routes +from ga.operators import selection +import mutation + + +class TestUtilsAndOperators(unittest.TestCase): + def test_split_join_roundtrip(self): + chrom = ['D1', 'C1', 'C2', 'D1', '|', 'D2', 'C3', 'D2'] + routes = split_routes(chrom) + self.assertEqual(join_routes(routes), chrom) + + def test_selection_basic(self): + pop = [['a'], ['b'], ['c']] + fitness = [10.0, 5.0, 7.0] + parents = selection(pop, fitness, elite_size=1, tournament_size=2) + # elites preserved + self.assertIn(['b'], parents) + self.assertEqual(len(parents), len(pop)) + + def test_reassign_customer_to_depot(self): + chrom = ['D1', 'C1', 'D1', '|', 'D2', 'C2', 'D2'] + # move C1 from D1 to D2 + new = mutation.reassign_customer_to_depot(chrom, 'C1', 'D1', 'D2') + self.assertIn('C1', new) + # ensure delimiter encoding preserved + self.assertIsInstance(new, list) + + +if __name__ == '__main__': + random.seed(1) + unittest.main() diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..d16ebf9 --- /dev/null +++ b/utils.py @@ -0,0 +1,218 @@ +"""Utilities for Genetic Algorithm MDVRP / SPKLU project. + +This module centralizes helpers used across selection, mutation, crossover, +population generation and fitness evaluation: +- loading nodes from CSV +- building a canonical distance matrix (tuple-keyed) +- flexible distance lookup (supports dict-of-dict and tuple-keyed) +- chromosome encoding/decoding helpers (delimiter '|' encoding) +- small constants (penalty) + +Keep functions pure and well-typed so other modules can import them. +""" +from __future__ import annotations + +import csv +import math +from dataclasses import dataclass +from typing import Dict, Tuple, List, Any, Optional + +# Penalty used for infeasible solutions (minimization problem) +INFEASIBLE_PENALTY = 1e6 + + +def load_nodes_from_csv(filename: str, delimiter: str = ';') -> Dict[str, Dict[str, Any]]: + """Load nodes from a CSV file. + + Returns a mapping node_id -> {'x': float, 'y': float, 'demand': int} + Node id is returned as string (preserves values like 'D1' or numeric indices). + The CSV is expected to have headers that include at least columns named + 'index' (or 'id'), 'x', 'y' and optionally 'Demand' or 'demand'. The function + is permissive and will attempt to parse common formats. + """ + nodes: Dict[str, Dict[str, Any]] = {} + with open(filename, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f, delimiter=delimiter) + for row in reader: + # find id column + node_id = None + for k in ('index', 'id', 'node', 'name'): + if k in row and row[k] not in (None, ''): + node_id = str(row[k]).strip() + break + + if node_id is None: + # try first column + try: + node_id = str(next(iter(row.values()))).strip() + except StopIteration: + continue + + try: + x_raw = row.get('x') or row.get('X') or row.get('lon') or row.get('longitude') + y_raw = row.get('y') or row.get('Y') or row.get('lat') or row.get('latitude') + x = float(str(x_raw).replace(',', '.')) + y = float(str(y_raw).replace(',', '.')) + except Exception: + # skip malformed rows + continue + + demand_raw = row.get('Demand') or row.get('demand') or row.get('d') or 0 + try: + demand = int(str(demand_raw)) + except Exception: + try: + demand = int(float(str(demand_raw).replace(',', '.'))) + except Exception: + demand = 0 + + nodes[node_id] = {'x': x, 'y': y, 'demand': demand} + + return nodes + + +def build_distance_matrix(nodes: Dict[str, Dict[str, Any]], symmetric: bool = True) -> Dict[Tuple[str, str], float]: + """Build a tuple-keyed distance matrix from nodes mapping. + + nodes: mapping node_id -> {'x': float, 'y': float, ...} + Returns dict[(a,b)] -> euclidean_distance + If symmetric=True, (a,b) == (b,a) distance is stored by computing once. + """ + dist: Dict[Tuple[str, str], float] = {} + keys = list(nodes.keys()) + for i, a in enumerate(keys): + ax, ay = nodes[a]['x'], nodes[a]['y'] + for j in range(i + 1, len(keys)): + b = keys[j] + bx, by = nodes[b]['x'], nodes[b]['y'] + d = math.hypot(ax - bx, ay - by) + dist[(a, b)] = d + if symmetric: + dist[(b, a)] = d + + # self distance + dist[(a, a)] = 0.0 + + return dist + + +def get_distance(a: str, b: str, distance_matrix: Any, default: float = float('inf')) -> float: + """Retrieve distance between a and b from flexible distance_matrix. + + Supports two common shapes: + - dict with tuple keys, e.g. distance_matrix[(a,b)] = 12.3 + - nested dict, e.g. distance_matrix[a][b] = 12.3 + + Returns default if no entry found. + """ + if a == b: + return 0.0 + + # tuple-keyed dict + try: + if (a, b) in distance_matrix: + return distance_matrix[(a, b)] + except Exception: + pass + + # nested dict-like + try: + if a in distance_matrix and b in distance_matrix[a]: + return distance_matrix[a][b] + if b in distance_matrix and a in distance_matrix[b]: + return distance_matrix[b][a] + except Exception: + pass + + return default + + +def split_routes(chromosome: List[str], delimiter: str = '|') -> List[List[str]]: + """Split a delimiter-flat chromosome into list of routes. + + Example: ['D1','C1','D1','|','D2','C2','D2'] -> [['D1','C1','D1'], ['D2','C2','D2']] + """ + routes: List[List[str]] = [] + cur: List[str] = [] + for gene in chromosome: + if gene == delimiter: + if cur: + routes.append(cur.copy()) + cur = [] + else: + cur.append(gene) + if cur: + routes.append(cur.copy()) + return routes + + +def join_routes(routes: List[List[str]], delimiter: str = '|') -> List[str]: + """Join nested routes into delimiter-flat chromosome. + + Example: [['D1','C1','D1'], ['D2','C2','D2']] -> ['D1','C1','D1','|','D2','C2','D2'] + """ + chrom: List[str] = [] + for i, r in enumerate(routes): + if i > 0: + chrom.append(delimiter) + chrom.extend(r) + return chrom + + +def flatten_from_nested(chromosome_nested: List[List[str]], delimiter: str = '|') -> List[str]: + """Alias to join_routes for clarity.""" + return join_routes(chromosome_nested, delimiter=delimiter) + + +def nested_from_flat(chromosome_flat: List[str], delimiter: str = '|') -> List[List[str]]: + """Alias to split_routes for clarity.""" + return split_routes(chromosome_flat, delimiter=delimiter) + + +def rebuild_chromosome_from_routes(routes: List[dict]) -> List[str]: + """Compatibility helper used by older code: expects routes as dict with 'full_route'. + + Each route dict should include 'full_route' which is a list of nodes. + Returns delimiter-flat chromosome. + """ + chrom: List[str] = [] + for i, route in enumerate(routes): + if i > 0: + chrom.append('|') + chrom.extend(route.get('full_route', [])) + return chrom + + +def euclidean_distance(a: Tuple[float, float], b: Tuple[float, float]) -> float: + """Simple 2D Euclidean distance between coordinate tuples.""" + return math.hypot(a[0] - b[0], a[1] - b[1]) + + +@dataclass +class GAConfig: + population_size: int = 50 + generations: int = 200 + elite_size: int = 2 + tournament_size: int = 3 + crossover_rate: float = 0.8 + mutation_rate: float = 0.1 + battery_capacity: float = 100.0 + consumption_rate: float = 1.0 + charging_rate: float = 1.0 + w_distance: float = 0.6 + w_charging: float = 0.4 + + +__all__ = [ + 'load_nodes_from_csv', + 'build_distance_matrix', + 'get_distance', + 'split_routes', + 'join_routes', + 'flatten_from_nested', + 'nested_from_flat', + 'rebuild_chromosome_from_routes', + 'euclidean_distance', + 'INFEASIBLE_PENALTY', + 'GAConfig', +] diff --git a/utlis.py b/utlis.py index 864f462..172c913 100644 --- a/utlis.py +++ b/utlis.py @@ -1,11 +1,22 @@ -def load_data(file_path): - # import pandas as pd - # data = pd.read_csv(file_path) - # return data - data=[] - return data +"""Compatibility shim for older code that imported `utlis`. -def get_dist(a,b): # euclidean distance - return a+b +This file used to contain minimal helper stubs. We now forward the common +helpers to `utils.py` so older imports keep working while the canonical +implementations live in `utils.py`. +""" +from typing import Any +from utils import load_nodes_from_csv, build_distance_matrix, get_distance + +def load_data(file_path: str, delimiter: str = ';') -> Any: + """Backward-compatible wrapper around `utils.load_nodes_from_csv`. + + Returns the same mapping produced by `load_nodes_from_csv`. + """ + return load_nodes_from_csv(file_path, delimiter=delimiter) + + +def get_dist(a: str, b: str, distance_matrix: Any): + """Wrapper around `utils.get_distance` for legacy calls.""" + return get_distance(a, b, distance_matrix) \ No newline at end of file