Module nkcs.nkcs
An implementation of the NKCS model for exploring aspects of coevolution.
Expand source code
#!/usr/bin/python3
#
# Copyright (C) 2019--2024 Richard Preen <rpreen@gmail.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
"""An implementation of the NKCS model for exploring aspects of coevolution."""
from __future__ import annotations
import logging
from typing import Final
import numpy as np
from .constants import Constants as Cons
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("nkcs")
class NKCS:
"""NKCS model."""
def __init__(self) -> None:
"""Initialise a randomly generated NKCS model."""
self.species: list[NKCS.Species] = [
self.Species(i) for i in range(Cons.S)
]
def __calc_fit(self, sp: int, team: list[np.ndarray]) -> float:
"""Return the fitness of an individual partnered with a given team."""
total: float = 0
for i in range(Cons.N):
inputs = self.__get_gene_inputs(sp, team, i)
total += self.species[sp].gene_fit(inputs, i)
return total / Cons.N
def calc_team_fit(self, team: list[np.ndarray]) -> float:
"""Return the total team fitness."""
total: float = 0
for s in range(Cons.S):
total += self.__calc_fit(s, team)
return total
def __get_inputs_internal(
self, sp: int, team: list[np.ndarray], gene_idx: int
) -> list[float]:
"""Return gene internal inputs."""
species: NKCS.Species = self.species[sp] # species containing the gene
offset: int = gene_idx * (species.n_gene_inputs - 1) # map offset
inputs: list[float] = []
for i in range(Cons.K):
node = species.map[offset + i]
inputs.append(team[sp][node])
return inputs
def __get_inputs_external_line(
self, sp: int, team: list[np.ndarray], gene_idx: int
) -> list[float]:
"""Return gene external inputs with line topology."""
species: NKCS.Species = self.species[sp] # species containing the gene
offset: int = gene_idx * (species.n_gene_inputs - 1) # map offset
cnt: int = Cons.K
inputs: list[float] = []
if sp != 0:
left = Cons.S - 1 if sp - 1 < 0 else sp - 1
for _ in range(Cons.C):
node = species.map[offset + cnt]
inputs.append(team[left][node])
cnt += 1
if sp != Cons.S - 1:
right = (sp + 1) % Cons.S
for _ in range(Cons.C):
node = species.map[offset + cnt]
inputs.append(team[right][node])
cnt += 1
return inputs
def __get_inputs_external_std(
self, sp: int, team: list[np.ndarray], gene_idx: int
) -> list[float]:
"""Return gene external inputs with standard topology."""
species: NKCS.Species = self.species[sp] # species containing the gene
offset: int = gene_idx * (species.n_gene_inputs - 1) # map offset
cnt: int = Cons.K
inputs: list[float] = []
for j in range(Cons.S):
if j != sp:
for _ in range(Cons.C):
node = species.map[offset + cnt]
inputs.append(team[j][node])
cnt += 1
return inputs
def __get_gene_inputs(
self, sp: int, team: list[np.ndarray], gene_idx: int
) -> np.ndarray:
"""Return the inputs to a gene (including the internal state)."""
# get internal connections
inputs_int = self.__get_inputs_internal(sp, team, gene_idx)
# get external connections
if Cons.NKCS_TOPOLOGY == "line":
inputs_ext = self.__get_inputs_external_line(sp, team, gene_idx)
elif Cons.NKCS_TOPOLOGY == "standard":
inputs_ext = self.__get_inputs_external_std(sp, team, gene_idx)
else:
raise ValueError("unsupported NKCS topology")
# add internal state
inputs = inputs_int + inputs_ext + team[sp][gene_idx]
return np.array(inputs)
def display(self, sp: int) -> None:
"""Print a specified NKCS species."""
logger.info("**********************")
logger.info("{sp} SPECIES:")
logger.info("**********************")
self.species[sp].display()
class Species:
"""A species within an NKCS model."""
def __init__(self, sp: int) -> None:
"""Initialise a species with random connectivity."""
x: int = 0 # number of coevolving species
if Cons.S > 1:
if Cons.NKCS_TOPOLOGY == "line":
x = 1 if sp in (0, Cons.S - 1) else 2
elif Cons.NKCS_TOPOLOGY == "standard":
x = Cons.S - 1
else:
raise ValueError("unsupported NKCS topology")
# n inputs to each gene
self.n_gene_inputs: Final[int] = Cons.K + (x * Cons.C) + 1
# connectivity length
map_length: Final[int] = Cons.N * self.n_gene_inputs - 1
# connectivity
self.map: np.ndarray = np.random.randint(0, Cons.N, map_length)
# each gene's hash table
self.ftable: list[dict[tuple, float]] = [{} for i in range(Cons.N)]
def gene_fit(self, inputs: np.ndarray, gene: int) -> float:
"""Return the fitness of an individual gene within a species."""
# find fitness in table
key = tuple(inputs)
fit = self.ftable[gene].get(key)
if fit is None: # not found, add new
fitness = np.random.uniform(low=0, high=1)
self.ftable[gene][key] = fitness
else:
fitness = fit
return fitness
def display(self) -> None:
"""Print an NKCS species."""
logger.info(f"con: {self.map}")
logger.info("fitness table:")
for i, fitness in enumerate(self.ftable):
logger.info(f"Gene {i}")
logger.info(fitness)
Classes
class NKCS
-
NKCS model.
Initialise a randomly generated NKCS model.
Expand source code
class NKCS: """NKCS model.""" def __init__(self) -> None: """Initialise a randomly generated NKCS model.""" self.species: list[NKCS.Species] = [ self.Species(i) for i in range(Cons.S) ] def __calc_fit(self, sp: int, team: list[np.ndarray]) -> float: """Return the fitness of an individual partnered with a given team.""" total: float = 0 for i in range(Cons.N): inputs = self.__get_gene_inputs(sp, team, i) total += self.species[sp].gene_fit(inputs, i) return total / Cons.N def calc_team_fit(self, team: list[np.ndarray]) -> float: """Return the total team fitness.""" total: float = 0 for s in range(Cons.S): total += self.__calc_fit(s, team) return total def __get_inputs_internal( self, sp: int, team: list[np.ndarray], gene_idx: int ) -> list[float]: """Return gene internal inputs.""" species: NKCS.Species = self.species[sp] # species containing the gene offset: int = gene_idx * (species.n_gene_inputs - 1) # map offset inputs: list[float] = [] for i in range(Cons.K): node = species.map[offset + i] inputs.append(team[sp][node]) return inputs def __get_inputs_external_line( self, sp: int, team: list[np.ndarray], gene_idx: int ) -> list[float]: """Return gene external inputs with line topology.""" species: NKCS.Species = self.species[sp] # species containing the gene offset: int = gene_idx * (species.n_gene_inputs - 1) # map offset cnt: int = Cons.K inputs: list[float] = [] if sp != 0: left = Cons.S - 1 if sp - 1 < 0 else sp - 1 for _ in range(Cons.C): node = species.map[offset + cnt] inputs.append(team[left][node]) cnt += 1 if sp != Cons.S - 1: right = (sp + 1) % Cons.S for _ in range(Cons.C): node = species.map[offset + cnt] inputs.append(team[right][node]) cnt += 1 return inputs def __get_inputs_external_std( self, sp: int, team: list[np.ndarray], gene_idx: int ) -> list[float]: """Return gene external inputs with standard topology.""" species: NKCS.Species = self.species[sp] # species containing the gene offset: int = gene_idx * (species.n_gene_inputs - 1) # map offset cnt: int = Cons.K inputs: list[float] = [] for j in range(Cons.S): if j != sp: for _ in range(Cons.C): node = species.map[offset + cnt] inputs.append(team[j][node]) cnt += 1 return inputs def __get_gene_inputs( self, sp: int, team: list[np.ndarray], gene_idx: int ) -> np.ndarray: """Return the inputs to a gene (including the internal state).""" # get internal connections inputs_int = self.__get_inputs_internal(sp, team, gene_idx) # get external connections if Cons.NKCS_TOPOLOGY == "line": inputs_ext = self.__get_inputs_external_line(sp, team, gene_idx) elif Cons.NKCS_TOPOLOGY == "standard": inputs_ext = self.__get_inputs_external_std(sp, team, gene_idx) else: raise ValueError("unsupported NKCS topology") # add internal state inputs = inputs_int + inputs_ext + team[sp][gene_idx] return np.array(inputs) def display(self, sp: int) -> None: """Print a specified NKCS species.""" logger.info("**********************") logger.info("{sp} SPECIES:") logger.info("**********************") self.species[sp].display() class Species: """A species within an NKCS model.""" def __init__(self, sp: int) -> None: """Initialise a species with random connectivity.""" x: int = 0 # number of coevolving species if Cons.S > 1: if Cons.NKCS_TOPOLOGY == "line": x = 1 if sp in (0, Cons.S - 1) else 2 elif Cons.NKCS_TOPOLOGY == "standard": x = Cons.S - 1 else: raise ValueError("unsupported NKCS topology") # n inputs to each gene self.n_gene_inputs: Final[int] = Cons.K + (x * Cons.C) + 1 # connectivity length map_length: Final[int] = Cons.N * self.n_gene_inputs - 1 # connectivity self.map: np.ndarray = np.random.randint(0, Cons.N, map_length) # each gene's hash table self.ftable: list[dict[tuple, float]] = [{} for i in range(Cons.N)] def gene_fit(self, inputs: np.ndarray, gene: int) -> float: """Return the fitness of an individual gene within a species.""" # find fitness in table key = tuple(inputs) fit = self.ftable[gene].get(key) if fit is None: # not found, add new fitness = np.random.uniform(low=0, high=1) self.ftable[gene][key] = fitness else: fitness = fit return fitness def display(self) -> None: """Print an NKCS species.""" logger.info(f"con: {self.map}") logger.info("fitness table:") for i, fitness in enumerate(self.ftable): logger.info(f"Gene {i}") logger.info(fitness)
Class variables
var Species
-
A species within an NKCS model.
Methods
def calc_team_fit(self, team: list[np.ndarray]) ‑> float
-
Return the total team fitness.
Expand source code
def calc_team_fit(self, team: list[np.ndarray]) -> float: """Return the total team fitness.""" total: float = 0 for s in range(Cons.S): total += self.__calc_fit(s, team) return total
def display(self, sp: int) ‑> None
-
Print a specified NKCS species.
Expand source code
def display(self, sp: int) -> None: """Print a specified NKCS species.""" logger.info("**********************") logger.info("{sp} SPECIES:") logger.info("**********************") self.species[sp].display()