npcpy 1.2.22__tar.gz → 1.2.24__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. {npcpy-1.2.22/npcpy.egg-info → npcpy-1.2.24}/PKG-INFO +147 -1
  2. {npcpy-1.2.22 → npcpy-1.2.24}/README.md +146 -0
  3. npcpy-1.2.24/npcpy/ft/diff.py +110 -0
  4. npcpy-1.2.24/npcpy/ft/ge.py +115 -0
  5. npcpy-1.2.24/npcpy/ft/model_ensembler.py +357 -0
  6. npcpy-1.2.24/npcpy/ft/rl.py +360 -0
  7. npcpy-1.2.24/npcpy/ft/sft.py +230 -0
  8. npcpy-1.2.24/npcpy/ft/usft.py +128 -0
  9. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/llm_funcs.py +1 -1
  10. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/memory/command_history.py +23 -0
  11. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/memory/memory_processor.py +27 -11
  12. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/npc_sysenv.py +152 -24
  13. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/serve.py +4 -2
  14. {npcpy-1.2.22 → npcpy-1.2.24/npcpy.egg-info}/PKG-INFO +147 -1
  15. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy.egg-info/SOURCES.txt +2 -0
  16. {npcpy-1.2.22 → npcpy-1.2.24}/setup.py +1 -1
  17. npcpy-1.2.22/npcpy/ft/diff.py +0 -1
  18. npcpy-1.2.22/npcpy/ft/ge.py +0 -1
  19. npcpy-1.2.22/npcpy/ft/rl.py +0 -1
  20. npcpy-1.2.22/npcpy/ft/sft.py +0 -1
  21. {npcpy-1.2.22 → npcpy-1.2.24}/LICENSE +0 -0
  22. {npcpy-1.2.22 → npcpy-1.2.24}/MANIFEST.in +0 -0
  23. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/__init__.py +0 -0
  24. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/__init__.py +0 -0
  25. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/audio.py +0 -0
  26. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/data_models.py +0 -0
  27. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/image.py +0 -0
  28. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/load.py +0 -0
  29. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/text.py +0 -0
  30. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/video.py +0 -0
  31. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/data/web.py +0 -0
  32. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/ft/__init__.py +0 -0
  33. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/ft/memory_trainer.py +0 -0
  34. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/gen/__init__.py +0 -0
  35. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/gen/audio_gen.py +0 -0
  36. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/gen/embeddings.py +0 -0
  37. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/gen/image_gen.py +0 -0
  38. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/gen/response.py +0 -0
  39. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/gen/video_gen.py +0 -0
  40. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/main.py +0 -0
  41. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/memory/__init__.py +0 -0
  42. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/memory/kg_vis.py +0 -0
  43. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/memory/knowledge_graph.py +0 -0
  44. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/memory/search.py +0 -0
  45. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/mix/__init__.py +0 -0
  46. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/mix/debate.py +0 -0
  47. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/npc_compiler.py +0 -0
  48. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/npcs.py +0 -0
  49. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/__init__.py +0 -0
  50. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/ai_function_tools.py +0 -0
  51. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/database_ai_adapters.py +0 -0
  52. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/database_ai_functions.py +0 -0
  53. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/model_runner.py +0 -0
  54. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/npcsql.py +0 -0
  55. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/sql/sql_model_compiler.py +0 -0
  56. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/tools.py +0 -0
  57. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/work/__init__.py +0 -0
  58. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/work/desktop.py +0 -0
  59. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/work/plan.py +0 -0
  60. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy/work/trigger.py +0 -0
  61. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy.egg-info/dependency_links.txt +0 -0
  62. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy.egg-info/requires.txt +0 -0
  63. {npcpy-1.2.22 → npcpy-1.2.24}/npcpy.egg-info/top_level.txt +0 -0
  64. {npcpy-1.2.22 → npcpy-1.2.24}/setup.cfg +0 -0
  65. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_audio.py +0 -0
  66. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_command_history.py +0 -0
  67. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_image.py +0 -0
  68. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_llm_funcs.py +0 -0
  69. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_load.py +0 -0
  70. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_npc_compiler.py +0 -0
  71. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_npcsql.py +0 -0
  72. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_response.py +0 -0
  73. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_serve.py +0 -0
  74. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_text.py +0 -0
  75. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_tools.py +0 -0
  76. {npcpy-1.2.22 → npcpy-1.2.24}/tests/test_web.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: npcpy
3
- Version: 1.2.22
3
+ Version: 1.2.24
4
4
  Summary: npcpy is the premier open-source library for integrating LLMs and Agents into python systems.
5
5
  Home-page: https://github.com/NPC-Worldwide/npcpy
6
6
  Author: Christopher Agostino
@@ -399,6 +399,152 @@ the citizens, being directed by simple and incontestable principles, may tend to
399
399
  maintenance of the Constitution, and the general happiness. ''')
400
400
  # it will play the audio automatically.
401
401
  ```
402
+ ## Fine-Tuning and Evolution
403
+
404
+ `npcpy` provides modular tools for building adaptive AI systems through supervised fine-tuning, reinforcement learning, and genetic algorithms.
405
+
406
+ See examples/fine_tuning_demo.py for a complete working example.
407
+
408
+
409
+ ### Supervised Fine-Tuning (SFT)
410
+
411
+ Train models on specific tasks using simple X, y pairs:
412
+ ```python
413
+ from npcpy.ft.sft import run_sft, load_sft_model, predict_sft
414
+
415
+ X_train = ["translate to french: hello", "translate to french: goodbye"]
416
+ y_train = ["bonjour", "au revoir"]
417
+
418
+ model_path = run_sft(X_train, y_train)
419
+
420
+ model, tokenizer = load_sft_model(model_path)
421
+ response = predict_sft(model, tokenizer, "translate to french: thanks")
422
+ ```
423
+ ### Unsupervised Fine-Tuning (USFT)
424
+ Adapt models to domain-specific text corpora without labels:
425
+ ```python
426
+ from npcpy.ft.usft import run_usft, load_corpus_from_hf
427
+
428
+ texts = load_corpus_from_hf("tiny_shakespeare", split="train[:1000]")
429
+
430
+ model_path = run_usft(
431
+ texts,
432
+ config=USFTConfig(
433
+ output_model_path="models/shakespeare",
434
+ num_train_epochs=3
435
+ )
436
+ )
437
+ Train on your own text corpus:
438
+ pythondomain_texts = [
439
+ "Your domain-specific text 1",
440
+ "Your domain-specific text 2",
441
+ ] * 100
442
+
443
+ model_path = run_usft(domain_texts)
444
+ ```
445
+ ### Diffusion Fine-tuning
446
+ ```
447
+ from npcpy.ft.diff import train_diffusion, generate_image
448
+
449
+ image_paths = ["img1.png", "img2.png", "img3.png"]
450
+ captions = ["a cat", "a dog", "a bird"]
451
+
452
+ model_path = train_diffusion(
453
+ image_paths,
454
+ captions,
455
+ config=DiffusionConfig(
456
+ num_epochs=100,
457
+ batch_size=4
458
+ )
459
+ )
460
+
461
+ generated = generate_image(
462
+ model_path,
463
+ prompt="a white square",
464
+ image_size=128
465
+ )
466
+ Resume training from checkpoint:
467
+ pythonmodel_path = train_diffusion(
468
+ image_paths,
469
+ captions,
470
+ config,
471
+ resume_from="models/diffusion/checkpoints/checkpoint-epoch10-step1000.pt"
472
+ )
473
+ ```
474
+
475
+
476
+ ### Reinforcement Learning (RL)
477
+ Collect agent traces and train with DPO based on reward signals:
478
+ ```python
479
+ from npcpy.ft.rl import collect_traces, run_rl_training
480
+ from npcpy.npc_compiler import NPC
481
+
482
+ tasks = [
483
+ {'prompt': 'Solve 2+2', 'expected': '4'},
484
+ {'prompt': 'Solve 5+3', 'expected': '8'}
485
+ ]
486
+
487
+ agents = [
488
+ NPC(name="farlor", primary_directive="Be concise",
489
+ model="qwen3:0.6b", provider="ollama"),
490
+ NPC(name="tedno", primary_directive="Show your work",
491
+ model="qwen3:0.6b", provider="ollama")
492
+ ]
493
+
494
+ def reward_fn(trace):
495
+ if trace['task_metadata']['expected'] in trace['final_output']:
496
+ return 1.0
497
+ return 0.0
498
+
499
+ adapter_path = run_rl_training(tasks, agents, reward_fn)
500
+ ```
501
+ ### Genetic Evolution
502
+
503
+ Evolve populations of knowledge graphs or model ensembles:
504
+ ```python
505
+ from npcpy.ft.ge import GeneticEvolver, GAConfig
506
+
507
+ config = GAConfig(
508
+ population_size=20,
509
+ generations=50,
510
+ mutation_rate=0.15
511
+ )
512
+
513
+ evolver = GeneticEvolver(
514
+ fitness_fn=your_fitness_function,
515
+ mutate_fn=your_mutation_function,
516
+ crossover_fn=your_crossover_function,
517
+ initialize_fn=your_init_function,
518
+ config=config
519
+ )
520
+
521
+ best_individual = evolver.run()
522
+ ```
523
+
524
+ ### Smart Model Ensembler and response router
525
+ Build fast intuitive responses with fallback to reasoning:
526
+ ```python
527
+ from npcpy.ft.model_ensembler import (
528
+ ResponseRouter,
529
+ create_model_genome
530
+ )
531
+
532
+ genome = create_model_genome(['math', 'code', 'factual'])
533
+ router = ResponseRouter(fast_threshold=0.8)
534
+
535
+ result = router.route_query("What is 2+2?", genome)
536
+
537
+ if result['used_fast_path']:
538
+ print("Fast gut reaction")
539
+ elif result['used_ensemble']:
540
+ print("Ensemble voting")
541
+ else:
542
+ print("Full reasoning")
543
+ ```
544
+ The intention for this model ensembler system is to mimic human cognition: pattern-matched gut reactions (System 1 of Kahneman) for familiar queries, falling back to deliberate reasoning (System 2 of Kahneman) for novel problems. Genetic algorithms evolve both knowledge structures and model specializations over time.
545
+
546
+
547
+
402
548
  ## Serving an NPC Team
403
549
 
404
550
  `npcpy` includes a built-in Flask server that makes it easy to deploy NPC teams for production use. You can serve teams with tools, jinxs, and complex workflows that frontends can interact with via REST APIs.
@@ -303,6 +303,152 @@ the citizens, being directed by simple and incontestable principles, may tend to
303
303
  maintenance of the Constitution, and the general happiness. ''')
304
304
  # it will play the audio automatically.
305
305
  ```
306
+ ## Fine-Tuning and Evolution
307
+
308
+ `npcpy` provides modular tools for building adaptive AI systems through supervised fine-tuning, reinforcement learning, and genetic algorithms.
309
+
310
+ See examples/fine_tuning_demo.py for a complete working example.
311
+
312
+
313
+ ### Supervised Fine-Tuning (SFT)
314
+
315
+ Train models on specific tasks using simple X, y pairs:
316
+ ```python
317
+ from npcpy.ft.sft import run_sft, load_sft_model, predict_sft
318
+
319
+ X_train = ["translate to french: hello", "translate to french: goodbye"]
320
+ y_train = ["bonjour", "au revoir"]
321
+
322
+ model_path = run_sft(X_train, y_train)
323
+
324
+ model, tokenizer = load_sft_model(model_path)
325
+ response = predict_sft(model, tokenizer, "translate to french: thanks")
326
+ ```
327
+ ### Unsupervised Fine-Tuning (USFT)
328
+ Adapt models to domain-specific text corpora without labels:
329
+ ```python
330
+ from npcpy.ft.usft import run_usft, load_corpus_from_hf
331
+
332
+ texts = load_corpus_from_hf("tiny_shakespeare", split="train[:1000]")
333
+
334
+ model_path = run_usft(
335
+ texts,
336
+ config=USFTConfig(
337
+ output_model_path="models/shakespeare",
338
+ num_train_epochs=3
339
+ )
340
+ )
341
+ Train on your own text corpus:
342
+ pythondomain_texts = [
343
+ "Your domain-specific text 1",
344
+ "Your domain-specific text 2",
345
+ ] * 100
346
+
347
+ model_path = run_usft(domain_texts)
348
+ ```
349
+ ### Diffusion Fine-tuning
350
+ ```
351
+ from npcpy.ft.diff import train_diffusion, generate_image
352
+
353
+ image_paths = ["img1.png", "img2.png", "img3.png"]
354
+ captions = ["a cat", "a dog", "a bird"]
355
+
356
+ model_path = train_diffusion(
357
+ image_paths,
358
+ captions,
359
+ config=DiffusionConfig(
360
+ num_epochs=100,
361
+ batch_size=4
362
+ )
363
+ )
364
+
365
+ generated = generate_image(
366
+ model_path,
367
+ prompt="a white square",
368
+ image_size=128
369
+ )
370
+ Resume training from checkpoint:
371
+ pythonmodel_path = train_diffusion(
372
+ image_paths,
373
+ captions,
374
+ config,
375
+ resume_from="models/diffusion/checkpoints/checkpoint-epoch10-step1000.pt"
376
+ )
377
+ ```
378
+
379
+
380
+ ### Reinforcement Learning (RL)
381
+ Collect agent traces and train with DPO based on reward signals:
382
+ ```python
383
+ from npcpy.ft.rl import collect_traces, run_rl_training
384
+ from npcpy.npc_compiler import NPC
385
+
386
+ tasks = [
387
+ {'prompt': 'Solve 2+2', 'expected': '4'},
388
+ {'prompt': 'Solve 5+3', 'expected': '8'}
389
+ ]
390
+
391
+ agents = [
392
+ NPC(name="farlor", primary_directive="Be concise",
393
+ model="qwen3:0.6b", provider="ollama"),
394
+ NPC(name="tedno", primary_directive="Show your work",
395
+ model="qwen3:0.6b", provider="ollama")
396
+ ]
397
+
398
+ def reward_fn(trace):
399
+ if trace['task_metadata']['expected'] in trace['final_output']:
400
+ return 1.0
401
+ return 0.0
402
+
403
+ adapter_path = run_rl_training(tasks, agents, reward_fn)
404
+ ```
405
+ ### Genetic Evolution
406
+
407
+ Evolve populations of knowledge graphs or model ensembles:
408
+ ```python
409
+ from npcpy.ft.ge import GeneticEvolver, GAConfig
410
+
411
+ config = GAConfig(
412
+ population_size=20,
413
+ generations=50,
414
+ mutation_rate=0.15
415
+ )
416
+
417
+ evolver = GeneticEvolver(
418
+ fitness_fn=your_fitness_function,
419
+ mutate_fn=your_mutation_function,
420
+ crossover_fn=your_crossover_function,
421
+ initialize_fn=your_init_function,
422
+ config=config
423
+ )
424
+
425
+ best_individual = evolver.run()
426
+ ```
427
+
428
+ ### Smart Model Ensembler and response router
429
+ Build fast intuitive responses with fallback to reasoning:
430
+ ```python
431
+ from npcpy.ft.model_ensembler import (
432
+ ResponseRouter,
433
+ create_model_genome
434
+ )
435
+
436
+ genome = create_model_genome(['math', 'code', 'factual'])
437
+ router = ResponseRouter(fast_threshold=0.8)
438
+
439
+ result = router.route_query("What is 2+2?", genome)
440
+
441
+ if result['used_fast_path']:
442
+ print("Fast gut reaction")
443
+ elif result['used_ensemble']:
444
+ print("Ensemble voting")
445
+ else:
446
+ print("Full reasoning")
447
+ ```
448
+ The intention for this model ensembler system is to mimic human cognition: pattern-matched gut reactions (System 1 of Kahneman) for familiar queries, falling back to deliberate reasoning (System 2 of Kahneman) for novel problems. Genetic algorithms evolve both knowledge structures and model specializations over time.
449
+
450
+
451
+
306
452
  ## Serving an NPC Team
307
453
 
308
454
  `npcpy` includes a built-in Flask server that makes it easy to deploy NPC teams for production use. You can serve teams with tools, jinxs, and complex workflows that frontends can interact with via REST APIs.
@@ -0,0 +1,110 @@
1
+ # finetuning diffuser models
2
+ try:
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.nn.functional as F
6
+ from torch.utils.data import DataLoader, Dataset as TorchDataset
7
+ from transformers import CLIPTextModel, CLIPTokenizer
8
+ except:
9
+ torch = None
10
+ nn = None
11
+ F = None
12
+ DataLoader = None
13
+ TorchDataset = None
14
+ CLIPTextModel = None
15
+ CLIPTokenizer = None
16
+ import math
17
+ from dataclasses import dataclass, field
18
+ from typing import List, Optional, Callable
19
+ import numpy as np
20
+ from PIL import Image
21
+ import os
22
+ from tqdm import tqdm
23
+ import gc
24
+
25
+
26
+ @dataclass
27
+ class DiffusionConfig:
28
+ image_size: int = 128
29
+ channels: int = 256
30
+ time_emb_dim: int = 128
31
+ timesteps: int = 1000
32
+ beta_start: float = 1e-4
33
+ beta_end: float = 0.02
34
+ num_epochs: int = 100
35
+ batch_size: int = 4
36
+ learning_rate: float = 1e-5
37
+ checkpoint_frequency: int = 1000
38
+ output_dir: str = "diffusion_model"
39
+ use_clip: bool = True
40
+ num_channels: int = 1
41
+
42
+
43
+ class SinusoidalPositionEmbeddings(nn.Module):
44
+
45
+ def __init__(self, dim):
46
+ super().__init__()
47
+ self.dim = dim
48
+
49
+ def forward(self, time):
50
+ device = time.device
51
+ half_dim = self.dim // 2
52
+ embeddings = math.log(10000) / (half_dim - 1)
53
+ embeddings = torch.exp(
54
+ torch.arange(half_dim, device=device) * -embeddings
55
+ )
56
+ embeddings = time[:, None] * embeddings[None, :]
57
+ embeddings = torch.cat(
58
+ (embeddings.sin(), embeddings.cos()),
59
+ dim=-1
60
+ )
61
+ return embeddings
62
+
63
+
64
+ class SimpleUNet(nn.Module):
65
+
66
+ def __init__(
67
+ self,
68
+ image_size=128,
69
+ channels=256,
70
+ time_emb_dim=128,
71
+ num_channels=1
72
+ ):
73
+ super().__init__()
74
+
75
+ self.image_size = image_size
76
+
77
+ self.time_mlp = nn.Sequential(
78
+ SinusoidalPositionEmbeddings(time_emb_dim),
79
+ nn.Linear(time_emb_dim, time_emb_dim * 4),
80
+ nn.GELU(),
81
+ nn.Linear(time_emb_dim * 4, channels),
82
+ )
83
+
84
+ self.text_mlp = nn.Sequential(
85
+ nn.Linear(768, time_emb_dim),
86
+ nn.GELU(),
87
+ nn.Linear(time_emb_dim, time_emb_dim),
88
+ nn.GELU(),
89
+ nn.Linear(time_emb_dim, channels),
90
+ )
91
+
92
+ self.conv_in = nn.Conv2d(num_channels, channels, 1, padding=0)
93
+
94
+ self.down1 = nn.Sequential(
95
+ nn.Conv2d(channels, channels * 2, 4, 2, 1),
96
+ nn.GroupNorm(8, channels * 2),
97
+ nn.GELU(),
98
+ )
99
+
100
+ self.down2 = nn.Sequential(
101
+ nn.Conv2d(channels * 2, channels * 4, 4, 2, 1),
102
+ nn.GroupNorm(8, channels * 4),
103
+ nn.GELU(),
104
+ )
105
+
106
+ self.down3 = nn.Sequential(
107
+ nn.Conv2d(channels * 4, channels * 8, 4, 2, 1),
108
+ nn.GroupNorm(8, channels * 8),
109
+ nn.GELU(),
110
+ )
@@ -0,0 +1,115 @@
1
+ import random
2
+ from dataclasses import dataclass
3
+ from typing import Callable, Optional, List
4
+
5
+
6
+ @dataclass
7
+ class GAConfig:
8
+ population_size: int = 20
9
+ mutation_rate: float = 0.15
10
+ crossover_rate: float = 0.7
11
+ tournament_size: int = 3
12
+ elitism_count: int = 2
13
+ generations: int = 50
14
+
15
+
16
+ class GeneticEvolver:
17
+ """
18
+ Generic GA that takes fitness, mutation, crossover
19
+ and initialization functions to evolve any population
20
+ """
21
+ def __init__(
22
+ self,
23
+ fitness_fn: Callable,
24
+ mutate_fn: Callable,
25
+ crossover_fn: Callable,
26
+ initialize_fn: Callable,
27
+ config: Optional[GAConfig] = None
28
+ ):
29
+ self.fitness_fn = fitness_fn
30
+ self.mutate_fn = mutate_fn
31
+ self.crossover_fn = crossover_fn
32
+ self.initialize_fn = initialize_fn
33
+ self.config = config or GAConfig()
34
+ self.population = []
35
+ self.history = []
36
+
37
+ def initialize_population(self):
38
+ self.population = [
39
+ self.initialize_fn()
40
+ for _ in range(self.config.population_size)
41
+ ]
42
+
43
+ def evaluate_population(self) -> List[float]:
44
+ return [
45
+ self.fitness_fn(individual)
46
+ for individual in self.population
47
+ ]
48
+
49
+ def tournament_select(self, fitness_scores: List[float]):
50
+ indices = random.sample(
51
+ range(len(self.population)),
52
+ self.config.tournament_size
53
+ )
54
+ tournament_fitness = [fitness_scores[i] for i in indices]
55
+ winner_idx = indices[
56
+ tournament_fitness.index(max(tournament_fitness))
57
+ ]
58
+ return self.population[winner_idx]
59
+
60
+ def evolve_generation(self):
61
+ fitness_scores = self.evaluate_population()
62
+
63
+ sorted_pop = sorted(
64
+ zip(self.population, fitness_scores),
65
+ key=lambda x: x[1],
66
+ reverse=True
67
+ )
68
+
69
+ new_population = [
70
+ ind for ind, _ in sorted_pop[:self.config.elitism_count]
71
+ ]
72
+
73
+ while len(new_population) < self.config.population_size:
74
+ parent1 = self.tournament_select(fitness_scores)
75
+ parent2 = self.tournament_select(fitness_scores)
76
+
77
+ if random.random() < self.config.crossover_rate:
78
+ child = self.crossover_fn(parent1, parent2)
79
+ else:
80
+ child = parent1
81
+
82
+ if random.random() < self.config.mutation_rate:
83
+ child = self.mutate_fn(child)
84
+
85
+ new_population.append(child)
86
+
87
+ self.population = new_population[:self.config.population_size]
88
+
89
+ best_fitness = max(fitness_scores)
90
+ avg_fitness = sum(fitness_scores) / len(fitness_scores)
91
+
92
+ return {
93
+ 'best_fitness': best_fitness,
94
+ 'avg_fitness': avg_fitness,
95
+ 'best_individual': sorted_pop[0][0]
96
+ }
97
+
98
+ def run(self, generations: Optional[int] = None):
99
+ if not self.population:
100
+ self.initialize_population()
101
+
102
+ gens = generations or self.config.generations
103
+
104
+ for gen in range(gens):
105
+ gen_stats = self.evolve_generation()
106
+ self.history.append(gen_stats)
107
+
108
+ if gen % 10 == 0:
109
+ print(
110
+ f"Gen {gen}: "
111
+ f"Best={gen_stats['best_fitness']:.3f}, "
112
+ f"Avg={gen_stats['avg_fitness']:.3f}"
113
+ )
114
+
115
+ return self.history[-1]['best_individual']