Skip to content

API Reference

Core

Population

pikaia.data.population.PikaiaPopulation

Represents a population matrix for genetic algorithms.

The matrix shape is (N, M) where N is the number of organisms and M is the number of genes (features). All values must lie in [0, 1].

Source code in pikaia/data/population.py
class PikaiaPopulation:
    """
    Represents a population matrix for genetic algorithms.

    The matrix shape is ``(N, M)`` where ``N`` is the number of organisms and
    ``M`` is the number of genes (features). All values must lie in ``[0, 1]``.
    """

    def __init__(self, matrix: np.ndarray, skip_correlation_validation: bool = True):
        """
        Initialize the Population with a matrix.

        Args:
            matrix (np.ndarray):
                A 2D numpy array of shape (N, M) representing the population.
                All values must be between 0 and 1. Higher values are treated as more
                desirable features, whereas lower values are less desirable.

            skip_correlation_validation (bool):
                If True, skips the correlation validation between features.

        Raises:
            ValueError: If the matrix does not meet the validation criteria.

        """
        self._matrix = matrix
        self._skip_correlation_validation = skip_correlation_validation
        self._validate_matrix()

    def _validate_matrix(self):
        """
        Validates the population matrix.

        Checks that all values are between 0 and 1, and that for high linear
        correlation between features (columns).

        Raises:
            ValueError: If validation fails.

        """
        # 0. Check for non-numeric types
        if not np.issubdtype(self._matrix.dtype, np.number):
            raise ValueError("Population matrix must contain numeric values.")

        # 1. Check for NaN values
        if np.any(np.isnan(self._matrix)):
            raise ValueError("Population matrix must not contain NaN values.")

        # 2. Check all values are between 0 and 1
        if not np.all((self._matrix >= 0) & (self._matrix <= 1)):
            raise ValueError(
                "All values in the population matrix must be between 0 and 1."
            )

        # 2. Check for linear correlation between features (columns)
        if self._skip_correlation_validation:
            return
        if self._matrix.shape[1] > 1:
            corr_matrix = np.corrcoef(self._matrix, rowvar=False)
            # Ignore diagonal and lower triangle
            upper_tri_indices = np.triu_indices_from(corr_matrix, k=1)
            upper_corrs = np.abs(corr_matrix[upper_tri_indices])
            if upper_corrs.size > 0:
                # Find all pairs with correlation > 0.80
                warn_corr_indices = np.where(upper_corrs > 0.80)[0]
                if warn_corr_indices.size > 0:
                    for idx in warn_corr_indices:
                        i = upper_tri_indices[0][idx]
                        j = upper_tri_indices[1][idx]
                        logger.warning(
                            f"Correlated feature pair: (col {i}, col {j}) "
                            f"with correlation {corr_matrix[i, j]:.4f}"
                        )
        else:
            raise ValueError(
                "Population matrix must have at least two features (columns)."
            )

    def __getitem__(self, idx):
        """
        Allows direct indexing into the population.

        Example:
            population[i, j] or population[i].

        """
        return self._matrix[idx]

    @property
    def N(self) -> int:
        """
        Returns the number of organisms (rows) in the population matrix.

        """
        return self._matrix.shape[0]

    @property
    def M(self) -> int:
        """
        Returns the number of genes (columns) in the population matrix.

        """
        return self._matrix.shape[1]

    @property
    def matrix(self) -> np.ndarray:
        """
        Returns the underlying population matrix.

        """
        return self._matrix

Attributes

M: int property

Returns the number of genes (columns) in the population matrix.

N: int property

Returns the number of organisms (rows) in the population matrix.

matrix: np.ndarray property

Returns the underlying population matrix.

Methods:

__getitem__(idx)

Allows direct indexing into the population.

Example

population[i, j] or population[i].

Source code in pikaia/data/population.py
def __getitem__(self, idx):
    """
    Allows direct indexing into the population.

    Example:
        population[i, j] or population[i].

    """
    return self._matrix[idx]

__init__(matrix: np.ndarray, skip_correlation_validation: bool = True)

Initialize the Population with a matrix.

Parameters:

Name Type Description Default
matrix ndarray

A 2D numpy array of shape (N, M) representing the population. All values must be between 0 and 1. Higher values are treated as more desirable features, whereas lower values are less desirable.

required
skip_correlation_validation bool

If True, skips the correlation validation between features.

True

Raises:

Type Description
ValueError

If the matrix does not meet the validation criteria.

Source code in pikaia/data/population.py
def __init__(self, matrix: np.ndarray, skip_correlation_validation: bool = True):
    """
    Initialize the Population with a matrix.

    Args:
        matrix (np.ndarray):
            A 2D numpy array of shape (N, M) representing the population.
            All values must be between 0 and 1. Higher values are treated as more
            desirable features, whereas lower values are less desirable.

        skip_correlation_validation (bool):
            If True, skips the correlation validation between features.

    Raises:
        ValueError: If the matrix does not meet the validation criteria.

    """
    self._matrix = matrix
    self._skip_correlation_validation = skip_correlation_validation
    self._validate_matrix()

Model

pikaia.models.pikaia_model.PikaiaModel

Bases: GeneticModel

Central organizing class for the Genetic AI model.

This class orchestrates the evolutionary simulation. It takes a population, a set of gene and organism strategies, and runs a simulation over a specified number of iterations. It tracks the history of gene and organism fitness, as well as the mixing coefficients for the strategies.

The model is fitted using the fit method, which iteratively updates the fitness values based on the provided strategies.

Source code in pikaia/models/pikaia_model.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
class PikaiaModel(GeneticModel):
    """
    Central organizing class for the Genetic AI model.

    This class orchestrates the evolutionary simulation. It takes a population,
    a set of gene and organism strategies, and runs a simulation over a specified
    number of iterations. It tracks the history of gene and organism fitness,
    as well as the mixing coefficients for the strategies.

    The model is fitted using the `fit` method, which iteratively updates the
    fitness values based on the provided strategies.
    """

    def __init__(self, *args, use_d_matrix: bool = False, **kwargs):
        """
        Initialises the PikaiaModel.

        Accepts all arguments of :class:`GeneticModel` plus:

        Args:
            use_d_matrix (bool):
                When ``True``, the D-matrix fast path is used instead of the
                standard per-organism loop. Precomputes the ``(M, M)`` D matrix
                and ``(M,)`` d-vector once before the iteration loop, reducing
                per-step cost from ``O(N·M²)`` to ``O(M²)``.  All active
                strategies must have a registered D-matrix kernel; a
                ``ValueError`` is raised at fit time if any do not.
                Defaults to ``False``.
        """
        super().__init__(*args, **kwargs)
        self._use_d_matrix = use_d_matrix

        # D-matrix state — populated by _compute_d_matrix() inside fit().
        # Initialized as empty lists so type-checkers accept slicing.
        self._D_matrix: np.ndarray | None = None
        self._d_vector: np.ndarray | None = None
        self._D_per_strategy: list = []
        self._d_per_strategy: list = []

    def fit(self) -> None:
        """
        Fits the genetic model to the population data by running the simulation.

        This method iteratively updates the gene and organism fitness values based on the
        provided strategies. The simulation runs for a maximum number of iterations as
        defined by `max_iter`. If an `epsilon` value is provided, the simulation will
        stop early if the change in gene fitness between iterations falls below this
        threshold, indicating convergence.
        """
        import time

        if self._initial_org_fitness_range == 0:
            logger.info(
                "Skipping fit: organism fitness range is 0. "
                "Returning uniform scores unchanged."
            )
            return

        start_time = time.perf_counter()
        if self._use_d_matrix:
            if self._max_iter is None:
                raise ValueError(
                    "use_d_matrix=True requires max_iter to be set. "
                    "There is no general closed-form fixed point for an arbitrary D matrix. "
                    "Use max_iter with a large value (e.g. max_iter=500) and optionally "
                    "epsilon for convergence detection, or use use_d_matrix=False for the "
                    "analytical Dominant+Balanced fixed point."
                )
            logger.info("D-matrix path selected. Precomputing D matrix...")
            self._compute_d_matrix()
            logger.info(
                f"Running D-matrix simulation for up to {self._max_iter} iterations."
            )
            self._run_d_matrix_iterations()
        elif self._max_iter is None:
            logger.info("No max iterations set, solving for optimal solution directly.")
            self._run_fix_point()
        else:
            logger.info(f"Running simulation for up to {self._max_iter} iterations.")
            self._run_iterations()
        total_time = time.perf_counter() - start_time
        logger.info(f"Total fit process time: {total_time:.4f} seconds.")

    def _run_fix_point(self):
        """
        Solves for the optimal gene fitness distribution directly, assuming a dominant
        gene strategy and a balanced organism strategy.
        """
        import time

        start_time = time.perf_counter()
        gene_means = np.mean(self._population.matrix, axis=0)  # (M,)
        denom = gene_means + 0.5  # (M,)
        sum_inv_denom = np.sum(1 / denom)  # scalar
        gene_fitness = 1 / (denom * sum_inv_denom)  # (M,)

        org_fitness = np.dot(self._population.matrix, gene_fitness)  # (N,)

        self._gene_fitness_hist[1, :] = gene_fitness
        self._org_fitness_hist[1, :] = org_fitness
        elapsed = time.perf_counter() - start_time
        logger.debug(f"_run_fix_point completed in {elapsed:.4f} seconds.")

    def _run_iterations(self):
        """
        Runs the evolutionary simulation for multiple iterations.

        This method iterates over the specified number of iterations, updating
        the gene and organism fitness values at each step. It checks for convergence
        based on the epsilon threshold if provided, and stops early if the simulation
        reaches an Evolutionarily Stable Equilibrium (ESE).
        """
        import time

        logger.info(
            f"Starting evolutionary simulation for up to {self._max_iter} iterations."
        )
        total_start = time.perf_counter()
        for i in range(1, (self._max_iter or 1) + 1):
            iter_start = time.perf_counter()
            logger.debug(f"Starting iteration {i}...")
            # 1. Run iteration
            (
                self._gene_fitness_hist[i, :],
                self._org_fitness_hist[i, :],
                self._gene_mixing_coeffs_hist[i, :],
                self._org_mixing_coeffs_hist[i, :],
            ) = self._run_iteration(i)

            # 2. Check for convergence
            delta = np.linalg.norm(
                self._gene_fitness_hist[i, :] - self._gene_fitness_hist[i - 1, :]
            )
            iter_elapsed = time.perf_counter() - iter_start
            logger.debug(
                f"Iteration {i} complete. Δgene_fitness = {delta:.6g}. "
                f"Iteration time: {iter_elapsed:.4f} seconds."
            )
            if self._epsilon is not None and delta < self._epsilon:
                self._ESE_iter = i
                total_elapsed = time.perf_counter() - total_start
                logger.info(
                    f"Reached ESE after {self._ESE_iter} iterations. "
                    f"Final delta = {delta}. "
                    f"Total time: {total_elapsed:.4f} seconds."
                )
                break
        else:
            total_elapsed = time.perf_counter() - total_start
            logger.info(
                f"Completed all {self._max_iter} iterations without reaching ESE. "
                f"Total time: {total_elapsed:.4f} seconds."
            )

    def _run_iteration(
        self, iter_num: int
    ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
        """
        Runs a single evolutionary step of the simulation.

        This method calculates the change in gene fitness based on the defined gene and
        organism strategies. It then mixes these strategies and applies the updates to
        compute the new gene and organism fitness values for the current iteration.

        Args:
            iter_num (int): The current iteration number.

        Returns:
            tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: A tuple containing:
                - The new gene fitness vector.
                - The new organism fitness vector.
                - The new gene mixing coefficients.
                - The new organism mixing coefficients.
        """
        current_org_fitness = self._org_fitness_hist[iter_num - 1, :]
        current_gene_fitness = self._gene_fitness_hist[iter_num - 1, :]

        delta_g, delta_o = self._calculate_deltas(
            current_org_fitness, current_gene_fitness
        )

        # 2. Mix evolutionary strategies
        mixed_delta_g, gene_mixing_coeffs = self._gene_mix_strategy(
            delta_g, self._gene_mixing_coeffs_hist[iter_num - 1, :]
        )
        mixed_delta_o, org_mixing_coeffs = self._org_mix_strategy(
            delta_o, self._org_mixing_coeffs_hist[iter_num - 1, :]
        )

        # 3. Apply delta in form of the central replicator equations
        gene_fitness = current_gene_fitness * (
            1 + np.sum(mixed_delta_g + mixed_delta_o, axis=0)
        )
        gene_fitness /= np.sum(gene_fitness)

        org_fitness = np.dot(self._population.matrix, gene_fitness)

        return (
            gene_fitness,
            org_fitness,
            gene_mixing_coeffs,
            org_mixing_coeffs,
        )

    def _calculate_deltas(
        self, current_org_fitness: np.ndarray, current_gene_fitness: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray]:
        """
        Calculates the delta contributions for gene and organism fitness updates.

        This method can run in parallel or sequentially based on the `n_jobs` setting.

        Args:
            current_org_fitness (np.ndarray): The current organism fitness vector.
            current_gene_fitness (np.ndarray): The current gene fitness vector.

        Returns:
            tuple[np.ndarray, np.ndarray]: A tuple containing the delta_g and delta_o matrices.
        """
        delta_g = np.zeros(
            [self._population.N, self._population.M, len(self._gene_strategies)]
        )
        delta_o = np.zeros(
            [self._population.N, self._population.M, len(self._org_strategies)]
        )

        context_args = {
            "population": self._population,
            "org_fitness": current_org_fitness,
            "gene_fitness": current_gene_fitness,
            "initial_org_fitness_range": self._initial_org_fitness_range,
            "org_similarity": self._org_similarity,
            "gene_similarity": self._gene_similarity,
            "y": self._y,
        }

        if self._n_jobs > 1:
            with multiprocessing.Pool(processes=self._n_jobs) as pool:
                # Organism strategies
                org_args_list = [
                    (strat, org_id, None, context_args)
                    for org_id in range(self._population.N)
                    for strat in self._org_strategies
                ]
                org_results = pool.starmap(
                    PikaiaModel._compute_single_delta, org_args_list
                )

                res_idx = 0
                for org_id in range(self._population.N):
                    for strat_idx in range(len(self._org_strategies)):
                        delta_o[org_id, :, strat_idx] = org_results[res_idx]
                        res_idx += 1

                # Gene strategies
                gene_args_list = [
                    (strat, org_id, gene_id, context_args)
                    for org_id in range(self._population.N)
                    for gene_id in range(self._population.M)
                    for strat in self._gene_strategies
                ]
                gene_results = pool.starmap(
                    PikaiaModel._compute_single_delta, gene_args_list
                )

                res_idx = 0
                for org_id in range(self._population.N):
                    for gene_id in range(self._population.M):
                        for strat_idx in range(len(self._gene_strategies)):
                            delta_g[org_id, gene_id, strat_idx] = gene_results[res_idx]
                            res_idx += 1
        else:
            # Naive loop-based calculation
            for org_id in range(self._population.N):
                for i, strat in enumerate(self._org_strategies):
                    delta_o[org_id, :, i] = PikaiaModel._compute_single_delta(
                        strat, org_id, None, context_args
                    )
                for gene_id in range(self._population.M):
                    for i, strat in enumerate(self._gene_strategies):
                        delta_g[org_id, gene_id, i] = PikaiaModel._compute_single_delta(
                            strat, org_id, gene_id, context_args
                        )
        return delta_g, delta_o

    @staticmethod
    def _compute_single_delta(
        strat: GeneStrategy | OrgStrategy,
        org_id: int,
        gene_id: int | None,
        context_args: dict,
    ) -> np.ndarray | float:
        """
        Computes a single delta contribution for either organism or gene strategies.

        Args:
            strat (GeneStrategy | OrgStrategy): The strategy to apply.
            org_id (int): The organism ID.
            gene_id (int | None): The gene ID, required for "gene" type.
            context_args (dict): A dictionary of common arguments for StrategyContext.

        Returns:
            np.ndarray | float: The computed delta value(s).
        """
        return strat(
            StrategyContext(
                org_id=org_id,
                gene_id=gene_id,
                **context_args,
            )
        )

    # ------------------------------------------------------------------
    # D-matrix fast paths
    # ------------------------------------------------------------------

    def _run_d_matrix_iterations(
        self, *, epsilon_override: float | None = None
    ) -> None:
        """Run the D-matrix fast iteration loop.

        Uses the precomputed ``self._D_matrix`` (bilinear term) and
        ``self._d_vector`` (linear term from balanced org) to execute each
        step in ``O(M²)`` instead of ``O(N·M²)``.

        Args:
            epsilon_override: If provided, overrides ``self._epsilon`` as the
                convergence threshold.  Used internally by
                ``_run_d_matrix_fix_point()`` to apply a tight tolerance.
        """
        import time

        D = self._D_matrix  # (M, M) or None
        d = self._d_vector  # (M,)  or None
        epsilon = epsilon_override if epsilon_override is not None else self._epsilon

        is_sc_gene = isinstance(self._gene_mix_strategy, SelfConsistentMixStrategy)
        is_sc_org = isinstance(self._org_mix_strategy, SelfConsistentMixStrategy)
        K_g = len(self._gene_strategies)

        gene_mix_coeffs = np.array(self._initial_gene_mixing_coeffs)
        org_mix_coeffs = np.array(self._initial_org_mixing_coeffs)

        logger.info(
            f"Starting D-matrix iteration for up to {self._max_iter} iterations."
        )
        total_start = time.perf_counter()

        for i in range(1, (self._max_iter or 1) + 1):
            iter_start = time.perf_counter()
            logger.debug(f"D-matrix iteration {i}...")
            gamma = self._gene_fitness_hist[i - 1, :]

            # When SelfConsistentMixStrategy is active, the mixing coefficients
            # evolve and D_total must be recomputed each step.
            if is_sc_gene or is_sc_org:
                all_coeffs = np.concatenate([gene_mix_coeffs, org_mix_coeffs])
                D_active, d_active = self._recompute_combined_d(all_coeffs)
            else:
                D_active, d_active = D, d

            # Fast replicator step
            bilinear = gamma * (D_active @ gamma) if D_active is not None else 0.0
            linear = d_active if d_active is not None else 0.0
            step = linear + bilinear
            gamma_new = gamma * (1.0 + step)

            if np.any(gamma_new <= 0):
                raise ValueError(
                    f"D-matrix step produced non-positive gene fitness at iteration "
                    f"{i}. Population structure may be incompatible with the "
                    "D-matrix path. Check for gene columns with mean expression > 0.5 "
                    "when using BalancedOrgStrategy."
                )
            gamma_new /= gamma_new.sum()

            self._gene_fitness_hist[i, :] = gamma_new
            self._org_fitness_hist[i, :] = self._population.matrix @ gamma_new

            # Update mixing coefficients for SelfConsistent
            if is_sc_gene:
                D_gene = self._D_per_strategy[:K_g]
                d_gene = self._d_per_strategy[:K_g]
                gene_mix_coeffs = SelfConsistentMixStrategy.update_coeffs_d_matrix(
                    D_gene, d_gene, gamma_new, gene_mix_coeffs
                )
            if is_sc_org:
                D_org = self._D_per_strategy[K_g:]
                d_org = self._d_per_strategy[K_g:]
                org_mix_coeffs = SelfConsistentMixStrategy.update_coeffs_d_matrix(
                    D_org, d_org, gamma_new, org_mix_coeffs
                )

            self._gene_mixing_coeffs_hist[i, :] = gene_mix_coeffs
            self._org_mixing_coeffs_hist[i, :] = org_mix_coeffs

            delta_norm = np.linalg.norm(gamma_new - gamma)
            iter_elapsed = time.perf_counter() - iter_start
            logger.debug(
                f"D-matrix iteration {i} done. Δ={delta_norm:.6g}. "
                f"Time: {iter_elapsed:.4f}s."
            )
            if epsilon is not None and delta_norm < epsilon:
                self._ESE_iter = i
                total_elapsed = time.perf_counter() - total_start
                logger.info(
                    f"D-matrix reached ESE after {i} iterations. "
                    f"Δ={delta_norm}. Total: {total_elapsed:.4f}s."
                )
                break
        else:
            total_elapsed = time.perf_counter() - total_start
            logger.info(
                f"D-matrix completed {self._max_iter} iterations without ESE. "
                f"Total: {total_elapsed:.4f}s."
            )

    def _recompute_combined_d(
        self, all_coeffs: np.ndarray
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Recompute the combined D matrix and d-vector from current mixing coefficients.

        Used only by ``_run_d_matrix_iterations()`` when ``SelfConsistentMixStrategy``
        is active (coefficients evolve each step).

        Args:
            all_coeffs: Combined mixing coefficients for gene strategies followed by
                org strategies, shape ``(K_g + K_o,)``.

        Returns:
            ``(D_total, d_total)`` with current coefficients applied.
        """
        M = self._population.M
        D_total = np.zeros((M, M))
        d_total = np.zeros(M)
        has_D = False
        has_d = False
        for idx, (D_s, d_s) in enumerate(
            zip(self._D_per_strategy, self._d_per_strategy)
        ):
            c = all_coeffs[idx]
            if D_s is not None:
                D_total += c * D_s
                has_D = True
            if d_s is not None:
                d_total += c * d_s
                has_d = True
        return (D_total if has_D else None), (d_total if has_d else None)

    def predict(self, population: PikaiaPopulation) -> np.ndarray:
        """
        Predicts the organism fitness for a new population using the fitted model.

        This method computes the organism fitness values for a given population
        based on the final gene fitness distribution obtained from the last iteration
        of the fitted model.

        Args:
            population (PikaiaPopulation): The new population for which to predict
                organism fitness.
        Returns:
            np.ndarray: A vector of predicted organism fitness values of shape (N,),
                where N is the number of organisms in the provided population.
        """
        if population.M != self._population.M:
            raise ValueError(
                "The number of genes in the new population must match the fitted model."
                f" Got {population.M}, expected {self._population.M}."
            )

        return np.dot(population.matrix, self._gene_fitness_hist[1, :])

Methods:

__init__(*args, use_d_matrix: bool = False, **kwargs)

Initialises the PikaiaModel.

Accepts all arguments of :class:GeneticModel plus:

Parameters:

Name Type Description Default
use_d_matrix bool

When True, the D-matrix fast path is used instead of the standard per-organism loop. Precomputes the (M, M) D matrix and (M,) d-vector once before the iteration loop, reducing per-step cost from O(N·M²) to O(M²). All active strategies must have a registered D-matrix kernel; a ValueError is raised at fit time if any do not. Defaults to False.

False
Source code in pikaia/models/pikaia_model.py
def __init__(self, *args, use_d_matrix: bool = False, **kwargs):
    """
    Initialises the PikaiaModel.

    Accepts all arguments of :class:`GeneticModel` plus:

    Args:
        use_d_matrix (bool):
            When ``True``, the D-matrix fast path is used instead of the
            standard per-organism loop. Precomputes the ``(M, M)`` D matrix
            and ``(M,)`` d-vector once before the iteration loop, reducing
            per-step cost from ``O(N·M²)`` to ``O(M²)``.  All active
            strategies must have a registered D-matrix kernel; a
            ``ValueError`` is raised at fit time if any do not.
            Defaults to ``False``.
    """
    super().__init__(*args, **kwargs)
    self._use_d_matrix = use_d_matrix

    # D-matrix state — populated by _compute_d_matrix() inside fit().
    # Initialized as empty lists so type-checkers accept slicing.
    self._D_matrix: np.ndarray | None = None
    self._d_vector: np.ndarray | None = None
    self._D_per_strategy: list = []
    self._d_per_strategy: list = []

fit() -> None

Fits the genetic model to the population data by running the simulation.

This method iteratively updates the gene and organism fitness values based on the provided strategies. The simulation runs for a maximum number of iterations as defined by max_iter. If an epsilon value is provided, the simulation will stop early if the change in gene fitness between iterations falls below this threshold, indicating convergence.

Source code in pikaia/models/pikaia_model.py
def fit(self) -> None:
    """
    Fits the genetic model to the population data by running the simulation.

    This method iteratively updates the gene and organism fitness values based on the
    provided strategies. The simulation runs for a maximum number of iterations as
    defined by `max_iter`. If an `epsilon` value is provided, the simulation will
    stop early if the change in gene fitness between iterations falls below this
    threshold, indicating convergence.
    """
    import time

    if self._initial_org_fitness_range == 0:
        logger.info(
            "Skipping fit: organism fitness range is 0. "
            "Returning uniform scores unchanged."
        )
        return

    start_time = time.perf_counter()
    if self._use_d_matrix:
        if self._max_iter is None:
            raise ValueError(
                "use_d_matrix=True requires max_iter to be set. "
                "There is no general closed-form fixed point for an arbitrary D matrix. "
                "Use max_iter with a large value (e.g. max_iter=500) and optionally "
                "epsilon for convergence detection, or use use_d_matrix=False for the "
                "analytical Dominant+Balanced fixed point."
            )
        logger.info("D-matrix path selected. Precomputing D matrix...")
        self._compute_d_matrix()
        logger.info(
            f"Running D-matrix simulation for up to {self._max_iter} iterations."
        )
        self._run_d_matrix_iterations()
    elif self._max_iter is None:
        logger.info("No max iterations set, solving for optimal solution directly.")
        self._run_fix_point()
    else:
        logger.info(f"Running simulation for up to {self._max_iter} iterations.")
        self._run_iterations()
    total_time = time.perf_counter() - start_time
    logger.info(f"Total fit process time: {total_time:.4f} seconds.")

predict(population: PikaiaPopulation) -> np.ndarray

Predicts the organism fitness for a new population using the fitted model.

This method computes the organism fitness values for a given population based on the final gene fitness distribution obtained from the last iteration of the fitted model.

Parameters:

Name Type Description Default
population PikaiaPopulation

The new population for which to predict organism fitness.

required

Returns: np.ndarray: A vector of predicted organism fitness values of shape (N,), where N is the number of organisms in the provided population.

Source code in pikaia/models/pikaia_model.py
def predict(self, population: PikaiaPopulation) -> np.ndarray:
    """
    Predicts the organism fitness for a new population using the fitted model.

    This method computes the organism fitness values for a given population
    based on the final gene fitness distribution obtained from the last iteration
    of the fitted model.

    Args:
        population (PikaiaPopulation): The new population for which to predict
            organism fitness.
    Returns:
        np.ndarray: A vector of predicted organism fitness values of shape (N,),
            where N is the number of organisms in the provided population.
    """
    if population.M != self._population.M:
        raise ValueError(
            "The number of genes in the new population must match the fitted model."
            f" Got {population.M}, expected {self._population.M}."
        )

    return np.dot(population.matrix, self._gene_fitness_hist[1, :])

Preprocessor

pikaia.preprocessing.pikaia_preprocessor.PikaiaPreprocessor

Preprocessor for Pikaia genetic algorithm data.

This class preprocesses feature data for use with the Pikaia genetic algorithm. It applies specified transformation functions to each feature and checks whether the transformed data is suitable for the genetic algorithm by checking that all values fall within the [0, 1] range.

The class follows the scikit-learn transformer interface, providing fit(), transform(), and fit_transform() methods for compatibility with ML pipelines.

Source code in pikaia/preprocessing/pikaia_preprocessor.py
class PikaiaPreprocessor:
    """
    Preprocessor for Pikaia genetic algorithm data.

    This class preprocesses feature data for use with the Pikaia genetic algorithm.
    It applies specified transformation functions to each feature and checks whether
    the transformed data is suitable for the genetic algorithm by checking that all
    values fall within the [0, 1] range.

    The class follows the scikit-learn transformer interface, providing fit(),
    transform(), and fit_transform() methods for compatibility with ML pipelines.
    """

    def __init__(
        self,
        num_features: int,
        feature_types: Sequence[FeatureType],
        feature_transforms: Sequence[Callable[[np.ndarray], np.ndarray] | None],
    ):
        """
        Initialize the PikaiaPreprocessor.

        Sets up the preprocessor with the specified number of features, their types,
        and the transformation functions to apply to each feature.

        Args:
            num_features (int): The number of features in the dataset. This must match
                the number of columns in the input data arrays passed to fit() and
                transform().
            feature_types (list[FeatureType]): A list of FeatureType enums, one for each
                feature. Each FeatureType indicates whether the feature represents a cost
                (lower values better) or gain (higher values better), though this info is
                stored for potential future use.
            feature_transforms (list[Callable[[NDArray], NDArray] | None]):
                A list of the same length as num_features. Each element is either a
                callable function that takes a 1D numpy array (a feature column) and
                returns a transformed 1D array, or None if no transformation should be
                applied to that feature.

        Raises:
            ValueError: If the lengths of feature_types or feature_transforms do not
                match num_features.
        """
        if len(feature_types) != num_features:
            raise ValueError(
                f"Length of feature_types ({len(feature_types)}) "
                f"must match num_features ({num_features})"
            )

        if len(feature_transforms) != num_features:
            raise ValueError(
                f"Length of feature_transforms ({len(feature_transforms)}) "
                f"must match num_features ({num_features})"
            )

        self.num_features = num_features
        self.feature_types = feature_types
        self.feature_transforms = feature_transforms

    def fit(self, X: np.ndarray) -> "PikaiaPreprocessor":
        """
        Fit the preprocessor to the input data.

        This method validates that the input data X has the correct number of features
        as specified during initialization. No actual fitting (e.g., parameter
        estimation) is performed since transformations are predefined.

        Args:
            X (np.ndarray): The input data array with shape (n_samples, n_features).
                Must have exactly num_features columns.

        Returns:
            PikaiaPreprocessor: Returns self to allow method chaining.

        Raises:
            ValueError: If the number of features in X does not match num_features.
        """
        if not np.issubdtype(X.dtype, np.number):
            raise ValueError("Input data must be numeric.")
        if np.any(np.isnan(X.astype(float))):
            raise ValueError("Input data must not contain NaN values.")
        if X.shape[1] != self.num_features:
            raise ValueError(
                f"Number of features in X ({X.shape[1]}) "
                f"does not match num_features ({self.num_features})"
            )

        return self

    def transform(self, X: np.ndarray) -> np.ndarray:
        """
        Transform the input data using the specified transformation functions.

        Applies the transformation function to each feature column if provided. For
        features marked as COST type, the values are inverted using the formula
        max_val + min_val - value to ensure higher values are more desirable for the
        genetic algorithm, regardless of the original data range. After all
        transformations, checks that all values in the transformed data are within the
        [0, 1] range. If not, logs a warning as this may indicate that the data is not
        suitable for the genetic algorithm.

        Args:
            X (np.ndarray): The input data array with shape (n_samples, n_features).
                Must have exactly num_features columns.

        Returns:
            np.ndarray: The transformed data array with the same shape as X, where each
                feature column has been processed according to the specified
                transformation and COST features have been inverted.

        Warns:
            Logs a warning if any values in the transformed data fall outside [0, 1].
        """
        if np.any(np.isnan(X.astype(float))):
            raise ValueError("Input data must not contain NaN values.")

        X_transformed = X.astype(float)

        for i in range(self.num_features):
            transform_func = self.feature_transforms[i]
            if transform_func is not None:
                X_transformed[:, i] = transform_func(X_transformed[:, i])
                if np.any(np.isnan(X_transformed[:, i])):
                    raise ValueError(
                        f"Transform function produced NaN values in feature column {i}."
                    )

            # Invert COST features
            if self.feature_types[i] == FeatureType.COST:
                min_val = np.min(X_transformed[:, i])
                max_val = np.max(X_transformed[:, i])
                X_transformed[:, i] = max_val + min_val - X_transformed[:, i]

            # Check if all feature values are in [0, 1]
            if not np.all((X_transformed[:, i] >= 0) & (X_transformed[:, i] <= 1)):
                logger.warning(
                    f"Some values in feature column {i} are outside [0,1] range "
                    "after transformation. This may not be valid input for the "
                    "genetic algorithm."
                )

        return X_transformed

    def fit_transform(self, X: np.ndarray) -> np.ndarray:
        """
        Fit the preprocessor and transform the data in one step.

        Equivalent to calling fit(X, y).transform(X). This is a convenience method
        for scikit-learn compatibility.

        Args:
            X (np.ndarray): The input data array with shape (n_samples, n_features).
                Must have exactly num_features columns.

        Returns:
            np.ndarray: The transformed data array with the same shape as X.
        """
        return self.fit(X).transform(X)

Methods:

__init__(num_features: int, feature_types: Sequence[FeatureType], feature_transforms: Sequence[Callable[[np.ndarray], np.ndarray] | None])

Initialize the PikaiaPreprocessor.

Sets up the preprocessor with the specified number of features, their types, and the transformation functions to apply to each feature.

Parameters:

Name Type Description Default
num_features int

The number of features in the dataset. This must match the number of columns in the input data arrays passed to fit() and transform().

required
feature_types list[FeatureType]

A list of FeatureType enums, one for each feature. Each FeatureType indicates whether the feature represents a cost (lower values better) or gain (higher values better), though this info is stored for potential future use.

required
feature_transforms list[Callable[[NDArray], NDArray] | None]

A list of the same length as num_features. Each element is either a callable function that takes a 1D numpy array (a feature column) and returns a transformed 1D array, or None if no transformation should be applied to that feature.

required

Raises:

Type Description
ValueError

If the lengths of feature_types or feature_transforms do not match num_features.

Source code in pikaia/preprocessing/pikaia_preprocessor.py
def __init__(
    self,
    num_features: int,
    feature_types: Sequence[FeatureType],
    feature_transforms: Sequence[Callable[[np.ndarray], np.ndarray] | None],
):
    """
    Initialize the PikaiaPreprocessor.

    Sets up the preprocessor with the specified number of features, their types,
    and the transformation functions to apply to each feature.

    Args:
        num_features (int): The number of features in the dataset. This must match
            the number of columns in the input data arrays passed to fit() and
            transform().
        feature_types (list[FeatureType]): A list of FeatureType enums, one for each
            feature. Each FeatureType indicates whether the feature represents a cost
            (lower values better) or gain (higher values better), though this info is
            stored for potential future use.
        feature_transforms (list[Callable[[NDArray], NDArray] | None]):
            A list of the same length as num_features. Each element is either a
            callable function that takes a 1D numpy array (a feature column) and
            returns a transformed 1D array, or None if no transformation should be
            applied to that feature.

    Raises:
        ValueError: If the lengths of feature_types or feature_transforms do not
            match num_features.
    """
    if len(feature_types) != num_features:
        raise ValueError(
            f"Length of feature_types ({len(feature_types)}) "
            f"must match num_features ({num_features})"
        )

    if len(feature_transforms) != num_features:
        raise ValueError(
            f"Length of feature_transforms ({len(feature_transforms)}) "
            f"must match num_features ({num_features})"
        )

    self.num_features = num_features
    self.feature_types = feature_types
    self.feature_transforms = feature_transforms

fit(X: np.ndarray) -> PikaiaPreprocessor

Fit the preprocessor to the input data.

This method validates that the input data X has the correct number of features as specified during initialization. No actual fitting (e.g., parameter estimation) is performed since transformations are predefined.

Parameters:

Name Type Description Default
X ndarray

The input data array with shape (n_samples, n_features). Must have exactly num_features columns.

required

Returns:

Name Type Description
PikaiaPreprocessor PikaiaPreprocessor

Returns self to allow method chaining.

Raises:

Type Description
ValueError

If the number of features in X does not match num_features.

Source code in pikaia/preprocessing/pikaia_preprocessor.py
def fit(self, X: np.ndarray) -> "PikaiaPreprocessor":
    """
    Fit the preprocessor to the input data.

    This method validates that the input data X has the correct number of features
    as specified during initialization. No actual fitting (e.g., parameter
    estimation) is performed since transformations are predefined.

    Args:
        X (np.ndarray): The input data array with shape (n_samples, n_features).
            Must have exactly num_features columns.

    Returns:
        PikaiaPreprocessor: Returns self to allow method chaining.

    Raises:
        ValueError: If the number of features in X does not match num_features.
    """
    if not np.issubdtype(X.dtype, np.number):
        raise ValueError("Input data must be numeric.")
    if np.any(np.isnan(X.astype(float))):
        raise ValueError("Input data must not contain NaN values.")
    if X.shape[1] != self.num_features:
        raise ValueError(
            f"Number of features in X ({X.shape[1]}) "
            f"does not match num_features ({self.num_features})"
        )

    return self

fit_transform(X: np.ndarray) -> np.ndarray

Fit the preprocessor and transform the data in one step.

Equivalent to calling fit(X, y).transform(X). This is a convenience method for scikit-learn compatibility.

Parameters:

Name Type Description Default
X ndarray

The input data array with shape (n_samples, n_features). Must have exactly num_features columns.

required

Returns:

Type Description
ndarray

np.ndarray: The transformed data array with the same shape as X.

Source code in pikaia/preprocessing/pikaia_preprocessor.py
def fit_transform(self, X: np.ndarray) -> np.ndarray:
    """
    Fit the preprocessor and transform the data in one step.

    Equivalent to calling fit(X, y).transform(X). This is a convenience method
    for scikit-learn compatibility.

    Args:
        X (np.ndarray): The input data array with shape (n_samples, n_features).
            Must have exactly num_features columns.

    Returns:
        np.ndarray: The transformed data array with the same shape as X.
    """
    return self.fit(X).transform(X)

transform(X: np.ndarray) -> np.ndarray

Transform the input data using the specified transformation functions.

Applies the transformation function to each feature column if provided. For features marked as COST type, the values are inverted using the formula max_val + min_val - value to ensure higher values are more desirable for the genetic algorithm, regardless of the original data range. After all transformations, checks that all values in the transformed data are within the [0, 1] range. If not, logs a warning as this may indicate that the data is not suitable for the genetic algorithm.

Parameters:

Name Type Description Default
X ndarray

The input data array with shape (n_samples, n_features). Must have exactly num_features columns.

required

Returns:

Type Description
ndarray

np.ndarray: The transformed data array with the same shape as X, where each feature column has been processed according to the specified transformation and COST features have been inverted.

Source code in pikaia/preprocessing/pikaia_preprocessor.py
def transform(self, X: np.ndarray) -> np.ndarray:
    """
    Transform the input data using the specified transformation functions.

    Applies the transformation function to each feature column if provided. For
    features marked as COST type, the values are inverted using the formula
    max_val + min_val - value to ensure higher values are more desirable for the
    genetic algorithm, regardless of the original data range. After all
    transformations, checks that all values in the transformed data are within the
    [0, 1] range. If not, logs a warning as this may indicate that the data is not
    suitable for the genetic algorithm.

    Args:
        X (np.ndarray): The input data array with shape (n_samples, n_features).
            Must have exactly num_features columns.

    Returns:
        np.ndarray: The transformed data array with the same shape as X, where each
            feature column has been processed according to the specified
            transformation and COST features have been inverted.

    Warns:
        Logs a warning if any values in the transformed data fall outside [0, 1].
    """
    if np.any(np.isnan(X.astype(float))):
        raise ValueError("Input data must not contain NaN values.")

    X_transformed = X.astype(float)

    for i in range(self.num_features):
        transform_func = self.feature_transforms[i]
        if transform_func is not None:
            X_transformed[:, i] = transform_func(X_transformed[:, i])
            if np.any(np.isnan(X_transformed[:, i])):
                raise ValueError(
                    f"Transform function produced NaN values in feature column {i}."
                )

        # Invert COST features
        if self.feature_types[i] == FeatureType.COST:
            min_val = np.min(X_transformed[:, i])
            max_val = np.max(X_transformed[:, i])
            X_transformed[:, i] = max_val + min_val - X_transformed[:, i]

        # Check if all feature values are in [0, 1]
        if not np.all((X_transformed[:, i] >= 0) & (X_transformed[:, i] <= 1)):
            logger.warning(
                f"Some values in feature column {i} are outside [0,1] range "
                "after transformation. This may not be valid input for the "
                "genetic algorithm."
            )

    return X_transformed

Plotter

pikaia.plotting.pikaia_plotter.PikaiaPlotter

A class for plotting results from a PikaiaModel.

This class provides a set of methods to visualize the outputs of an evolutionary simulation, including fitness histories, mixing coefficients, and similarity matrices.

Source code in pikaia/plotting/pikaia_plotter.py
class PikaiaPlotter:
    """
    A class for plotting results from a PikaiaModel.

    This class provides a set of methods to visualize the outputs of an evolutionary
    simulation, including fitness histories, mixing coefficients, and similarity matrices.

    """

    def __init__(self, model: PikaiaModel):
        """
        Initializes the PikaiaPlotter with a PikaiaModel instance.

        Args:
            model (PikaiaModel):
                The fitted PikaiaModel to be plotted.

        """
        self.model = model
        plt.style.use("seaborn-v0_8-whitegrid")

    def plot(
        self,
        plot_type: PlotType,
        show: bool = False,
        save_path: Path | None = None,
        gene_labels: list[str] | None = None,
        org_labels: list[str] | None = None,
        title: str | None = None,
    ) -> tuple[Figure, Axes]:
        """
        Plots the specified data from the model.

        Args:
            plot_type (PlotType):
                The type of plot to generate.
            show (bool):
                If True, the plot is displayed. Defaults to False.
            save_path (Path | None):
                If provided, the plot is saved to this path. Defaults to None.
            gene_labels (list[str] | None):
                Custom labels for genes.
            org_labels (list[str] | None):
                Custom labels for organisms.
            title : str | None
                Custom title for the plot. If None, a default title is used.

        Returns:
            tuple:
                A tuple containing the matplotlib Figure and Axes objects.

        """
        if gene_labels is None:
            gene_labels = [f"Gene {i}" for i in range(self.model.population.M)]
        if org_labels is None:
            org_labels = [f"Organism {i}" for i in range(self.model.population.N)]

        match plot_type:
            case PlotType.GENE_FITNESS_HISTORY:
                plot_title = title or "Gene Fitness History"
                fig, ax = self._plot_history(
                    data=self.model.gene_fitness_history,
                    title=plot_title,
                    ylabel="Fitness",
                    labels=gene_labels,
                    show=show,
                    save_path=save_path,
                )
            case PlotType.ORGANISM_FITNESS_HISTORY:
                plot_title = title or "Organism Fitness History"
                fig, ax = self._plot_history(
                    data=self.model.organism_fitness_history,
                    title=plot_title,
                    ylabel="Fitness",
                    labels=org_labels,
                    show=show,
                    save_path=save_path,
                )
            case PlotType.GENE_MIXING_HISTORY:
                plot_title = title or "Gene Mixing Coefficients History"
                fig, ax = self._plot_history(
                    data=self.model.gene_mixing_history,
                    title=plot_title,
                    ylabel="Mixing Coefficient",
                    labels=[strategy.name for strategy in self.model.gene_strategies],
                    show=show,
                    save_path=save_path,
                )
            case PlotType.ORGANISM_MIXING_HISTORY:
                plot_title = title or "Organism Mixing Coefficients History"
                fig, ax = self._plot_history(
                    data=self.model.organism_mixing_history,
                    title=plot_title,
                    ylabel="Mixing Coefficient",
                    labels=[strategy.name for strategy in self.model.org_strategies],
                    show=show,
                    save_path=save_path,
                )
            case PlotType.GENE_SIMILARITY:
                plot_title = title or "Gene Similarity Matrix"
                fig, ax = self._plot_heatmap(
                    data=self.model.gene_similarity,
                    title=plot_title,
                    labels=gene_labels,
                    show=show,
                    save_path=save_path,
                )
            case PlotType.ORGANISM_SIMILARITY:
                plot_title = title or "Organism Similarity Matrix"
                fig, ax = self._plot_heatmap(
                    data=self.model.org_similarity,
                    title=plot_title,
                    labels=org_labels,
                    show=show,
                    save_path=save_path,
                )
            case _:
                raise ValueError(f"Invalid plot type: {plot_type}")
        return fig, ax

    def _plot_history(
        self,
        data: np.ndarray,
        title: str,
        ylabel: str,
        labels: list[str] | None = None,
        show: bool = False,
        save_path: Path | None = None,
    ) -> tuple[Figure, Axes]:
        """
        Helper function to plot 2D history data.

        Args:
            data (np.ndarray):
                The 2D data array to plot (iterations x variables).
            title (str):
                The title of the plot.
            ylabel (str):
                The label for the y-axis.
            labels (list[str] | None, optional):
                Labels for each line. Defaults to None.
            show (bool):
                Whether to display the plot. Defaults to False.
            save_path (Path | None, optional):
                Filename to save the plot. Defaults to None.

        Returns:
            tuple:
                A tuple containing the matplotlib Figure and Axes objects.

        """
        fig, ax = plt.subplots(figsize=(10, 6))
        num_iterations, num_vars = data.shape
        iterations = range(num_iterations)

        for i in range(num_vars):
            label = labels[i] if labels else f"Variable {i + 1}"
            ax.plot(iterations, data[:, i], marker="o", linestyle="-", label=label)

        ax.set_title(title, fontsize=16)
        ax.set_xlabel("Iteration", fontsize=12)
        ax.set_ylabel(ylabel, fontsize=12)
        ax.legend()
        ax.grid(True)

        if save_path:
            plt.savefig(save_path.with_suffix(".png"), bbox_inches="tight", dpi=300)

        if show:
            plt.show()
        elif save_path is not None:
            plt.close(fig)
        return fig, ax

    def _plot_heatmap(
        self,
        data: np.ndarray,
        title: str,
        labels: list[str] | None = None,
        show: bool = False,
        save_path: Path | None = None,
    ) -> tuple[Figure, Axes]:
        """
        Helper function to plot a similarity matrix as a heatmap.

        Args:
            data (np.ndarray):
                The similarity matrix to plot.
            title (str):
                The title of the plot.
            labels (list[str] | None, optional):
                Labels for the ticks. Defaults to None.
            show (bool):
                Whether to display the plot. Defaults to False.
            save_path (Path | None, optional):
                Filename to save the plot. Defaults to None.

        Returns:
            tuple:
                A tuple containing the matplotlib Figure and Axes objects.

        """
        fig, ax = plt.subplots(figsize=(10, 6))
        cax = ax.matshow(data, cmap="viridis")
        fig.colorbar(cax)

        ax.set_title(title, fontsize=16, pad=20)

        if labels:
            ax.set_xticks(np.arange(len(labels)))
            ax.set_yticks(np.arange(len(labels)))
            ax.set_xticklabels(labels, rotation=90)
            ax.set_yticklabels(labels)

        if save_path:
            plt.savefig(save_path.with_suffix(".png"), bbox_inches="tight", dpi=300)

        if show:
            plt.show()
        return fig, ax

Methods:

__init__(model: PikaiaModel)

Initializes the PikaiaPlotter with a PikaiaModel instance.

Parameters:

Name Type Description Default
model PikaiaModel

The fitted PikaiaModel to be plotted.

required
Source code in pikaia/plotting/pikaia_plotter.py
def __init__(self, model: PikaiaModel):
    """
    Initializes the PikaiaPlotter with a PikaiaModel instance.

    Args:
        model (PikaiaModel):
            The fitted PikaiaModel to be plotted.

    """
    self.model = model
    plt.style.use("seaborn-v0_8-whitegrid")

plot(plot_type: PlotType, show: bool = False, save_path: Path | None = None, gene_labels: list[str] | None = None, org_labels: list[str] | None = None, title: str | None = None) -> tuple[Figure, Axes]

Plots the specified data from the model.

Parameters:

Name Type Description Default
plot_type PlotType

The type of plot to generate.

required
show bool

If True, the plot is displayed. Defaults to False.

False
save_path Path | None

If provided, the plot is saved to this path. Defaults to None.

None
gene_labels list[str] | None

Custom labels for genes.

None
org_labels list[str] | None

Custom labels for organisms.

None
title

str | None Custom title for the plot. If None, a default title is used.

required

Returns:

Name Type Description
tuple tuple[Figure, Axes]

A tuple containing the matplotlib Figure and Axes objects.

Source code in pikaia/plotting/pikaia_plotter.py
def plot(
    self,
    plot_type: PlotType,
    show: bool = False,
    save_path: Path | None = None,
    gene_labels: list[str] | None = None,
    org_labels: list[str] | None = None,
    title: str | None = None,
) -> tuple[Figure, Axes]:
    """
    Plots the specified data from the model.

    Args:
        plot_type (PlotType):
            The type of plot to generate.
        show (bool):
            If True, the plot is displayed. Defaults to False.
        save_path (Path | None):
            If provided, the plot is saved to this path. Defaults to None.
        gene_labels (list[str] | None):
            Custom labels for genes.
        org_labels (list[str] | None):
            Custom labels for organisms.
        title : str | None
            Custom title for the plot. If None, a default title is used.

    Returns:
        tuple:
            A tuple containing the matplotlib Figure and Axes objects.

    """
    if gene_labels is None:
        gene_labels = [f"Gene {i}" for i in range(self.model.population.M)]
    if org_labels is None:
        org_labels = [f"Organism {i}" for i in range(self.model.population.N)]

    match plot_type:
        case PlotType.GENE_FITNESS_HISTORY:
            plot_title = title or "Gene Fitness History"
            fig, ax = self._plot_history(
                data=self.model.gene_fitness_history,
                title=plot_title,
                ylabel="Fitness",
                labels=gene_labels,
                show=show,
                save_path=save_path,
            )
        case PlotType.ORGANISM_FITNESS_HISTORY:
            plot_title = title or "Organism Fitness History"
            fig, ax = self._plot_history(
                data=self.model.organism_fitness_history,
                title=plot_title,
                ylabel="Fitness",
                labels=org_labels,
                show=show,
                save_path=save_path,
            )
        case PlotType.GENE_MIXING_HISTORY:
            plot_title = title or "Gene Mixing Coefficients History"
            fig, ax = self._plot_history(
                data=self.model.gene_mixing_history,
                title=plot_title,
                ylabel="Mixing Coefficient",
                labels=[strategy.name for strategy in self.model.gene_strategies],
                show=show,
                save_path=save_path,
            )
        case PlotType.ORGANISM_MIXING_HISTORY:
            plot_title = title or "Organism Mixing Coefficients History"
            fig, ax = self._plot_history(
                data=self.model.organism_mixing_history,
                title=plot_title,
                ylabel="Mixing Coefficient",
                labels=[strategy.name for strategy in self.model.org_strategies],
                show=show,
                save_path=save_path,
            )
        case PlotType.GENE_SIMILARITY:
            plot_title = title or "Gene Similarity Matrix"
            fig, ax = self._plot_heatmap(
                data=self.model.gene_similarity,
                title=plot_title,
                labels=gene_labels,
                show=show,
                save_path=save_path,
            )
        case PlotType.ORGANISM_SIMILARITY:
            plot_title = title or "Organism Similarity Matrix"
            fig, ax = self._plot_heatmap(
                data=self.model.org_similarity,
                title=plot_title,
                labels=org_labels,
                show=show,
                save_path=save_path,
            )
        case _:
            raise ValueError(f"Invalid plot type: {plot_type}")
    return fig, ax

pikaia.plotting.pikaia_plotter.PlotType

Bases: StrEnum

Enum for the different types of plots.

Source code in pikaia/plotting/pikaia_plotter.py
class PlotType(StrEnum):
    """
    Enum for the different types of plots.
    """

    GENE_FITNESS_HISTORY = "gene_fitness_history"
    ORGANISM_FITNESS_HISTORY = "organism_fitness_history"
    GENE_MIXING_HISTORY = "gene_mixing_history"
    ORGANISM_MIXING_HISTORY = "organism_mixing_history"
    GENE_SIMILARITY = "gene_similarity"
    ORGANISM_SIMILARITY = "organism_similarity"

Schemas

pikaia.schemas.strategies.GeneStrategyEnum

Bases: str, Enum

Enum representing gene-level evolutionary strategies.

Source code in pikaia/schemas/strategies.py
class GeneStrategyEnum(str, Enum):
    """Enum representing gene-level evolutionary strategies."""

    DOMINANT = "DOMINANT"
    """Gene expresses dominance over others."""

    SELFISH = "SELFISH"
    """Gene acts in its own interest."""

    KIN_ALTRUISTIC = "KIN_ALTRUISTIC"
    """Gene favors kin altruism."""

    ALTRUISTIC = "ALTRUISTIC"
    """Gene acts altruistically toward others."""

    SELL_HARD = "SELL_HARD"
    """Trading sell signal weighted by gene difficulty.

    Hard genes (low mean expression) lose more value per unit of performance.
    Pair with ``OrgStrategyEnum.BUY_HARD``.
    """

    SELL_UNIFORM = "SELL_UNIFORM"
    """Trading sell signal applied uniformly to all genes.

    All genes lose value at the same rate, independent of difficulty.
    Pair with ``OrgStrategyEnum.BUY_UNIFORM``.
    """

    SELL_EASY = "SELL_EASY"
    """Trading sell signal weighted by gene ease — the inverse of ``SELL_HARD``.

    Easy genes (high mean expression) lose more value.
    Pair with ``OrgStrategyEnum.BUY_EASY``.
    """

    ENTROPY_MAX = "ENTROPY_MAX"
    """Information-theoretic supervised strategy.

    Rewards features with high mutual information with the target weighted by
    differential entropy.  Converges within 5 iterations.
    """

    ORTHO_GENE = "ORTHO_GENE"
    """Orthogonality-based strategy.

    Promotes features that are minimally correlated with all other features.
    """

    PARTIAL_CORR = "PARTIAL_CORR"
    """Partial-correlation supervised strategy.

    Rewards features whose relationship with the target survives controlling for
    all other features.
    """

    REDUNDANCY_PENALTY = "REDUNDANCY_PENALTY"
    """Redundancy-penalty strategy.

    Suppresses features that are highly correlated with their peers.
    """

    VARIANCE = "VARIANCE"
    """Rewards genes with high cross-organism dispersion (column std)."""

    NONE = "NONE"
    """No specific strategy — zero contribution."""

Attributes

ALTRUISTIC = 'ALTRUISTIC' class-attribute instance-attribute

Gene acts altruistically toward others.

DOMINANT = 'DOMINANT' class-attribute instance-attribute

Gene expresses dominance over others.

ENTROPY_MAX = 'ENTROPY_MAX' class-attribute instance-attribute

Information-theoretic supervised strategy.

Rewards features with high mutual information with the target weighted by differential entropy. Converges within 5 iterations.

KIN_ALTRUISTIC = 'KIN_ALTRUISTIC' class-attribute instance-attribute

Gene favors kin altruism.

NONE = 'NONE' class-attribute instance-attribute

No specific strategy — zero contribution.

ORTHO_GENE = 'ORTHO_GENE' class-attribute instance-attribute

Orthogonality-based strategy.

Promotes features that are minimally correlated with all other features.

PARTIAL_CORR = 'PARTIAL_CORR' class-attribute instance-attribute

Partial-correlation supervised strategy.

Rewards features whose relationship with the target survives controlling for all other features.

REDUNDANCY_PENALTY = 'REDUNDANCY_PENALTY' class-attribute instance-attribute

Redundancy-penalty strategy.

Suppresses features that are highly correlated with their peers.

SELFISH = 'SELFISH' class-attribute instance-attribute

Gene acts in its own interest.

SELL_EASY = 'SELL_EASY' class-attribute instance-attribute

Trading sell signal weighted by gene ease — the inverse of SELL_HARD.

Easy genes (high mean expression) lose more value. Pair with OrgStrategyEnum.BUY_EASY.

SELL_HARD = 'SELL_HARD' class-attribute instance-attribute

Trading sell signal weighted by gene difficulty.

Hard genes (low mean expression) lose more value per unit of performance. Pair with OrgStrategyEnum.BUY_HARD.

SELL_UNIFORM = 'SELL_UNIFORM' class-attribute instance-attribute

Trading sell signal applied uniformly to all genes.

All genes lose value at the same rate, independent of difficulty. Pair with OrgStrategyEnum.BUY_UNIFORM.

VARIANCE = 'VARIANCE' class-attribute instance-attribute

Rewards genes with high cross-organism dispersion (column std).

pikaia.schemas.strategies.OrgStrategyEnum

Bases: str, Enum

Enum representing organism-level evolutionary strategies.

Source code in pikaia/schemas/strategies.py
class OrgStrategyEnum(str, Enum):
    """Enum representing organism-level evolutionary strategies."""

    BALANCED = "BALANCED"
    """Organism balances gene contributions to promote uniform fitness."""

    ALTRUISTIC = "ALTRUISTIC"
    """Organism acts altruistically toward similar organisms."""

    KIN_SELFISH = "KIN_SELFISH"
    """Organism is selfish toward non-kin, altruistic toward kin."""

    SELFISH = "SELFISH"
    """Organism acts selfishly, promoting its own gene expression."""

    BUY_HARD = "BUY_HARD"
    """Trading buy-phase paired with ``SELL_HARD``.

    Redistributes hard-gene sell capital to easy genes the organism failed.
    Pair with ``GeneStrategyEnum.SELL_HARD``.
    """

    BUY_UNIFORM = "BUY_UNIFORM"
    """Trading buy-phase paired with ``SELL_UNIFORM``.

    Redistributes uniform sell capital to hard genes the organism failed.
    Pair with ``GeneStrategyEnum.SELL_UNIFORM``.
    """

    BUY_EASY = "BUY_EASY"
    """Trading buy-phase paired with ``SELL_EASY`` — mirror of ``BUY_HARD``.

    Redistributes capital with inverted sign relative to ``BUY_HARD``.
    Pair with ``GeneStrategyEnum.SELL_EASY``.
    """

    NONE = "NONE"
    """No specific strategy — zero contribution."""

Attributes

ALTRUISTIC = 'ALTRUISTIC' class-attribute instance-attribute

Organism acts altruistically toward similar organisms.

BALANCED = 'BALANCED' class-attribute instance-attribute

Organism balances gene contributions to promote uniform fitness.

BUY_EASY = 'BUY_EASY' class-attribute instance-attribute

Trading buy-phase paired with SELL_EASY — mirror of BUY_HARD.

Redistributes capital with inverted sign relative to BUY_HARD. Pair with GeneStrategyEnum.SELL_EASY.

BUY_HARD = 'BUY_HARD' class-attribute instance-attribute

Trading buy-phase paired with SELL_HARD.

Redistributes hard-gene sell capital to easy genes the organism failed. Pair with GeneStrategyEnum.SELL_HARD.

BUY_UNIFORM = 'BUY_UNIFORM' class-attribute instance-attribute

Trading buy-phase paired with SELL_UNIFORM.

Redistributes uniform sell capital to hard genes the organism failed. Pair with GeneStrategyEnum.SELL_UNIFORM.

KIN_SELFISH = 'KIN_SELFISH' class-attribute instance-attribute

Organism is selfish toward non-kin, altruistic toward kin.

NONE = 'NONE' class-attribute instance-attribute

No specific strategy — zero contribution.

SELFISH = 'SELFISH' class-attribute instance-attribute

Organism acts selfishly, promoting its own gene expression.

pikaia.schemas.strategies.MixStrategyEnum

Bases: str, Enum

Enum representing strategy mixing modes.

Source code in pikaia/schemas/strategies.py
class MixStrategyEnum(str, Enum):
    """Enum representing strategy mixing modes."""

    NONE = "NONE"
    """No mixed strategy applied."""

    FIXED = "FIXED"
    """Fixed mixing coefficients — proportions do not adapt over iterations."""

    SELF_CONSISTENT = "SELF_CONSISTENT"
    """Self-consistent mixing — coefficients adapt each iteration based on delta magnitude."""

Attributes

FIXED = 'FIXED' class-attribute instance-attribute

Fixed mixing coefficients — proportions do not adapt over iterations.

NONE = 'NONE' class-attribute instance-attribute

No mixed strategy applied.

SELF_CONSISTENT = 'SELF_CONSISTENT' class-attribute instance-attribute

Self-consistent mixing — coefficients adapt each iteration based on delta magnitude.

Strategies

Base Classes

pikaia.strategies.base_strategies.GeneStrategy

Bases: ABC

Abstract base class for gene strategies.

Defines the interface for all gene-level evolutionary strategies. Subclasses must implement the __call__ method, which calculates the fitness delta for a specific gene based on the strategy's logic.

Source code in pikaia/strategies/base_strategies.py
class GeneStrategy(ABC):
    """
    Abstract base class for gene strategies.

    Defines the interface for all gene-level evolutionary strategies. Subclasses
    must implement the `__call__` method, which calculates the fitness delta
    for a specific gene based on the strategy's logic.

    """

    def __init__(self, **kwargs):
        """
        Initializes the strategy with optional parameters.

        Args:
            **kwargs:
                Arbitrary keyword arguments that can be used to configure
                the strategy. These are stored in the `self.options` dictionary.

        """
        self.options = kwargs

    @property
    @abstractmethod
    def name(self) -> str:
        """The name of the strategy."""
        pass  # pragma: no cover

    @abstractmethod
    def __call__(self, ctx: StrategyContext) -> float:
        """
        Computes the delta for a gene strategy.

        Args:
            ctx (StrategyContext):
                Context object containing all required and optional fields.

        Returns:
            float:
                The computed delta value `Delta_G(i,j)` for the specified gene and organism.

        """
        pass  # pragma: no cover

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: Optional[np.ndarray] = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Return the D-matrix kernel contribution ``(D, d)`` for this strategy.

        ``D`` is an ``(M, M)`` array for the bilinear term
        ``gamma * (D @ gamma)``; ``d`` is an ``(M,)`` array for the linear
        term.  Either may be ``None`` when the strategy has no contribution
        of that type.

        The default returns ``(None, None)`` (zero contribution).  Subclasses
        that support the fast D-matrix path override this method.
        """
        return None, None

Attributes

name: str abstractmethod property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float abstractmethod

Computes the delta for a gene strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Name Type Description
float float

The computed delta value Delta_G(i,j) for the specified gene and organism.

Source code in pikaia/strategies/base_strategies.py
@abstractmethod
def __call__(self, ctx: StrategyContext) -> float:
    """
    Computes the delta for a gene strategy.

    Args:
        ctx (StrategyContext):
            Context object containing all required and optional fields.

    Returns:
        float:
            The computed delta value `Delta_G(i,j)` for the specified gene and organism.

    """
    pass  # pragma: no cover

__init__(**kwargs)

Initializes the strategy with optional parameters.

Parameters:

Name Type Description Default
**kwargs

Arbitrary keyword arguments that can be used to configure the strategy. These are stored in the self.options dictionary.

{}
Source code in pikaia/strategies/base_strategies.py
def __init__(self, **kwargs):
    """
    Initializes the strategy with optional parameters.

    Args:
        **kwargs:
            Arbitrary keyword arguments that can be used to configure
            the strategy. These are stored in the `self.options` dictionary.

    """
    self.options = kwargs

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: Optional[np.ndarray] = None) -> tuple[np.ndarray | None, np.ndarray | None]

Return the D-matrix kernel contribution (D, d) for this strategy.

D is an (M, M) array for the bilinear term gamma * (D @ gamma); d is an (M,) array for the linear term. Either may be None when the strategy has no contribution of that type.

The default returns (None, None) (zero contribution). Subclasses that support the fast D-matrix path override this method.

Source code in pikaia/strategies/base_strategies.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: Optional[np.ndarray] = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Return the D-matrix kernel contribution ``(D, d)`` for this strategy.

    ``D`` is an ``(M, M)`` array for the bilinear term
    ``gamma * (D @ gamma)``; ``d`` is an ``(M,)`` array for the linear
    term.  Either may be ``None`` when the strategy has no contribution
    of that type.

    The default returns ``(None, None)`` (zero contribution).  Subclasses
    that support the fast D-matrix path override this method.
    """
    return None, None

pikaia.strategies.base_strategies.OrgStrategy

Bases: ABC

Abstract base class for organism strategies.

Defines the interface for all organism-level evolutionary strategies. Subclasses must implement the __call__ method, which calculates the fitness deltas for all genes based on the organism's interactions.

Source code in pikaia/strategies/base_strategies.py
class OrgStrategy(ABC):
    """
    Abstract base class for organism strategies.

    Defines the interface for all organism-level evolutionary strategies.
    Subclasses must implement the `__call__` method, which calculates the
    fitness deltas for all genes based on the organism's interactions.

    """

    def __init__(self, **kwargs):
        """
        Initializes the strategy with optional parameters.

        Args:
            **kwargs:
                Arbitrary keyword arguments that can be used to configure
                the strategy. These are stored in the `self.options` dictionary.

        """
        self.options = kwargs

    @property
    @abstractmethod
    def name(self) -> str:
        """The name of the strategy."""
        pass  # pragma: no cover

    @abstractmethod
    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        """
        Computes deltas for an organism strategy.

        Args:
            ctx (StrategyContext):
                Context object containing all required and optional fields.

        Returns:
            np.ndarray:
                A vector of shape `(m,)` containing the computed
                delta values `Delta_O(i,j)` for the specified organism.

        """
        pass  # pragma: no cover

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: Optional[np.ndarray] = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Return the D-matrix kernel contribution ``(D, d)`` for this strategy.

        The default returns ``(None, None)`` (zero contribution).  Subclasses
        that support the fast D-matrix path override this method.
        """
        return None, None

Attributes

name: str abstractmethod property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> np.ndarray abstractmethod

Computes deltas for an organism strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Type Description
ndarray

np.ndarray: A vector of shape (m,) containing the computed delta values Delta_O(i,j) for the specified organism.

Source code in pikaia/strategies/base_strategies.py
@abstractmethod
def __call__(self, ctx: StrategyContext) -> np.ndarray:
    """
    Computes deltas for an organism strategy.

    Args:
        ctx (StrategyContext):
            Context object containing all required and optional fields.

    Returns:
        np.ndarray:
            A vector of shape `(m,)` containing the computed
            delta values `Delta_O(i,j)` for the specified organism.

    """
    pass  # pragma: no cover

__init__(**kwargs)

Initializes the strategy with optional parameters.

Parameters:

Name Type Description Default
**kwargs

Arbitrary keyword arguments that can be used to configure the strategy. These are stored in the self.options dictionary.

{}
Source code in pikaia/strategies/base_strategies.py
def __init__(self, **kwargs):
    """
    Initializes the strategy with optional parameters.

    Args:
        **kwargs:
            Arbitrary keyword arguments that can be used to configure
            the strategy. These are stored in the `self.options` dictionary.

    """
    self.options = kwargs

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: Optional[np.ndarray] = None) -> tuple[np.ndarray | None, np.ndarray | None]

Return the D-matrix kernel contribution (D, d) for this strategy.

The default returns (None, None) (zero contribution). Subclasses that support the fast D-matrix path override this method.

Source code in pikaia/strategies/base_strategies.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: Optional[np.ndarray] = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Return the D-matrix kernel contribution ``(D, d)`` for this strategy.

    The default returns ``(None, None)`` (zero contribution).  Subclasses
    that support the fast D-matrix path override this method.
    """
    return None, None

pikaia.strategies.base_strategies.MixStrategy

Bases: ABC

Abstract base class for mixing strategies.

Defines the interface for strategies that determine how to mix or weigh the contributions of different evolutionary strategies (gene or organism). Subclasses must implement the __call__ method.

Source code in pikaia/strategies/base_strategies.py
class MixStrategy(ABC):
    """
    Abstract base class for mixing strategies.

    Defines the interface for strategies that determine how to mix or weigh
    the contributions of different evolutionary strategies (gene or organism).
    Subclasses must implement the `__call__` method.

    """

    def __init__(self, **kwargs):
        """
        Initializes the strategy with optional parameters.

        Args:
            **kwargs:
                Arbitrary keyword arguments that can be used to configure
                the strategy. These are stored in the `self.options` dictionary.

        """
        self.options = kwargs

    @property
    @abstractmethod
    def name(self) -> str:
        """The name of the strategy."""
        pass  # pragma: no cover

    @abstractmethod
    def __call__(
        self, delta: np.ndarray, mix_coeffs: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray]:
        """
        Mixes organism deltas and dynamically updates mixing coefficients.

        The method first calculates the combined delta using the current mixing
        coefficients. It then updates these coefficients for the next iteration.

        Args:
            delta (np.ndarray):
                A 3D array of shape `(n, m, n_strat)` containing the delta matrices
                from each strategy.
            mix_coeffs (np.ndarray):
                A 1D array of shape `(n_strat,)` containing the current mixing
                coefficients for each strategy.

        Returns:
            tuple:
                np.ndarray: The mixed delta matrix of shape `(n, m)`.
                np.ndarray: The updated mixing coefficients for the next iteration.

        """
        pass  # pragma: no cover

Attributes

name: str abstractmethod property

The name of the strategy.

Methods:

__call__(delta: np.ndarray, mix_coeffs: np.ndarray) -> tuple[np.ndarray, np.ndarray] abstractmethod

Mixes organism deltas and dynamically updates mixing coefficients.

The method first calculates the combined delta using the current mixing coefficients. It then updates these coefficients for the next iteration.

Parameters:

Name Type Description Default
delta ndarray

A 3D array of shape (n, m, n_strat) containing the delta matrices from each strategy.

required
mix_coeffs ndarray

A 1D array of shape (n_strat,) containing the current mixing coefficients for each strategy.

required

Returns:

Name Type Description
tuple tuple[ndarray, ndarray]

np.ndarray: The mixed delta matrix of shape (n, m). np.ndarray: The updated mixing coefficients for the next iteration.

Source code in pikaia/strategies/base_strategies.py
@abstractmethod
def __call__(
    self, delta: np.ndarray, mix_coeffs: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
    """
    Mixes organism deltas and dynamically updates mixing coefficients.

    The method first calculates the combined delta using the current mixing
    coefficients. It then updates these coefficients for the next iteration.

    Args:
        delta (np.ndarray):
            A 3D array of shape `(n, m, n_strat)` containing the delta matrices
            from each strategy.
        mix_coeffs (np.ndarray):
            A 1D array of shape `(n_strat,)` containing the current mixing
            coefficients for each strategy.

    Returns:
        tuple:
            np.ndarray: The mixed delta matrix of shape `(n, m)`.
            np.ndarray: The updated mixing coefficients for the next iteration.

    """
    pass  # pragma: no cover

__init__(**kwargs)

Initializes the strategy with optional parameters.

Parameters:

Name Type Description Default
**kwargs

Arbitrary keyword arguments that can be used to configure the strategy. These are stored in the self.options dictionary.

{}
Source code in pikaia/strategies/base_strategies.py
def __init__(self, **kwargs):
    """
    Initializes the strategy with optional parameters.

    Args:
        **kwargs:
            Arbitrary keyword arguments that can be used to configure
            the strategy. These are stored in the `self.options` dictionary.

    """
    self.options = kwargs

pikaia.strategies.base_strategies.StrategyContext dataclass

Holds the context for a strategy calculation.

Source code in pikaia/strategies/base_strategies.py
@dataclass(slots=True)
class StrategyContext:
    """Holds the context for a strategy calculation."""

    #: The population object.
    population: PikaiaPopulation
    #: The fitness array for all organisms, shape ``(n,)``.
    org_fitness: np.ndarray
    #: The fitness array for all genes, shape ``(m,)``.
    gene_fitness: np.ndarray
    #: A similarity matrix for organisms, shape ``(n, n)``.
    org_similarity: np.ndarray
    #: A similarity matrix for genes, shape ``(m, m)``.
    gene_similarity: np.ndarray
    #: The initial range of organism fitness values.
    initial_org_fitness_range: float
    #: The index of the current organism being evaluated.
    org_id: Optional[int] = None
    #: The index of the current gene being evaluated.
    gene_id: Optional[int] = None
    #: Optional target variable for supervised strategies.
    y: Optional[np.ndarray] = None

Factories

pikaia.strategies.strategy_factories.GeneStrategyFactory

Factory class for creating gene strategy instances.

This factory provides a centralized way to instantiate gene strategy objects based on the GeneStrategyEnum. It maps the enum members to their corresponding strategy classes.

Source code in pikaia/strategies/strategy_factories.py
class GeneStrategyFactory:
    """
    Factory class for creating gene strategy instances.

    This factory provides a centralized way to instantiate gene strategy
    objects based on the `GeneStrategyEnum`. It maps the enum members to their
    corresponding strategy classes.
    """

    _strategies = {
        GeneStrategyEnum.DOMINANT: DominantGeneStrategy,
        GeneStrategyEnum.ALTRUISTIC: AltruisticGeneStrategy,
        GeneStrategyEnum.KIN_ALTRUISTIC: KinAltruisticGeneStrategy,
        GeneStrategyEnum.SELFISH: SelfishGeneStrategy,
        GeneStrategyEnum.SELL_HARD: SellHardGeneStrategy,
        GeneStrategyEnum.SELL_UNIFORM: SellUniformGeneStrategy,
        GeneStrategyEnum.SELL_EASY: SellEasyGeneStrategy,
        GeneStrategyEnum.ENTROPY_MAX: EntropyMaxGeneStrategy,
        GeneStrategyEnum.ORTHO_GENE: OrthoGeneStrategy,
        GeneStrategyEnum.PARTIAL_CORR: PartialCorrGeneStrategy,
        GeneStrategyEnum.REDUNDANCY_PENALTY: RedundancyPenaltyGeneStrategy,
        GeneStrategyEnum.VARIANCE: VarianceGeneStrategy,
        GeneStrategyEnum.NONE: NoneGeneStrategy,
    }

    @classmethod
    def get_strategy(cls, name: GeneStrategyEnum, *args, **kwargs) -> GeneStrategy:
        """
        Retrieves an instance of the requested gene strategy.

        Args:
            name (GeneStrategyEnum): The enum member representing the desired
                strategy.
            ``*args``: Positional arguments to pass to the strategy's constructor.
            ``**kwargs``: Keyword arguments to pass to the strategy's constructor.

        Returns:
            GeneStrategy: An instance of the corresponding gene strategy class.

        Raises:
            ValueError: If the requested strategy name is not found in the
                factory's registry.
        """
        strategy_cls = cls._strategies.get(name)
        if strategy_cls is None:
            raise ValueError(f"Strategy '{name}' not found.")
        return strategy_cls(*args, **kwargs)

Methods:

get_strategy(name: GeneStrategyEnum, *args, **kwargs) -> GeneStrategy classmethod

Retrieves an instance of the requested gene strategy.

Parameters:

Name Type Description Default
name GeneStrategyEnum

The enum member representing the desired strategy.

required
``*args``

Positional arguments to pass to the strategy's constructor.

required
``**kwargs``

Keyword arguments to pass to the strategy's constructor.

required

Returns:

Name Type Description
GeneStrategy GeneStrategy

An instance of the corresponding gene strategy class.

Raises:

Type Description
ValueError

If the requested strategy name is not found in the factory's registry.

Source code in pikaia/strategies/strategy_factories.py
@classmethod
def get_strategy(cls, name: GeneStrategyEnum, *args, **kwargs) -> GeneStrategy:
    """
    Retrieves an instance of the requested gene strategy.

    Args:
        name (GeneStrategyEnum): The enum member representing the desired
            strategy.
        ``*args``: Positional arguments to pass to the strategy's constructor.
        ``**kwargs``: Keyword arguments to pass to the strategy's constructor.

    Returns:
        GeneStrategy: An instance of the corresponding gene strategy class.

    Raises:
        ValueError: If the requested strategy name is not found in the
            factory's registry.
    """
    strategy_cls = cls._strategies.get(name)
    if strategy_cls is None:
        raise ValueError(f"Strategy '{name}' not found.")
    return strategy_cls(*args, **kwargs)

pikaia.strategies.strategy_factories.OrgStrategyFactory

Factory class for creating organism strategy instances.

This factory provides a centralized way to instantiate organism strategy objects based on the OrgStrategyEnum. It maps the enum members to their corresponding strategy classes.

Source code in pikaia/strategies/strategy_factories.py
class OrgStrategyFactory:
    """
    Factory class for creating organism strategy instances.

    This factory provides a centralized way to instantiate organism strategy
    objects based on the `OrgStrategyEnum`. It maps the enum members to their
    corresponding strategy classes.
    """

    _strategies = {
        OrgStrategyEnum.BALANCED: BalancedOrgStrategy,
        OrgStrategyEnum.ALTRUISTIC: AltruisticOrgStrategy,
        OrgStrategyEnum.KIN_SELFISH: KinSelfishOrgStrategy,
        OrgStrategyEnum.SELFISH: SelfishOrgStrategy,
        OrgStrategyEnum.BUY_HARD: BuyHardOrgStrategy,
        OrgStrategyEnum.BUY_UNIFORM: BuyUniformOrgStrategy,
        OrgStrategyEnum.BUY_EASY: BuyEasyOrgStrategy,
        OrgStrategyEnum.NONE: NoneOrgStrategy,
    }

    @classmethod
    def get_strategy(cls, name: OrgStrategyEnum, *args, **kwargs) -> OrgStrategy:
        """
        Retrieves an instance of the requested organism strategy.

        Args:
            name (OrgStrategyEnum): The enum member representing the desired
                strategy.
            ``*args``: Positional arguments to pass to the strategy's constructor.
            ``**kwargs``: Keyword arguments to pass to the strategy's constructor.

        Returns:
            OrgStrategy: An instance of the corresponding organism strategy class.

        Raises:
            ValueError: If the requested strategy name is not found in the
                factory's registry.
        """
        strategy_cls = cls._strategies.get(name)
        if strategy_cls is None:
            raise ValueError(f"Strategy '{name}' not found.")
        return strategy_cls(*args, **kwargs)

Methods:

get_strategy(name: OrgStrategyEnum, *args, **kwargs) -> OrgStrategy classmethod

Retrieves an instance of the requested organism strategy.

Parameters:

Name Type Description Default
name OrgStrategyEnum

The enum member representing the desired strategy.

required
``*args``

Positional arguments to pass to the strategy's constructor.

required
``**kwargs``

Keyword arguments to pass to the strategy's constructor.

required

Returns:

Name Type Description
OrgStrategy OrgStrategy

An instance of the corresponding organism strategy class.

Raises:

Type Description
ValueError

If the requested strategy name is not found in the factory's registry.

Source code in pikaia/strategies/strategy_factories.py
@classmethod
def get_strategy(cls, name: OrgStrategyEnum, *args, **kwargs) -> OrgStrategy:
    """
    Retrieves an instance of the requested organism strategy.

    Args:
        name (OrgStrategyEnum): The enum member representing the desired
            strategy.
        ``*args``: Positional arguments to pass to the strategy's constructor.
        ``**kwargs``: Keyword arguments to pass to the strategy's constructor.

    Returns:
        OrgStrategy: An instance of the corresponding organism strategy class.

    Raises:
        ValueError: If the requested strategy name is not found in the
            factory's registry.
    """
    strategy_cls = cls._strategies.get(name)
    if strategy_cls is None:
        raise ValueError(f"Strategy '{name}' not found.")
    return strategy_cls(*args, **kwargs)

pikaia.strategies.strategy_factories.MixStrategyFactory

Factory class for creating mixing strategy instances.

This factory provides a centralized way to instantiate mixing strategy objects based on the MixinGeneStrategyEnum.

Source code in pikaia/strategies/strategy_factories.py
class MixStrategyFactory:
    """
    Factory class for creating mixing strategy instances.

    This factory provides a centralized way to instantiate mixing strategy
    objects based on the `MixinGeneStrategyEnum`.
    """

    _strategies = {
        MixStrategyEnum.FIXED: FixedMixStrategy,
        MixStrategyEnum.SELF_CONSISTENT: SelfConsistentMixStrategy,
    }

    @classmethod
    def get_strategy(cls, name: MixStrategyEnum, *args, **kwargs) -> MixStrategy:
        """
        Retrieves a singleton instance of the requested mixing strategy.

        Args:
            name (MixinGeneStrategyEnum): The enum member representing the desired
                strategy.
            ``*args``: Positional arguments to pass to the strategy's constructor.
            ``**kwargs``: Keyword arguments to pass to the strategy's constructor.

        Returns:
            MixStrategy: An instance of the corresponding mixing strategy class.

        Raises:
            ValueError: If the requested strategy name is not found.
        """
        strategy_cls = cls._strategies.get(name)
        if strategy_cls is None:
            raise ValueError(f"Strategy '{name}' not found.")
        return strategy_cls(*args, **kwargs)

Methods:

get_strategy(name: MixStrategyEnum, *args, **kwargs) -> MixStrategy classmethod

Retrieves a singleton instance of the requested mixing strategy.

Parameters:

Name Type Description Default
name MixinGeneStrategyEnum

The enum member representing the desired strategy.

required
``*args``

Positional arguments to pass to the strategy's constructor.

required
``**kwargs``

Keyword arguments to pass to the strategy's constructor.

required

Returns:

Name Type Description
MixStrategy MixStrategy

An instance of the corresponding mixing strategy class.

Raises:

Type Description
ValueError

If the requested strategy name is not found.

Source code in pikaia/strategies/strategy_factories.py
@classmethod
def get_strategy(cls, name: MixStrategyEnum, *args, **kwargs) -> MixStrategy:
    """
    Retrieves a singleton instance of the requested mixing strategy.

    Args:
        name (MixinGeneStrategyEnum): The enum member representing the desired
            strategy.
        ``*args``: Positional arguments to pass to the strategy's constructor.
        ``**kwargs``: Keyword arguments to pass to the strategy's constructor.

    Returns:
        MixStrategy: An instance of the corresponding mixing strategy class.

    Raises:
        ValueError: If the requested strategy name is not found.
    """
    strategy_cls = cls._strategies.get(name)
    if strategy_cls is None:
        raise ValueError(f"Strategy '{name}' not found.")
    return strategy_cls(*args, **kwargs)

Gene Strategies

pikaia.strategies.gs_strategies.dominant_strategy.DominantGeneStrategy

Bases: GeneStrategy

A gene strategy that promotes dominant genes.

This strategy increases the fitness of genes that are highly expressed (dominant), reinforcing their prevalence in the population. The delta is proportional to the square of the gene's fitness and its expression level. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/gs_strategies/dominant_strategy.py
class DominantGeneStrategy(GeneStrategy):
    """
    A gene strategy that promotes dominant genes.

    This strategy increases the fitness of genes that are highly expressed
    (dominant), reinforcing their prevalence in the population. The delta is
    proportional to the square of the gene's fitness and its expression level.
    This implementation follows the logic from the original `alg.py`.
    """

    def __init__(self, **kwargs):
        """Initialise the Dominant gene strategy.

        Args:
            **kwargs: Keyword options forwarded to `GeneStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Dominant"

    def __call__(self, ctx: StrategyContext) -> float:
        """
        Computes the delta for a dominant gene.

        The formula reinforces the fitness of the gene based on its current
        fitness and expression.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
        """
        return float(
            # constant factor and normalization by population size
            (4 / ctx.population.N)
            # fitness of current gene squared
            * ctx.gene_fitness[ctx.gene_id] ** 2
            # gene variant fitness minus 0.5
            * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Diagonal D matrix from population mean expression.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Unused.
            org_similarity: Unused.
            initial_org_fitness_range: Unused.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix
            with ``D[j, j] = 4 * (x_bar_j - 0.5)``.
        """
        D = np.diag(4.0 * (population.matrix.mean(axis=0) - 0.5))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Computes the delta for a dominant gene.

The formula reinforces the fitness of the gene based on its current fitness and expression.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Name Type Description
float float

The computed delta value Delta_G(i,j) for the specified gene and organism.

Source code in pikaia/strategies/gs_strategies/dominant_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """
    Computes the delta for a dominant gene.

    The formula reinforces the fitness of the gene based on its current
    fitness and expression.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
    """
    return float(
        # constant factor and normalization by population size
        (4 / ctx.population.N)
        # fitness of current gene squared
        * ctx.gene_fitness[ctx.gene_id] ** 2
        # gene variant fitness minus 0.5
        * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
    )

__init__(**kwargs)

Initialise the Dominant gene strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to GeneStrategy and stored in self.options.

{}
Source code in pikaia/strategies/gs_strategies/dominant_strategy.py
def __init__(self, **kwargs):
    """Initialise the Dominant gene strategy.

    Args:
        **kwargs: Keyword options forwarded to `GeneStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Diagonal D matrix from population mean expression.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Unused.

required
org_similarity ndarray

Unused.

required
initial_org_fitness_range float

Unused.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is a diagonal (M, M) matrix

ndarray | None

with D[j, j] = 4 * (x_bar_j - 0.5).

Source code in pikaia/strategies/gs_strategies/dominant_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Diagonal D matrix from population mean expression.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Unused.
        org_similarity: Unused.
        initial_org_fitness_range: Unused.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix
        with ``D[j, j] = 4 * (x_bar_j - 0.5)``.
    """
    D = np.diag(4.0 * (population.matrix.mean(axis=0) - 0.5))
    return D, None

pikaia.strategies.gs_strategies.sell_hard_strategy.SellHardGeneStrategy

Bases: GeneStrategy

Trading sell signal weighted by gene difficulty.

Hard genes (low mean expression, high exclusiveness) lose more value per unit of performance — organisms that solved them "sell" at a premium.

For organism i, gene j:

\[ \Delta_{\text{sell\_hard}}(i, j) = -\frac{x_{ij}}{N} \cdot \frac{\text{excl}_j}{1 - \text{excl}_j + \varepsilon} \]

Summed over all organisms this equals -mean_j · excl_j / (1 - excl_j), the proportional sell loss weighted by gene difficulty.

Pair with BuyHardOrgStrategy for the full hard-gene trading round.

Source code in pikaia/strategies/gs_strategies/sell_hard_strategy.py
class SellHardGeneStrategy(GeneStrategy):
    """
    Trading sell signal weighted by gene difficulty.

    Hard genes (low mean expression, high exclusiveness) lose more value per
    unit of performance — organisms that solved them "sell" at a premium.

    For organism *i*, gene *j*:

    $$
    \\Delta_{\\text{sell\\_hard}}(i, j) =
        -\\frac{x_{ij}}{N}
        \\cdot \\frac{\\text{excl}_j}{1 - \\text{excl}_j + \\varepsilon}
    $$

    Summed over all organisms this equals
    ``-mean_j · excl_j / (1 - excl_j)``, the proportional sell loss
    weighted by gene difficulty.

    Pair with `BuyHardOrgStrategy` for the full hard-gene trading round.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        return "SellHard"

    def __call__(self, ctx: StrategyContext) -> float:
        X = ctx.population.matrix
        N = ctx.population.N
        mean_j = X[:, ctx.gene_id].mean()
        excl_j = 1.0 - mean_j
        sell_signal_j = excl_j / (1.0 - excl_j + 1e-8)
        return -(1.0 / N) * X[ctx.org_id, ctx.gene_id] * sell_signal_j

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Linear d-vector: ``d[j] = -mean_j · excl_j / (1 - excl_j + eps)``."""
        mean_all = population.matrix.mean(axis=0)
        excl = 1.0 - mean_all
        sell_signal = excl / (1.0 - excl + 1e-8)
        d = -mean_all * sell_signal
        return None, d

Methods:

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Linear d-vector: d[j] = -mean_j · excl_j / (1 - excl_j + eps).

Source code in pikaia/strategies/gs_strategies/sell_hard_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Linear d-vector: ``d[j] = -mean_j · excl_j / (1 - excl_j + eps)``."""
    mean_all = population.matrix.mean(axis=0)
    excl = 1.0 - mean_all
    sell_signal = excl / (1.0 - excl + 1e-8)
    d = -mean_all * sell_signal
    return None, d

pikaia.strategies.gs_strategies.sell_uniform_strategy.SellUniformGeneStrategy

Bases: GeneStrategy

Trading sell signal applied uniformly to all genes.

All genes lose value at the same rate regardless of difficulty — organisms "sell" their solved genes uniformly.

For organism i, gene j:

\[ \Delta_{\text{sell\_uniform}}(i, j) = -\frac{x_{ij}}{N} \]

Summed over all organisms this equals -mean_j, a uniform sell loss independent of gene difficulty.

Pair with BuyUniformOrgStrategy for the full uniform trading round.

Source code in pikaia/strategies/gs_strategies/sell_uniform_strategy.py
class SellUniformGeneStrategy(GeneStrategy):
    """
    Trading sell signal applied uniformly to all genes.

    All genes lose value at the same rate regardless of difficulty — organisms
    "sell" their solved genes uniformly.

    For organism *i*, gene *j*:

    $$
    \\Delta_{\\text{sell\\_uniform}}(i, j) = -\\frac{x_{ij}}{N}
    $$

    Summed over all organisms this equals ``-mean_j``, a uniform sell loss
    independent of gene difficulty.

    Pair with `BuyUniformOrgStrategy` for the full uniform trading round.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        return "SellUniform"

    def __call__(self, ctx: StrategyContext) -> float:
        X = ctx.population.matrix
        N = X.shape[0]
        j = ctx.gene_id
        mean_j = X[:, j].mean()
        excl_j = 1.0 - mean_j
        # Skip genes with trivial exclusiveness: all-solved (excl≈0) or none-solved (excl≈1)
        if excl_j < 1e-6 or excl_j > 1.0 - 1e-6:
            return 0.0
        return float(-(1.0 / N) * X[ctx.org_id, j])

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Linear d-vector: ``d[j] = -mean_j`` for non-trivial genes only."""
        mean = population.matrix.mean(axis=0)
        excl = 1.0 - mean
        mask = (excl > 1e-6) & (excl < 1.0 - 1e-6)
        d = -mean * mask.astype(float)
        return None, d

Methods:

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Linear d-vector: d[j] = -mean_j for non-trivial genes only.

Source code in pikaia/strategies/gs_strategies/sell_uniform_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Linear d-vector: ``d[j] = -mean_j`` for non-trivial genes only."""
    mean = population.matrix.mean(axis=0)
    excl = 1.0 - mean
    mask = (excl > 1e-6) & (excl < 1.0 - 1e-6)
    d = -mean * mask.astype(float)
    return None, d

pikaia.strategies.gs_strategies.sell_easy_strategy.SellEasyGeneStrategy

Bases: GeneStrategy

Trading sell signal weighted by gene ease — the inverse of SellHardGeneStrategy.

Easy genes (high mean expression, low exclusiveness) lose more value.

For organism i, gene j:

\[ \Delta_{\text{sell\_easy}}(i, j) = +\frac{x_{ij}}{N} \cdot \frac{\text{excl}_j}{1 - \text{excl}_j + \varepsilon} \]

Summed over all organisms this equals +mean_j · excl_j / (1 - excl_j), the exact negation of the SellHardGeneStrategy signal.

Pair with BuyEasyOrgStrategy for the full easy-gene trading round.

Source code in pikaia/strategies/gs_strategies/sell_easy_strategy.py
class SellEasyGeneStrategy(GeneStrategy):
    """
    Trading sell signal weighted by gene ease — the inverse of `SellHardGeneStrategy`.

    Easy genes (high mean expression, low exclusiveness) lose more value.

    For organism *i*, gene *j*:

    $$
    \\Delta_{\\text{sell\\_easy}}(i, j) =
        +\\frac{x_{ij}}{N}
        \\cdot \\frac{\\text{excl}_j}{1 - \\text{excl}_j + \\varepsilon}
    $$

    Summed over all organisms this equals
    ``+mean_j · excl_j / (1 - excl_j)``, the exact negation of the
    ``SellHardGeneStrategy`` signal.

    Pair with `BuyEasyOrgStrategy` for the full easy-gene trading round.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        return "SellEasy"

    def __call__(self, ctx: StrategyContext) -> float:
        X = ctx.population.matrix
        N = ctx.population.N
        mean_j = X[:, ctx.gene_id].mean()
        excl_j = 1.0 - mean_j
        sell_signal_j = excl_j / (1.0 - excl_j + 1e-8)
        return float((1.0 / N) * X[ctx.org_id, ctx.gene_id] * sell_signal_j)

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Linear d-vector: ``d[j] = +mean_j · excl_j / (1 - excl_j + eps)``."""
        mean_all = population.matrix.mean(axis=0)
        excl = 1.0 - mean_all
        sell_signal = excl / (1.0 - excl + 1e-8)
        d = mean_all * sell_signal
        return None, d

Methods:

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Linear d-vector: d[j] = +mean_j · excl_j / (1 - excl_j + eps).

Source code in pikaia/strategies/gs_strategies/sell_easy_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Linear d-vector: ``d[j] = +mean_j · excl_j / (1 - excl_j + eps)``."""
    mean_all = population.matrix.mean(axis=0)
    excl = 1.0 - mean_all
    sell_signal = excl / (1.0 - excl + 1e-8)
    d = mean_all * sell_signal
    return None, d

pikaia.strategies.gs_strategies.altruistic_strategy.AltruisticGeneStrategy

Bases: GeneStrategy

A gene strategy that promotes altruistic behavior.

This strategy models altruism where a gene's fitness is influenced by its interaction with other genes. The delta for a gene's fitness is calculated based on its similarity to other genes and their respective fitness values. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/gs_strategies/altruistic_strategy.py
class AltruisticGeneStrategy(GeneStrategy):
    """
    A gene strategy that promotes altruistic behavior.

    This strategy models altruism where a gene's fitness is influenced by its
    interaction with other genes. The delta for a gene's fitness is calculated
    based on its similarity to other genes and their respective fitness values.
    This implementation follows the logic from the original `alg.py`.


    """

    def __init__(self, **kwargs):
        """Initialise the Altruistic gene strategy.

        Args:
            **kwargs: Keyword options forwarded to `GeneStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Altruistic"

    def __call__(self, ctx: StrategyContext) -> float:
        """
        Computes the delta for an altruistic gene.

        The formula is derived from the replicator equation, considering the
        interactions between the current gene and all other genes in the organism.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
        """
        # Get all gene indices except the current gene
        indices = np.arange(ctx.population.M) != ctx.gene_id

        # Vectorized computation for all genes except self
        # 16 / N * similarity * fitness_self * (pop_self - 0.5) * fitness_others *
        # (pop_others - pop_self)
        return float(
            np.sum(
                # constant factor and normalization by population size
                (16 / ctx.population.N)
                # similarity to other genes
                * ctx.gene_similarity[ctx.gene_id, indices]
                # fitness of current gene
                * ctx.gene_fitness[ctx.gene_id]
                # pop value of current gene minus 0.5
                * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
                # fitness of other genes
                * ctx.gene_fitness[indices]
                # gene variant fitness of other genes
                # minus gene variant fitness of current gene
                * (
                    ctx.population[ctx.org_id, indices]
                    - ctx.population[ctx.org_id, ctx.gene_id]
                )
            )
            # normalization by number of genes
            / ctx.population.M
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Full ``(M, M)`` D matrix encoding cross-gene altruistic interactions.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Gene similarity matrix of shape ``(M, M)``.
            org_similarity: Unused.
            initial_org_fitness_range: Unused.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
            ``D[j, k] = (16/M) * gene_similarity[j, k]``
            ``* mean_i[(x_ij - 0.5) * (x_ik - x_ij)]``
            and the diagonal set to zero.
        """
        X = population.matrix  # (N, M)
        M = population.M
        X_centered = X - 0.5  # (N, M)
        # X_diff[i, j, k] = X[i,k] - X[i,j]
        X_diff = X[:, np.newaxis, :] - X[:, :, np.newaxis]  # (N, M, M)
        kernel = np.mean(X_centered[:, :, np.newaxis] * X_diff, axis=0)  # (M, M)
        D = (16.0 / M) * gene_similarity * kernel
        np.fill_diagonal(D, 0.0)
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Computes the delta for an altruistic gene.

The formula is derived from the replicator equation, considering the interactions between the current gene and all other genes in the organism.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Name Type Description
float float

The computed delta value Delta_G(i,j) for the specified gene and organism.

Source code in pikaia/strategies/gs_strategies/altruistic_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """
    Computes the delta for an altruistic gene.

    The formula is derived from the replicator equation, considering the
    interactions between the current gene and all other genes in the organism.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
    """
    # Get all gene indices except the current gene
    indices = np.arange(ctx.population.M) != ctx.gene_id

    # Vectorized computation for all genes except self
    # 16 / N * similarity * fitness_self * (pop_self - 0.5) * fitness_others *
    # (pop_others - pop_self)
    return float(
        np.sum(
            # constant factor and normalization by population size
            (16 / ctx.population.N)
            # similarity to other genes
            * ctx.gene_similarity[ctx.gene_id, indices]
            # fitness of current gene
            * ctx.gene_fitness[ctx.gene_id]
            # pop value of current gene minus 0.5
            * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
            # fitness of other genes
            * ctx.gene_fitness[indices]
            # gene variant fitness of other genes
            # minus gene variant fitness of current gene
            * (
                ctx.population[ctx.org_id, indices]
                - ctx.population[ctx.org_id, ctx.gene_id]
            )
        )
        # normalization by number of genes
        / ctx.population.M
    )

__init__(**kwargs)

Initialise the Altruistic gene strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to GeneStrategy and stored in self.options.

{}
Source code in pikaia/strategies/gs_strategies/altruistic_strategy.py
def __init__(self, **kwargs):
    """Initialise the Altruistic gene strategy.

    Args:
        **kwargs: Keyword options forwarded to `GeneStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Full (M, M) D matrix encoding cross-gene altruistic interactions.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Gene similarity matrix of shape (M, M).

required
org_similarity ndarray

Unused.

required
initial_org_fitness_range float

Unused.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is an (M, M) matrix with

ndarray | None

D[j, k] = (16/M) * gene_similarity[j, k]

tuple[ndarray | None, ndarray | None]

* mean_i[(x_ij - 0.5) * (x_ik - x_ij)]

tuple[ndarray | None, ndarray | None]

and the diagonal set to zero.

Source code in pikaia/strategies/gs_strategies/altruistic_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Full ``(M, M)`` D matrix encoding cross-gene altruistic interactions.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Gene similarity matrix of shape ``(M, M)``.
        org_similarity: Unused.
        initial_org_fitness_range: Unused.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
        ``D[j, k] = (16/M) * gene_similarity[j, k]``
        ``* mean_i[(x_ij - 0.5) * (x_ik - x_ij)]``
        and the diagonal set to zero.
    """
    X = population.matrix  # (N, M)
    M = population.M
    X_centered = X - 0.5  # (N, M)
    # X_diff[i, j, k] = X[i,k] - X[i,j]
    X_diff = X[:, np.newaxis, :] - X[:, :, np.newaxis]  # (N, M, M)
    kernel = np.mean(X_centered[:, :, np.newaxis] * X_diff, axis=0)  # (M, M)
    D = (16.0 / M) * gene_similarity * kernel
    np.fill_diagonal(D, 0.0)
    return D, None

pikaia.strategies.gs_strategies.selfish_strategy.SelfishGeneStrategy

Bases: GeneStrategy

A gene strategy that promotes selfish behavior.

Warning

This strategy is experimental and its behavior may change in future versions.

This strategy models selfish behavior where a gene's fitness is increased at the expense of other, dissimilar genes within the same organism. The effect is proportional to the similarity, meaning it acts more selfishly against more similar genes. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/gs_strategies/selfish_strategy.py
class SelfishGeneStrategy(GeneStrategy):
    """
    A gene strategy that promotes selfish behavior.

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    This strategy models selfish behavior where a gene's fitness is increased
    at the expense of other, dissimilar genes within the same organism. The
    effect is proportional to the similarity, meaning it acts more selfishly
    against more similar genes. This implementation follows the logic from the
    original `alg.py`.


    """

    def __init__(self, **kwargs):
        """Initialise the Selfish gene strategy.

        Args:
            **kwargs: Keyword options forwarded to `GeneStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Selfish"

    def __call__(self, ctx: StrategyContext) -> float:
        """
        Computes the delta for a selfish gene.

        The formula calculates a negative delta contribution, effectively
        penalizing other genes to benefit the current one.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
        """
        # Get all gene indices except the current gene
        indices = np.arange(ctx.population.M) != ctx.gene_id

        # Vectorized computation for all genes except self
        # -16 / N * similarity * fitness_self * (pop_self - 0.5) * fitness_others *
        # (pop_others - pop_self)
        return float(
            np.sum(
                # constant factor and normalization by population size
                (-16 / ctx.population.N)
                # similarity to other genes
                * ctx.gene_similarity[ctx.gene_id, indices]
                # fitness of current gene
                * ctx.gene_fitness[ctx.gene_id]
                # pop value of current gene minus 0.5
                * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
                # fitness of other genes
                * ctx.gene_fitness[indices]
                # gene variant fitness of other genes
                # minus gene variant fitness of current gene
                * (
                    ctx.population[ctx.org_id, indices]
                    - ctx.population[ctx.org_id, ctx.gene_id]
                )
            )
            / ctx.population.M
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Full ``(M, M)`` D matrix as the negation of the altruistic kernel.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Gene similarity matrix of shape ``(M, M)``.
            org_similarity: Unused.
            initial_org_fitness_range: Unused.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D = -D_altruistic``; an ``(M, M)``
            matrix with the diagonal set to zero.
        """
        X = population.matrix  # (N, M)
        M = population.M
        X_centered = X - 0.5
        X_diff = X[:, np.newaxis, :] - X[:, :, np.newaxis]  # (N, M, M)
        kernel = np.mean(X_centered[:, :, np.newaxis] * X_diff, axis=0)
        D = -(16.0 / M) * gene_similarity * kernel
        np.fill_diagonal(D, 0.0)
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Computes the delta for a selfish gene.

The formula calculates a negative delta contribution, effectively penalizing other genes to benefit the current one.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Name Type Description
float float

The computed delta value Delta_G(i,j) for the specified gene and organism.

Source code in pikaia/strategies/gs_strategies/selfish_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """
    Computes the delta for a selfish gene.

    The formula calculates a negative delta contribution, effectively
    penalizing other genes to benefit the current one.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
    """
    # Get all gene indices except the current gene
    indices = np.arange(ctx.population.M) != ctx.gene_id

    # Vectorized computation for all genes except self
    # -16 / N * similarity * fitness_self * (pop_self - 0.5) * fitness_others *
    # (pop_others - pop_self)
    return float(
        np.sum(
            # constant factor and normalization by population size
            (-16 / ctx.population.N)
            # similarity to other genes
            * ctx.gene_similarity[ctx.gene_id, indices]
            # fitness of current gene
            * ctx.gene_fitness[ctx.gene_id]
            # pop value of current gene minus 0.5
            * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
            # fitness of other genes
            * ctx.gene_fitness[indices]
            # gene variant fitness of other genes
            # minus gene variant fitness of current gene
            * (
                ctx.population[ctx.org_id, indices]
                - ctx.population[ctx.org_id, ctx.gene_id]
            )
        )
        / ctx.population.M
    )

__init__(**kwargs)

Initialise the Selfish gene strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to GeneStrategy and stored in self.options.

{}
Source code in pikaia/strategies/gs_strategies/selfish_strategy.py
def __init__(self, **kwargs):
    """Initialise the Selfish gene strategy.

    Args:
        **kwargs: Keyword options forwarded to `GeneStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Full (M, M) D matrix as the negation of the altruistic kernel.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Gene similarity matrix of shape (M, M).

required
org_similarity ndarray

Unused.

required
initial_org_fitness_range float

Unused.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D = -D_altruistic; an (M, M)

ndarray | None

matrix with the diagonal set to zero.

Source code in pikaia/strategies/gs_strategies/selfish_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Full ``(M, M)`` D matrix as the negation of the altruistic kernel.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Gene similarity matrix of shape ``(M, M)``.
        org_similarity: Unused.
        initial_org_fitness_range: Unused.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D = -D_altruistic``; an ``(M, M)``
        matrix with the diagonal set to zero.
    """
    X = population.matrix  # (N, M)
    M = population.M
    X_centered = X - 0.5
    X_diff = X[:, np.newaxis, :] - X[:, :, np.newaxis]  # (N, M, M)
    kernel = np.mean(X_centered[:, :, np.newaxis] * X_diff, axis=0)
    D = -(16.0 / M) * gene_similarity * kernel
    np.fill_diagonal(D, 0.0)
    return D, None

pikaia.strategies.gs_strategies.kin_altruistic_strategy.KinAltruisticGeneStrategy

Bases: GeneStrategy

A gene strategy that promotes altruism towards kin (similar genes).

Warning

This strategy is experimental and its behavior may change in future versions.

This strategy increases a gene's fitness by helping other, similar genes, even at a potential cost to itself. The altruistic effect is inversely proportional to the similarity, meaning it helps less similar genes more. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/gs_strategies/kin_altruistic_strategy.py
class KinAltruisticGeneStrategy(GeneStrategy):
    """
    A gene strategy that promotes altruism towards kin (similar genes).

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    This strategy increases a gene's fitness by helping other, similar genes,
    even at a potential cost to itself. The altruistic effect is inversely
    proportional to the similarity, meaning it helps less similar genes more.
    This implementation follows the logic from the original `alg.py`.
    """

    def __init__(self, **kwargs):
        """Initialise the KinAltruistic gene strategy.

        Keyword Args:
            kin_range (int): Number of most-similar genes to consider as kin
                when computing the interaction term.  Defaults to ``M``
                (the full feature dimension).
            **kwargs: Additional options forwarded to `GeneStrategy`
                and stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "KinAltruistic"

    def __call__(self, ctx: StrategyContext) -> float:
        """
        Computes the delta for a kin-altruistic gene.

        The formula considers the interaction with other genes, weighted by a
        factor of `(0.5 - similarity)`.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
        """
        # Determine kin range
        kin_range = self.options.get("kin_range", ctx.population.M)

        # Get indices of most similar genes, excluding self
        indices = np.argsort(-ctx.gene_similarity[ctx.gene_id, :])
        indices = indices[:kin_range]
        indices = indices[indices != ctx.gene_id]

        # Early exit if no kin
        if len(indices) == 0:
            return 0.0

        # Vectorized computation for kin genes
        # 16 / N * (0.5 - similarity) * fitness_self * (pop_self - 0.5) *
        # fitness_others * (pop_others - pop_self)
        return float(
            np.sum(
                # constant factor and normalization by population size
                (16 / ctx.population.N)
                # kin altruism weight: 0.5 - similarity to other genes
                * (0.5 - ctx.gene_similarity[ctx.gene_id, indices])
                # fitness of current gene
                * ctx.gene_fitness[ctx.gene_id]
                # pop value of current gene minus 0.5
                * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
                # fitness of other genes
                * ctx.gene_fitness[indices]
                # gene variant fitness of other genes
                # minus gene variant fitness of current gene
                * (
                    ctx.population[ctx.org_id, indices]
                    - ctx.population[ctx.org_id, ctx.gene_id]
                )
            )
            # normalization by number of genes
            / ctx.population.M
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Full ``(M, M)`` D matrix with kin-weighted similarity.

        Respects the ``kin_range`` option: per gene *j*, only the top
        ``kin_range`` most similar genes *k* contribute (self excluded).

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Gene similarity matrix of shape ``(M, M)``.
            org_similarity: Unused.
            initial_org_fitness_range: Unused.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
            ``D[j, k] = (16/M) * (0.5 - gene_sim_masked[j, k])``
            ``* mean_i[(x_ij - 0.5) * (x_ik - x_ij)]``
            and the diagonal set to zero.
        """
        X = population.matrix  # (N, M)
        M = population.M
        kin_range = self.options.get("kin_range", M)

        # Build masked similarity: only top kin_range similar genes per row
        gene_sim_masked = np.zeros_like(gene_similarity)
        for j in range(M):
            sorted_k = np.argsort(-gene_similarity[j, :])
            top_k = sorted_k[:kin_range]
            top_k = top_k[top_k != j]  # exclude self
            gene_sim_masked[j, top_k] = gene_similarity[j, top_k]

        X_centered = X - 0.5  # (N, M)
        X_diff = X[:, np.newaxis, :] - X[:, :, np.newaxis]  # (N, M, M)
        inner = np.mean(X_centered[:, :, np.newaxis] * X_diff, axis=0)  # (M, M)
        D = (16.0 / M) * (0.5 - gene_sim_masked) * inner
        np.fill_diagonal(D, 0.0)
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Computes the delta for a kin-altruistic gene.

The formula considers the interaction with other genes, weighted by a factor of (0.5 - similarity).

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Name Type Description
float float

The computed delta value Delta_G(i,j) for the specified gene and organism.

Source code in pikaia/strategies/gs_strategies/kin_altruistic_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """
    Computes the delta for a kin-altruistic gene.

    The formula considers the interaction with other genes, weighted by a
    factor of `(0.5 - similarity)`.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
    """
    # Determine kin range
    kin_range = self.options.get("kin_range", ctx.population.M)

    # Get indices of most similar genes, excluding self
    indices = np.argsort(-ctx.gene_similarity[ctx.gene_id, :])
    indices = indices[:kin_range]
    indices = indices[indices != ctx.gene_id]

    # Early exit if no kin
    if len(indices) == 0:
        return 0.0

    # Vectorized computation for kin genes
    # 16 / N * (0.5 - similarity) * fitness_self * (pop_self - 0.5) *
    # fitness_others * (pop_others - pop_self)
    return float(
        np.sum(
            # constant factor and normalization by population size
            (16 / ctx.population.N)
            # kin altruism weight: 0.5 - similarity to other genes
            * (0.5 - ctx.gene_similarity[ctx.gene_id, indices])
            # fitness of current gene
            * ctx.gene_fitness[ctx.gene_id]
            # pop value of current gene minus 0.5
            * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
            # fitness of other genes
            * ctx.gene_fitness[indices]
            # gene variant fitness of other genes
            # minus gene variant fitness of current gene
            * (
                ctx.population[ctx.org_id, indices]
                - ctx.population[ctx.org_id, ctx.gene_id]
            )
        )
        # normalization by number of genes
        / ctx.population.M
    )

__init__(**kwargs)

Initialise the KinAltruistic gene strategy.

Other Parameters:

Name Type Description
kin_range int

Number of most-similar genes to consider as kin when computing the interaction term. Defaults to M (the full feature dimension).

**kwargs

Additional options forwarded to GeneStrategy and stored in self.options.

Source code in pikaia/strategies/gs_strategies/kin_altruistic_strategy.py
def __init__(self, **kwargs):
    """Initialise the KinAltruistic gene strategy.

    Keyword Args:
        kin_range (int): Number of most-similar genes to consider as kin
            when computing the interaction term.  Defaults to ``M``
            (the full feature dimension).
        **kwargs: Additional options forwarded to `GeneStrategy`
            and stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Full (M, M) D matrix with kin-weighted similarity.

Respects the kin_range option: per gene j, only the top kin_range most similar genes k contribute (self excluded).

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Gene similarity matrix of shape (M, M).

required
org_similarity ndarray

Unused.

required
initial_org_fitness_range float

Unused.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is an (M, M) matrix with

ndarray | None

D[j, k] = (16/M) * (0.5 - gene_sim_masked[j, k])

tuple[ndarray | None, ndarray | None]

* mean_i[(x_ij - 0.5) * (x_ik - x_ij)]

tuple[ndarray | None, ndarray | None]

and the diagonal set to zero.

Source code in pikaia/strategies/gs_strategies/kin_altruistic_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Full ``(M, M)`` D matrix with kin-weighted similarity.

    Respects the ``kin_range`` option: per gene *j*, only the top
    ``kin_range`` most similar genes *k* contribute (self excluded).

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Gene similarity matrix of shape ``(M, M)``.
        org_similarity: Unused.
        initial_org_fitness_range: Unused.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
        ``D[j, k] = (16/M) * (0.5 - gene_sim_masked[j, k])``
        ``* mean_i[(x_ij - 0.5) * (x_ik - x_ij)]``
        and the diagonal set to zero.
    """
    X = population.matrix  # (N, M)
    M = population.M
    kin_range = self.options.get("kin_range", M)

    # Build masked similarity: only top kin_range similar genes per row
    gene_sim_masked = np.zeros_like(gene_similarity)
    for j in range(M):
        sorted_k = np.argsort(-gene_similarity[j, :])
        top_k = sorted_k[:kin_range]
        top_k = top_k[top_k != j]  # exclude self
        gene_sim_masked[j, top_k] = gene_similarity[j, top_k]

    X_centered = X - 0.5  # (N, M)
    X_diff = X[:, np.newaxis, :] - X[:, :, np.newaxis]  # (N, M, M)
    inner = np.mean(X_centered[:, :, np.newaxis] * X_diff, axis=0)  # (M, M)
    D = (16.0 / M) * (0.5 - gene_sim_masked) * inner
    np.fill_diagonal(D, 0.0)
    return D, None

pikaia.strategies.gs_strategies.variance_strategy.VarianceGeneStrategy

Bases: GeneStrategy

A gene strategy that rewards features with high cross-organism dispersion.

Scales a Dominant-style expression signal by the column's normalised standard deviation so that genes which separate organisms more strongly receive larger fitness deltas. Near-constant columns contribute ~0.

Source code in pikaia/strategies/gs_strategies/variance_strategy.py
class VarianceGeneStrategy(GeneStrategy):
    """
    A gene strategy that rewards features with high cross-organism dispersion.

    Scales a Dominant-style expression signal by the column's normalised
    standard deviation so that genes which separate organisms more strongly
    receive larger fitness deltas. Near-constant columns contribute ~0.
    """

    def __init__(self, **kwargs):
        """Initialise the Variance gene strategy.

        Args:
            **kwargs: Keyword options forwarded to `GeneStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Variance"

    def __call__(self, ctx: StrategyContext) -> float:
        """
        Computes the delta for a variance-weighted gene.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
        """
        s_hat = _normalized_std(ctx.population.matrix)
        return float(
            (4 / ctx.population.N)
            * ctx.gene_fitness[ctx.gene_id] ** 2
            * s_hat[ctx.gene_id]
            * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Diagonal D matrix scaled by normalised column std.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Unused.
            org_similarity: Unused.
            initial_org_fitness_range: Unused.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix
            with ``D[j, j] = 4 * s_hat_j * (x_bar_j - 0.5)``.
        """
        s_hat = _normalized_std(population.matrix)
        D = np.diag(4.0 * s_hat * (population.matrix.mean(axis=0) - 0.5))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Computes the delta for a variance-weighted gene.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Name Type Description
float float

The computed delta value Delta_G(i,j) for the specified gene and organism.

Source code in pikaia/strategies/gs_strategies/variance_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """
    Computes the delta for a variance-weighted gene.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        float: The computed delta value `Delta_G(i,j)` for the specified gene and organism.
    """
    s_hat = _normalized_std(ctx.population.matrix)
    return float(
        (4 / ctx.population.N)
        * ctx.gene_fitness[ctx.gene_id] ** 2
        * s_hat[ctx.gene_id]
        * (ctx.population[ctx.org_id, ctx.gene_id] - 0.5)
    )

__init__(**kwargs)

Initialise the Variance gene strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to GeneStrategy and stored in self.options.

{}
Source code in pikaia/strategies/gs_strategies/variance_strategy.py
def __init__(self, **kwargs):
    """Initialise the Variance gene strategy.

    Args:
        **kwargs: Keyword options forwarded to `GeneStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Diagonal D matrix scaled by normalised column std.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Unused.

required
org_similarity ndarray

Unused.

required
initial_org_fitness_range float

Unused.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is a diagonal (M, M) matrix

ndarray | None

with D[j, j] = 4 * s_hat_j * (x_bar_j - 0.5).

Source code in pikaia/strategies/gs_strategies/variance_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Diagonal D matrix scaled by normalised column std.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Unused.
        org_similarity: Unused.
        initial_org_fitness_range: Unused.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix
        with ``D[j, j] = 4 * s_hat_j * (x_bar_j - 0.5)``.
    """
    s_hat = _normalized_std(population.matrix)
    D = np.diag(4.0 * s_hat * (population.matrix.mean(axis=0) - 0.5))
    return D, None

pikaia.strategies.gs_strategies.entropy_max_strategy.EntropyMaxGeneStrategy

Bases: GeneStrategy

A supervised gene strategy driven by information-theoretic relevance.

Warning

This strategy is experimental and its behavior may change in future versions.

Scores each feature as the product of its normalised mutual information with the target y and its normalised differential entropy (a proxy for feature variance). Both components are independently normalised to [0, 1] before multiplication, so the combined score rewards features that are simultaneously informative and spread out.

The replicator delta is::

delta[j] = gf[j] * (4 / N) * (info_score[j] - 0.5)

where info_score[j] ∈ [0, 1] is the product described above.

When y is not supplied the strategy falls back to entropy-only scores (mutual information term is zero), making all info_score[j] = 0 and the delta uniformly negative — equivalent to a mild anti-high-variance penalty. For the strategy to be useful, always pass y.

The per-feature scores are computed once on the first call and cached. To supply labels when using the kernel path, pass y to kernel().

Parameters:

Name Type Description Default
n_bins int

Number of bins used when discretising continuous features for mutual information estimation. Default 10.

10
precomputed_info ndarray | None

Pre-computed info scores of shape (n_features,). If provided, skips the MI computation entirely.

None
**kwargs

Forwarded to GeneStrategy.

{}
Source code in pikaia/strategies/gs_strategies/entropy_max_strategy.py
class EntropyMaxGeneStrategy(GeneStrategy):
    """
    A supervised gene strategy driven by information-theoretic relevance.

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    Scores each feature as the product of its normalised mutual information
    with the target ``y`` and its normalised differential entropy (a proxy for
    feature variance).  Both components are independently normalised to
    ``[0, 1]`` before multiplication, so the combined score rewards features
    that are simultaneously informative *and* spread out.

    The replicator delta is::

        delta[j] = gf[j] * (4 / N) * (info_score[j] - 0.5)

    where ``info_score[j] ∈ [0, 1]`` is the product described above.

    When ``y`` is not supplied the strategy falls back to entropy-only scores
    (mutual information term is zero), making all ``info_score[j] = 0`` and
    the delta uniformly negative — equivalent to a mild anti-high-variance
    penalty.  For the strategy to be useful, always pass ``y``.

    The per-feature scores are computed once on the first call and cached.  To
    supply labels when using the kernel path, pass ``y`` to ``kernel()``.

    Args:
        n_bins: Number of bins used when discretising continuous features for
            mutual information estimation.  Default ``10``.
        precomputed_info: Pre-computed info scores of shape ``(n_features,)``.
            If provided, skips the MI computation entirely.
        **kwargs: Forwarded to `GeneStrategy`.
    """

    def __init__(
        self, n_bins: int = 10, precomputed_info: np.ndarray | None = None, **kwargs
    ):
        super().__init__(**kwargs)
        self.n_bins = n_bins
        self._info_scores: np.ndarray | None = precomputed_info
        self._mode: str | None = None if precomputed_info is None else "precomputed"

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "EntropyMax"

    @staticmethod
    def _encode_target(y: np.ndarray) -> np.ndarray:
        """Encode a target array to integer labels for mutual information estimation.

        Args:
            y: Target array of any dtype.

        Returns:
            1D array of integer-encoded labels.
        """
        y = np.asarray(y).flatten()
        if not np.issubdtype(y.dtype, np.number):
            unique_vals = np.unique(y)
            return np.searchsorted(unique_vals, y).astype(int)
        return y

    @staticmethod
    def compute_info_scores(
        X: np.ndarray, y: np.ndarray | None = None, n_bins: int = 10
    ) -> np.ndarray:
        """Compute per-feature information scores as MI × entropy (both normalised).

        Args:
            X: Data matrix of shape ``(n_samples, n_features)``, values in ``[0, 1]``.
            y: Target array of shape ``(n_samples,)``.  Required for useful scores.
            n_bins: Number of histogram bins for MI estimation.

        Returns:
            Array of shape ``(n_features,)`` with values in ``[0, 1]``.
        """
        from sklearn.metrics import mutual_info_score

        n_features = X.shape[1]

        # Mutual information component
        if y is not None:
            y_enc = EntropyMaxGeneStrategy._encode_target(y)
            mi_scores = np.zeros(n_features)
            for j in range(n_features):
                try:
                    x_j = X[:, j]
                    bins = np.histogram(x_j, bins=n_bins)[1][:-1]
                    x_bin = np.clip(np.digitize(x_j, bins=bins), 1, n_bins)
                    mi_scores[j] = mutual_info_score(y_enc, x_bin)
                except Exception:
                    mi_scores[j] = 0.0
        else:
            mi_scores = np.zeros(n_features)

        # Differential entropy proxy: 0.5 * log(2πe * var)
        variances = np.maximum(np.var(X, axis=0), 1e-12)
        entropy_scores = 0.5 * np.log(2 * np.pi * np.e * variances)
        e_min, e_max = entropy_scores.min(), entropy_scores.max()
        entropy_norm = (
            (entropy_scores - e_min) / (e_max - e_min)
            if e_max > e_min
            else np.full(n_features, 0.5)
        )

        # Normalise MI and combine
        mi_max = mi_scores.max()
        mi_norm = mi_scores / mi_max if mi_max > 0 else np.zeros(n_features)

        return np.clip(mi_norm * entropy_norm, 0.0, 1.0)

    def _get_scores(self, X: np.ndarray, y: np.ndarray | None) -> np.ndarray:
        """Return cached info scores, recomputing if the mode changes.

        Args:
            X: Data matrix of shape ``(n_samples, n_features)``.
            y: Optional target array; triggers supervised mode when provided.

        Returns:
            Per-feature information scores of shape ``(n_features,)``.
        """
        mode = "supervised" if y is not None else "unsupervised"
        if self._info_scores is None or (
            self._mode != "precomputed" and self._mode != mode
        ):
            self._mode = mode
            self._info_scores = self.compute_info_scores(X, y, self.n_bins)
        return self._info_scores

    def __call__(self, ctx: StrategyContext) -> float:
        """Compute delta for the EntropyMax gene strategy.

        Args:
            ctx: Strategy context.  ``ctx.y`` is used when available.

        Returns:
            float: The computed delta ``Delta_G(i,j)``.
        """
        scores = self._get_scores(ctx.population.matrix, ctx.y)
        return float(
            (4 / ctx.population.N)
            * ctx.gene_fitness[ctx.gene_id]
            * (scores[ctx.gene_id] - 0.5)
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Diagonal D: ``D[j,j] = 4 * (info_score[j] - 0.5)``.

        Args:
            population: Current population.
            gene_similarity: Gene-similarity matrix (unused).
            org_similarity: Organism-similarity matrix (unused).
            initial_org_fitness_range: Initial fitness range (unused).
            y: Target labels.  Pass these to enable MI computation.

        Returns:
            ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
        """
        scores = self._get_scores(population.matrix, y)
        D = np.diag(4.0 * (scores - 0.5))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Compute delta for the EntropyMax gene strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Strategy context. ctx.y is used when available.

required

Returns:

Name Type Description
float float

The computed delta Delta_G(i,j).

Source code in pikaia/strategies/gs_strategies/entropy_max_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """Compute delta for the EntropyMax gene strategy.

    Args:
        ctx: Strategy context.  ``ctx.y`` is used when available.

    Returns:
        float: The computed delta ``Delta_G(i,j)``.
    """
    scores = self._get_scores(ctx.population.matrix, ctx.y)
    return float(
        (4 / ctx.population.N)
        * ctx.gene_fitness[ctx.gene_id]
        * (scores[ctx.gene_id] - 0.5)
    )

compute_info_scores(X: np.ndarray, y: np.ndarray | None = None, n_bins: int = 10) -> np.ndarray staticmethod

Compute per-feature information scores as MI × entropy (both normalised).

Parameters:

Name Type Description Default
X ndarray

Data matrix of shape (n_samples, n_features), values in [0, 1].

required
y ndarray | None

Target array of shape (n_samples,). Required for useful scores.

None
n_bins int

Number of histogram bins for MI estimation.

10

Returns:

Type Description
ndarray

Array of shape (n_features,) with values in [0, 1].

Source code in pikaia/strategies/gs_strategies/entropy_max_strategy.py
@staticmethod
def compute_info_scores(
    X: np.ndarray, y: np.ndarray | None = None, n_bins: int = 10
) -> np.ndarray:
    """Compute per-feature information scores as MI × entropy (both normalised).

    Args:
        X: Data matrix of shape ``(n_samples, n_features)``, values in ``[0, 1]``.
        y: Target array of shape ``(n_samples,)``.  Required for useful scores.
        n_bins: Number of histogram bins for MI estimation.

    Returns:
        Array of shape ``(n_features,)`` with values in ``[0, 1]``.
    """
    from sklearn.metrics import mutual_info_score

    n_features = X.shape[1]

    # Mutual information component
    if y is not None:
        y_enc = EntropyMaxGeneStrategy._encode_target(y)
        mi_scores = np.zeros(n_features)
        for j in range(n_features):
            try:
                x_j = X[:, j]
                bins = np.histogram(x_j, bins=n_bins)[1][:-1]
                x_bin = np.clip(np.digitize(x_j, bins=bins), 1, n_bins)
                mi_scores[j] = mutual_info_score(y_enc, x_bin)
            except Exception:
                mi_scores[j] = 0.0
    else:
        mi_scores = np.zeros(n_features)

    # Differential entropy proxy: 0.5 * log(2πe * var)
    variances = np.maximum(np.var(X, axis=0), 1e-12)
    entropy_scores = 0.5 * np.log(2 * np.pi * np.e * variances)
    e_min, e_max = entropy_scores.min(), entropy_scores.max()
    entropy_norm = (
        (entropy_scores - e_min) / (e_max - e_min)
        if e_max > e_min
        else np.full(n_features, 0.5)
    )

    # Normalise MI and combine
    mi_max = mi_scores.max()
    mi_norm = mi_scores / mi_max if mi_max > 0 else np.zeros(n_features)

    return np.clip(mi_norm * entropy_norm, 0.0, 1.0)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Diagonal D: D[j,j] = 4 * (info_score[j] - 0.5).

Parameters:

Name Type Description Default
population PikaiaPopulation

Current population.

required
gene_similarity ndarray

Gene-similarity matrix (unused).

required
org_similarity ndarray

Organism-similarity matrix (unused).

required
initial_org_fitness_range float

Initial fitness range (unused).

required
y ndarray | None

Target labels. Pass these to enable MI computation.

None

Returns:

Type Description
tuple[ndarray | None, ndarray | None]

(D, None) where D is a diagonal (M, M) matrix.

Source code in pikaia/strategies/gs_strategies/entropy_max_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Diagonal D: ``D[j,j] = 4 * (info_score[j] - 0.5)``.

    Args:
        population: Current population.
        gene_similarity: Gene-similarity matrix (unused).
        org_similarity: Organism-similarity matrix (unused).
        initial_org_fitness_range: Initial fitness range (unused).
        y: Target labels.  Pass these to enable MI computation.

    Returns:
        ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
    """
    scores = self._get_scores(population.matrix, y)
    D = np.diag(4.0 * (scores - 0.5))
    return D, None

pikaia.strategies.gs_strategies.orthogonality_strategy.OrthoGeneStrategy

Bases: GeneStrategy

A gene strategy driven by feature orthogonality (low pairwise correlation).

Warning

This strategy is experimental and its behavior may change in future versions.

Rewards features that are minimally correlated with all other features. The orthogonality score for feature j is::

orthogonality[j] = 1 - mean(|corr(j, k)|) for k ≠ j

where correlations are computed on the MinMax-normalised data matrix. The score is in [0, 1]; a perfectly uncorrelated feature scores 1.

When the target y is provided, it is appended as an extra column before computing correlations (supervised mode). Because orthogonality is now measured against the augmented matrix, features that correlate strongly with y receive lower scores and are suppressed — the opposite of conventional supervised feature selection. This makes the strategy a novelty/diversity pressure: it promotes features that add information beyond what the target and the other features already capture. It is most useful when mixed with a target-aware strategy (e.g. DOMINANT or ENTROPY_MAX) that handles target relevance, leaving OrthoGene to enforce diversity.

The replicator delta is::

delta[j] = gf[j] * (4 / N) * (orthogonality[j] - 0.5)

Scores are computed once on the first call and cached; a mode change (supervised ↔ unsupervised) triggers a recomputation.

Parameters:

Name Type Description Default
**kwargs

Forwarded to GeneStrategy.

{}
Source code in pikaia/strategies/gs_strategies/orthogonality_strategy.py
class OrthoGeneStrategy(GeneStrategy):
    """
    A gene strategy driven by feature orthogonality (low pairwise correlation).

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    Rewards features that are minimally correlated with all other features.
    The orthogonality score for feature *j* is::

        orthogonality[j] = 1 - mean(|corr(j, k)|) for k ≠ j

    where correlations are computed on the MinMax-normalised data matrix.  The
    score is in ``[0, 1]``; a perfectly uncorrelated feature scores 1.

    When the target ``y`` is provided, it is appended as an extra column before
    computing correlations (supervised mode).  Because orthogonality is now
    measured against the augmented matrix, features that correlate strongly
    with ``y`` receive *lower* scores and are suppressed — the opposite of
    conventional supervised feature selection.  This makes the strategy a
    **novelty/diversity pressure**: it promotes features that add information
    beyond what the target and the other features already capture.  It is most
    useful when mixed with a target-aware strategy (e.g. ``DOMINANT`` or
    ``ENTROPY_MAX``) that handles target relevance, leaving OrthoGene to
    enforce diversity.

    The replicator delta is::

        delta[j] = gf[j] * (4 / N) * (orthogonality[j] - 0.5)

    Scores are computed once on the first call and cached; a mode change
    (supervised ↔ unsupervised) triggers a recomputation.

    Args:
        **kwargs: Forwarded to `GeneStrategy`.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._orthogonality: np.ndarray | None = None
        self._mode: str | None = None

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "OrthoGene"

    @staticmethod
    def _encode_target(y: np.ndarray) -> np.ndarray:
        """Encode a target array to binary ±1 labels centred at the median.

        Args:
            y: Target array of any dtype.

        Returns:
            1D float array with values ``+1.0`` (≥ median) or ``-1.0`` (< median).
        """
        y = np.asarray(y).flatten()
        if not np.issubdtype(y.dtype, np.number):
            unique_vals = np.unique(y)
            y_numeric = np.searchsorted(unique_vals, y).astype(float)
        else:
            y_numeric = y.astype(float)
        median = np.median(y_numeric)
        return np.where(y_numeric >= median, 1.0, -1.0)

    @staticmethod
    def _compute_orthogonality_from_matrix(X: np.ndarray) -> np.ndarray:
        """Return per-column orthogonality scores for matrix ``X``.

        Args:
            X: Matrix of shape ``(n_samples, n_cols)``.

        Returns:
            Array of shape ``(n_cols,)`` with values in ``[0, 1]``.
        """
        if X.shape[1] <= 1:
            return np.array([1.0])
        corr = np.corrcoef(X.T)
        corr = np.nan_to_num(corr, nan=0.0)
        n = corr.shape[0]
        # Sum of absolute off-diagonal correlations per column
        abs_corr_sum = np.sum(np.abs(corr), axis=0) - 1.0  # subtract self-correlation
        return 1.0 - abs_corr_sum / (n - 1)

    def _get_scores(self, X: np.ndarray, y: np.ndarray | None) -> np.ndarray:
        """Return cached orthogonality scores, recomputing if the mode changes.

        Args:
            X: Data matrix of shape ``(n_samples, n_features)``.
            y: Optional target array; triggers supervised mode when provided.

        Returns:
            Per-feature orthogonality scores of shape ``(n_features,)``.
        """
        mode = "supervised" if y is not None else "unsupervised"
        if self._orthogonality is None or self._mode != mode:
            self._mode = mode
            if y is not None:
                y_col = self._encode_target(y).reshape(-1, 1)
                X_aug = np.column_stack([X, y_col])
                # Slice back to n_features — y column was appended only for correlation
                self._orthogonality = self._compute_orthogonality_from_matrix(X_aug)[
                    : X.shape[1]
                ]
            else:
                self._orthogonality = self._compute_orthogonality_from_matrix(X)
        return self._orthogonality

    def __call__(self, ctx: StrategyContext) -> float:
        """Compute delta for the OrthoGene strategy.

        Args:
            ctx: Strategy context.  ``ctx.y`` is used when available.

        Returns:
            float: The computed delta ``Delta_G(i,j)``.
        """
        scores = self._get_scores(ctx.population.matrix, ctx.y)
        return float(
            (4 / ctx.population.N)
            * ctx.gene_fitness[ctx.gene_id]
            * (scores[ctx.gene_id] - 0.5)
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Diagonal D: ``D[j,j] = 4 * (orthogonality[j] - 0.5)``.

        Args:
            population: Current population.
            gene_similarity: Gene-similarity matrix (unused).
            org_similarity: Organism-similarity matrix (unused).
            initial_org_fitness_range: Initial fitness range (unused).
            y: Optional target labels for supervised mode.

        Returns:
            ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
        """
        scores = self._get_scores(population.matrix, y)
        D = np.diag(4.0 * (scores - 0.5))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Compute delta for the OrthoGene strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Strategy context. ctx.y is used when available.

required

Returns:

Name Type Description
float float

The computed delta Delta_G(i,j).

Source code in pikaia/strategies/gs_strategies/orthogonality_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """Compute delta for the OrthoGene strategy.

    Args:
        ctx: Strategy context.  ``ctx.y`` is used when available.

    Returns:
        float: The computed delta ``Delta_G(i,j)``.
    """
    scores = self._get_scores(ctx.population.matrix, ctx.y)
    return float(
        (4 / ctx.population.N)
        * ctx.gene_fitness[ctx.gene_id]
        * (scores[ctx.gene_id] - 0.5)
    )

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Diagonal D: D[j,j] = 4 * (orthogonality[j] - 0.5).

Parameters:

Name Type Description Default
population PikaiaPopulation

Current population.

required
gene_similarity ndarray

Gene-similarity matrix (unused).

required
org_similarity ndarray

Organism-similarity matrix (unused).

required
initial_org_fitness_range float

Initial fitness range (unused).

required
y ndarray | None

Optional target labels for supervised mode.

None

Returns:

Type Description
tuple[ndarray | None, ndarray | None]

(D, None) where D is a diagonal (M, M) matrix.

Source code in pikaia/strategies/gs_strategies/orthogonality_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Diagonal D: ``D[j,j] = 4 * (orthogonality[j] - 0.5)``.

    Args:
        population: Current population.
        gene_similarity: Gene-similarity matrix (unused).
        org_similarity: Organism-similarity matrix (unused).
        initial_org_fitness_range: Initial fitness range (unused).
        y: Optional target labels for supervised mode.

    Returns:
        ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
    """
    scores = self._get_scores(population.matrix, y)
    D = np.diag(4.0 * (scores - 0.5))
    return D, None

pikaia.strategies.gs_strategies.partial_corr_strategy.PartialCorrGeneStrategy

Bases: GeneStrategy

A gene strategy driven by partial correlation with the target.

Warning

This strategy is experimental and its behavior may change in future versions.

Rewards features whose relationship with the target survives controlling for all other features. The partial correlation of feature j with the target is estimated via shrinkage precision matrices to handle multicollinearity::

pc[j] = |P[j, target]| / sqrt(|P[j,j]| * |P[target,target]|)

where P is the shrinkage precision matrix of [X | y]. Scores are clipped to [0, 1].

The replicator delta is::

delta[j] = gf[j] * (4 / N) * (pc[j] - 0.5)

Without a target (unsupervised mode), all partial correlations are set to zero, making every delta negative. For meaningful results, always pass y.

Pre-computed partial correlations can be supplied via precomputed_pc to skip the expensive matrix inversion. The scores are cached on first use.

Parameters:

Name Type Description Default
precomputed_pc ndarray | None

Pre-computed partial correlations of shape (n_features,). If provided, no computation is done.

None
n_bins int

Unused; kept for API compatibility.

10
**kwargs

Forwarded to GeneStrategy.

{}
Source code in pikaia/strategies/gs_strategies/partial_corr_strategy.py
class PartialCorrGeneStrategy(GeneStrategy):
    """
    A gene strategy driven by partial correlation with the target.

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    Rewards features whose relationship with the target survives controlling
    for all other features.  The partial correlation of feature *j* with the
    target is estimated via shrinkage precision matrices to handle
    multicollinearity::

        pc[j] = |P[j, target]| / sqrt(|P[j,j]| * |P[target,target]|)

    where ``P`` is the shrinkage precision matrix of ``[X | y]``.  Scores are
    clipped to ``[0, 1]``.

    The replicator delta is::

        delta[j] = gf[j] * (4 / N) * (pc[j] - 0.5)

    Without a target (unsupervised mode), all partial correlations are set to
    zero, making every delta negative.  For meaningful results, always pass
    ``y``.

    Pre-computed partial correlations can be supplied via ``precomputed_pc``
    to skip the expensive matrix inversion.  The scores are cached on first
    use.

    Args:
        precomputed_pc: Pre-computed partial correlations of shape
            ``(n_features,)``.  If provided, no computation is done.
        n_bins: Unused; kept for API compatibility.
        **kwargs: Forwarded to `GeneStrategy`.
    """

    def __init__(
        self, precomputed_pc: np.ndarray | None = None, n_bins: int = 10, **kwargs
    ):
        super().__init__(**kwargs)
        self._partial_corrs: np.ndarray | None = precomputed_pc
        self._n_bins = n_bins
        self._mode: str | None = None if precomputed_pc is None else "precomputed"

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "PartialCorr"

    @staticmethod
    def _encode_target(y: np.ndarray) -> np.ndarray:
        """Encode a target array to binary ±1 labels centred at the median.

        Args:
            y: Target array of any dtype.

        Returns:
            1D float array with values ``+1.0`` (≥ median) or ``-1.0`` (< median).
        """
        y = np.asarray(y).flatten()
        if not np.issubdtype(y.dtype, np.number):
            unique_vals = np.unique(y)
            y_numeric = np.searchsorted(unique_vals, y).astype(float)
        else:
            y_numeric = y.astype(float)
        median = np.median(y_numeric)
        return np.where(y_numeric >= median, 1.0, -1.0)

    @staticmethod
    def compute_partial_correlations(
        X: np.ndarray, y: np.ndarray | None = None, reg: float = 0.5
    ) -> np.ndarray:
        """Compute partial correlations between each feature and the target.

        Uses Ledoit-Wolf-style shrinkage towards a scaled identity to regularise
        the precision matrix.

        Args:
            X: Data matrix ``(n_samples, n_features)``, values in ``[0, 1]``.
            y: Target array ``(n_samples,)``.  If ``None``, returns zeros.
            reg: Shrinkage coefficient in ``[0, 1]``.  Higher values pull the
                precision matrix towards the diagonal.  Default ``0.5``.

        Returns:
            Array of shape ``(n_features,)`` with values in ``[0, 1]``.
        """
        n_features = X.shape[1]
        if y is None:
            return np.zeros(n_features)

        y_enc = PartialCorrGeneStrategy._encode_target(y)
        joint = np.column_stack([X, y_enc])
        cov = np.nan_to_num(np.cov(joint.T, ddof=0), nan=0.0)
        p = cov.shape[0]

        # Shrink towards scaled identity
        target_diag = (np.trace(cov) / p) * np.eye(p)
        cov_shrunk = (1.0 - reg) * cov + reg * target_diag + 1e-6 * np.eye(p)

        try:
            precision = np.linalg.inv(cov_shrunk)
        except np.linalg.LinAlgError:
            precision = np.linalg.pinv(cov_shrunk)
        precision = np.nan_to_num(precision, nan=0.0)

        last = p - 1  # index of the target column
        pc = np.zeros(n_features)
        for j in range(n_features):
            denom = np.sqrt(abs(precision[j, j]) * abs(precision[last, last]))
            pc[j] = abs(precision[j, last]) / denom if denom > 1e-12 else 0.0

        return np.clip(pc, 0.0, 1.0)

    def _get_scores(self, X: np.ndarray, y: np.ndarray | None) -> np.ndarray:
        """Return cached partial correlation scores, recomputing if the mode changes.

        Args:
            X: Data matrix of shape ``(n_samples, n_features)``.
            y: Optional target array; triggers supervised mode when provided.

        Returns:
            Per-feature partial correlation scores of shape ``(n_features,)``.
        """
        mode = "supervised" if y is not None else "unsupervised"
        if self._partial_corrs is None or (
            self._mode != "precomputed" and self._mode != mode
        ):
            self._mode = mode
            self._partial_corrs = self.compute_partial_correlations(X, y)
        return self._partial_corrs

    def __call__(self, ctx: StrategyContext) -> float:
        """Compute delta for the PartialCorr gene strategy.

        Args:
            ctx: Strategy context.  ``ctx.y`` is used when available.

        Returns:
            float: The computed delta ``Delta_G(i,j)``.
        """
        scores = self._get_scores(ctx.population.matrix, ctx.y)
        return float(
            (4 / ctx.population.N)
            * ctx.gene_fitness[ctx.gene_id]
            * (scores[ctx.gene_id] - 0.5)
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Diagonal D: ``D[j,j] = 4 * (pc[j] - 0.5)``.

        Args:
            population: Current population.
            gene_similarity: Gene-similarity matrix (unused).
            org_similarity: Organism-similarity matrix (unused).
            initial_org_fitness_range: Initial fitness range (unused).
            y: Target labels.  Pass these to enable partial correlation computation.

        Returns:
            ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
        """
        scores = self._get_scores(population.matrix, y)
        D = np.diag(4.0 * (scores - 0.5))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Compute delta for the PartialCorr gene strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Strategy context. ctx.y is used when available.

required

Returns:

Name Type Description
float float

The computed delta Delta_G(i,j).

Source code in pikaia/strategies/gs_strategies/partial_corr_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """Compute delta for the PartialCorr gene strategy.

    Args:
        ctx: Strategy context.  ``ctx.y`` is used when available.

    Returns:
        float: The computed delta ``Delta_G(i,j)``.
    """
    scores = self._get_scores(ctx.population.matrix, ctx.y)
    return float(
        (4 / ctx.population.N)
        * ctx.gene_fitness[ctx.gene_id]
        * (scores[ctx.gene_id] - 0.5)
    )

compute_partial_correlations(X: np.ndarray, y: np.ndarray | None = None, reg: float = 0.5) -> np.ndarray staticmethod

Compute partial correlations between each feature and the target.

Uses Ledoit-Wolf-style shrinkage towards a scaled identity to regularise the precision matrix.

Parameters:

Name Type Description Default
X ndarray

Data matrix (n_samples, n_features), values in [0, 1].

required
y ndarray | None

Target array (n_samples,). If None, returns zeros.

None
reg float

Shrinkage coefficient in [0, 1]. Higher values pull the precision matrix towards the diagonal. Default 0.5.

0.5

Returns:

Type Description
ndarray

Array of shape (n_features,) with values in [0, 1].

Source code in pikaia/strategies/gs_strategies/partial_corr_strategy.py
@staticmethod
def compute_partial_correlations(
    X: np.ndarray, y: np.ndarray | None = None, reg: float = 0.5
) -> np.ndarray:
    """Compute partial correlations between each feature and the target.

    Uses Ledoit-Wolf-style shrinkage towards a scaled identity to regularise
    the precision matrix.

    Args:
        X: Data matrix ``(n_samples, n_features)``, values in ``[0, 1]``.
        y: Target array ``(n_samples,)``.  If ``None``, returns zeros.
        reg: Shrinkage coefficient in ``[0, 1]``.  Higher values pull the
            precision matrix towards the diagonal.  Default ``0.5``.

    Returns:
        Array of shape ``(n_features,)`` with values in ``[0, 1]``.
    """
    n_features = X.shape[1]
    if y is None:
        return np.zeros(n_features)

    y_enc = PartialCorrGeneStrategy._encode_target(y)
    joint = np.column_stack([X, y_enc])
    cov = np.nan_to_num(np.cov(joint.T, ddof=0), nan=0.0)
    p = cov.shape[0]

    # Shrink towards scaled identity
    target_diag = (np.trace(cov) / p) * np.eye(p)
    cov_shrunk = (1.0 - reg) * cov + reg * target_diag + 1e-6 * np.eye(p)

    try:
        precision = np.linalg.inv(cov_shrunk)
    except np.linalg.LinAlgError:
        precision = np.linalg.pinv(cov_shrunk)
    precision = np.nan_to_num(precision, nan=0.0)

    last = p - 1  # index of the target column
    pc = np.zeros(n_features)
    for j in range(n_features):
        denom = np.sqrt(abs(precision[j, j]) * abs(precision[last, last]))
        pc[j] = abs(precision[j, last]) / denom if denom > 1e-12 else 0.0

    return np.clip(pc, 0.0, 1.0)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Diagonal D: D[j,j] = 4 * (pc[j] - 0.5).

Parameters:

Name Type Description Default
population PikaiaPopulation

Current population.

required
gene_similarity ndarray

Gene-similarity matrix (unused).

required
org_similarity ndarray

Organism-similarity matrix (unused).

required
initial_org_fitness_range float

Initial fitness range (unused).

required
y ndarray | None

Target labels. Pass these to enable partial correlation computation.

None

Returns:

Type Description
tuple[ndarray | None, ndarray | None]

(D, None) where D is a diagonal (M, M) matrix.

Source code in pikaia/strategies/gs_strategies/partial_corr_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Diagonal D: ``D[j,j] = 4 * (pc[j] - 0.5)``.

    Args:
        population: Current population.
        gene_similarity: Gene-similarity matrix (unused).
        org_similarity: Organism-similarity matrix (unused).
        initial_org_fitness_range: Initial fitness range (unused).
        y: Target labels.  Pass these to enable partial correlation computation.

    Returns:
        ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
    """
    scores = self._get_scores(population.matrix, y)
    D = np.diag(4.0 * (scores - 0.5))
    return D, None

pikaia.strategies.gs_strategies.redundancy_penalty_strategy.RedundancyPenaltyGeneStrategy

Bases: GeneStrategy

A gene strategy that penalises redundant (highly correlated) features.

Warning

This strategy is experimental and its behavior may change in future versions.

Computes a redundancy score for each feature as the mean absolute pairwise correlation with all other features::

redundancy[j] = mean(|corr(j, k)|) for k ≠ j

and promotes features with low redundancy::

delta[j] = gf[j] * (4 / N) * (0.5 - redundancy[j])

Features with redundancy below 0.5 receive a positive delta (promoted); highly correlated features receive a negative delta (suppressed).

In supervised mode (when y is provided), the target is appended as an extra column before computing the correlation matrix, biasing the strategy towards features that are both non-redundant among themselves and non-redundant relative to the target. The target column is excluded from the returned scores.

Scores are computed once on first use and cached; a mode change triggers a recomputation.

Parameters:

Name Type Description Default
precomputed_redundancy ndarray | None

Pre-computed redundancy scores of shape (n_features,). If provided, skips computation entirely.

None
**kwargs

Forwarded to GeneStrategy.

{}
Source code in pikaia/strategies/gs_strategies/redundancy_penalty_strategy.py
class RedundancyPenaltyGeneStrategy(GeneStrategy):
    """
    A gene strategy that penalises redundant (highly correlated) features.

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    Computes a redundancy score for each feature as the mean absolute pairwise
    correlation with all other features::

        redundancy[j] = mean(|corr(j, k)|) for k ≠ j

    and promotes features with *low* redundancy::

        delta[j] = gf[j] * (4 / N) * (0.5 - redundancy[j])

    Features with redundancy below 0.5 receive a positive delta (promoted);
    highly correlated features receive a negative delta (suppressed).

    In supervised mode (when ``y`` is provided), the target is appended as an
    extra column before computing the correlation matrix, biasing the strategy
    towards features that are both non-redundant *among themselves* and
    non-redundant relative to the target.  The target column is excluded from
    the returned scores.

    Scores are computed once on first use and cached; a mode change triggers
    a recomputation.

    Args:
        precomputed_redundancy: Pre-computed redundancy scores of shape
            ``(n_features,)``.  If provided, skips computation entirely.
        **kwargs: Forwarded to `GeneStrategy`.
    """

    def __init__(self, precomputed_redundancy: np.ndarray | None = None, **kwargs):
        super().__init__(**kwargs)
        self._redundancy: np.ndarray | None = precomputed_redundancy
        self._mode: str | None = None

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "RedundancyPenalty"

    @staticmethod
    def _encode_target(y: np.ndarray) -> np.ndarray:
        """Encode a target array to binary ±1 labels centred at the median.

        Args:
            y: Target array of any dtype.

        Returns:
            1D float array with values ``+1.0`` (≥ median) or ``-1.0`` (< median).
        """
        y = np.asarray(y).flatten()
        if not np.issubdtype(y.dtype, np.number):
            unique_vals = np.unique(y)
            y_numeric = np.searchsorted(unique_vals, y).astype(float)
        else:
            y_numeric = y.astype(float)
        median = np.median(y_numeric)
        return np.where(y_numeric >= median, 1.0, -1.0)

    @staticmethod
    def compute_redundancy(X: np.ndarray) -> np.ndarray:
        """Compute mean absolute pairwise correlation for each column of ``X``.

        Args:
            X: Matrix of shape ``(n_samples, n_cols)``.

        Returns:
            Array of shape ``(n_cols,)`` with values in ``[0, 1]``.
        """
        if X.shape[1] <= 1:
            return np.array([0.0])
        corr = np.nan_to_num(np.corrcoef(X.T), nan=0.0)
        n = corr.shape[0]
        # Mean absolute off-diagonal correlation per column
        abs_corr_sum = np.sum(np.abs(corr), axis=0) - 1.0  # subtract self-correlation
        return abs_corr_sum / (n - 1)

    def _get_scores(self, X: np.ndarray, y: np.ndarray | None) -> np.ndarray:
        """Return cached redundancy scores, recomputing if the mode changes.

        Args:
            X: Data matrix of shape ``(n_samples, n_features)``.
            y: Optional target array; triggers supervised mode when provided.

        Returns:
            Per-feature redundancy scores of shape ``(n_features,)``.
        """
        mode = "supervised" if y is not None else "unsupervised"
        if self._redundancy is None or self._mode != mode:
            self._mode = mode
            if y is not None:
                y_col = self._encode_target(y).reshape(-1, 1)
                X_aug = np.column_stack([X, y_col])
                # Slice back to n_features — y column was appended only for correlation
                self._redundancy = self.compute_redundancy(X_aug)[: X.shape[1]]
            else:
                self._redundancy = self.compute_redundancy(X)
        return self._redundancy

    def __call__(self, ctx: StrategyContext) -> float:
        """Compute delta for the RedundancyPenalty gene strategy.

        Args:
            ctx: Strategy context.  ``ctx.y`` is used when available.

        Returns:
            float: The computed delta ``Delta_G(i,j)``.
        """
        scores = self._get_scores(ctx.population.matrix, ctx.y)
        return float(
            (4 / ctx.population.N)
            * ctx.gene_fitness[ctx.gene_id]
            * (0.5 - scores[ctx.gene_id])
        )

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Diagonal D: ``D[j,j] = 4 * (0.5 - redundancy[j])``.

        Args:
            population: Current population.
            gene_similarity: Gene-similarity matrix (unused).
            org_similarity: Organism-similarity matrix (unused).
            initial_org_fitness_range: Initial fitness range (unused).
            y: Optional target labels for supervised mode.

        Returns:
            ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
        """
        scores = self._get_scores(population.matrix, y)
        D = np.diag(4.0 * (0.5 - scores))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> float

Compute delta for the RedundancyPenalty gene strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Strategy context. ctx.y is used when available.

required

Returns:

Name Type Description
float float

The computed delta Delta_G(i,j).

Source code in pikaia/strategies/gs_strategies/redundancy_penalty_strategy.py
def __call__(self, ctx: StrategyContext) -> float:
    """Compute delta for the RedundancyPenalty gene strategy.

    Args:
        ctx: Strategy context.  ``ctx.y`` is used when available.

    Returns:
        float: The computed delta ``Delta_G(i,j)``.
    """
    scores = self._get_scores(ctx.population.matrix, ctx.y)
    return float(
        (4 / ctx.population.N)
        * ctx.gene_fitness[ctx.gene_id]
        * (0.5 - scores[ctx.gene_id])
    )

compute_redundancy(X: np.ndarray) -> np.ndarray staticmethod

Compute mean absolute pairwise correlation for each column of X.

Parameters:

Name Type Description Default
X ndarray

Matrix of shape (n_samples, n_cols).

required

Returns:

Type Description
ndarray

Array of shape (n_cols,) with values in [0, 1].

Source code in pikaia/strategies/gs_strategies/redundancy_penalty_strategy.py
@staticmethod
def compute_redundancy(X: np.ndarray) -> np.ndarray:
    """Compute mean absolute pairwise correlation for each column of ``X``.

    Args:
        X: Matrix of shape ``(n_samples, n_cols)``.

    Returns:
        Array of shape ``(n_cols,)`` with values in ``[0, 1]``.
    """
    if X.shape[1] <= 1:
        return np.array([0.0])
    corr = np.nan_to_num(np.corrcoef(X.T), nan=0.0)
    n = corr.shape[0]
    # Mean absolute off-diagonal correlation per column
    abs_corr_sum = np.sum(np.abs(corr), axis=0) - 1.0  # subtract self-correlation
    return abs_corr_sum / (n - 1)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Diagonal D: D[j,j] = 4 * (0.5 - redundancy[j]).

Parameters:

Name Type Description Default
population PikaiaPopulation

Current population.

required
gene_similarity ndarray

Gene-similarity matrix (unused).

required
org_similarity ndarray

Organism-similarity matrix (unused).

required
initial_org_fitness_range float

Initial fitness range (unused).

required
y ndarray | None

Optional target labels for supervised mode.

None

Returns:

Type Description
tuple[ndarray | None, ndarray | None]

(D, None) where D is a diagonal (M, M) matrix.

Source code in pikaia/strategies/gs_strategies/redundancy_penalty_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Diagonal D: ``D[j,j] = 4 * (0.5 - redundancy[j])``.

    Args:
        population: Current population.
        gene_similarity: Gene-similarity matrix (unused).
        org_similarity: Organism-similarity matrix (unused).
        initial_org_fitness_range: Initial fitness range (unused).
        y: Optional target labels for supervised mode.

    Returns:
        ``(D, None)`` where ``D`` is a diagonal ``(M, M)`` matrix.
    """
    scores = self._get_scores(population.matrix, y)
    D = np.diag(4.0 * (0.5 - scores))
    return D, None

Organism Strategies

pikaia.strategies.os_strategies.balanced_strategy.BalancedOrgStrategy

Bases: OrgStrategy

An organism strategy that promotes balanced gene contributions.

This strategy adjusts gene fitness to favor organisms where the contribution of each gene to the organism's total fitness is balanced. It penalizes genes that contribute disproportionately (more or less) than the average. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/os_strategies/balanced_strategy.py
class BalancedOrgStrategy(OrgStrategy):
    """
    An organism strategy that promotes balanced gene contributions.

    This strategy adjusts gene fitness to favor organisms where the
    contribution of each gene to the organism's total fitness is balanced.
    It penalizes genes that contribute disproportionately (more or less) than
    the average. This implementation follows the logic from the original
    `alg.py`.
    """

    def __init__(self, **kwargs):
        """Initialise the Balanced organism strategy.

        Args:
            **kwargs: Keyword options forwarded to `OrgStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Balanced"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        """
        Computes deltas for a balanced organism strategy.

        The formula calculates the deviation of each gene's contribution from
        the ideal balanced state (`1/m`) and adjusts its fitness accordingly.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
        """
        current_org_fitness = ctx.org_fitness[ctx.org_id]

        if current_org_fitness == 0:
            return np.zeros(ctx.population.M)

        delta_o = (
            # constant factor and normalization by population size
            (-2 / ctx.population.N)
            # deviation from ideal balanced contribution
            * (
                (ctx.population[ctx.org_id, :] * ctx.gene_fitness) / current_org_fitness
                - 1 / ctx.population.M
            )
            * current_org_fitness
        )
        return delta_o

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Rank-1 D matrix exploiting gamma normalisation.

        Because ``sum_j gamma_j = 1``, a row-constant matrix
        ``D[j, k] = -2 * x_bar_j`` satisfies ``(D @ gamma)_j = -2 * x_bar_j``
        for any normalised ``gamma``, exactly reproducing the balanced-org
        contribution ``delta_j ≈ -2 * x_bar_j * gamma_j``.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Unused.
            org_similarity: Unused.
            initial_org_fitness_range: Unused.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is a rank-1 ``(M, M)`` matrix
            with ``D[j, k] = -2 * x_bar_j`` for all *k*.
        """
        x_bar = population.matrix.mean(axis=0)  # (M,)
        M = population.M
        # D[j, k] = -2*x_bar_j  for all k
        D = np.outer(-2.0 * x_bar, np.ones(M))
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> np.ndarray

Computes deltas for a balanced organism strategy.

The formula calculates the deviation of each gene's contribution from the ideal balanced state (1/m) and adjusts its fitness accordingly.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Type Description
ndarray

np.ndarray: A vector of computed delta values Delta_O(i,j) of shape (m,).

Source code in pikaia/strategies/os_strategies/balanced_strategy.py
def __call__(self, ctx: StrategyContext) -> np.ndarray:
    """
    Computes deltas for a balanced organism strategy.

    The formula calculates the deviation of each gene's contribution from
    the ideal balanced state (`1/m`) and adjusts its fitness accordingly.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
    """
    current_org_fitness = ctx.org_fitness[ctx.org_id]

    if current_org_fitness == 0:
        return np.zeros(ctx.population.M)

    delta_o = (
        # constant factor and normalization by population size
        (-2 / ctx.population.N)
        # deviation from ideal balanced contribution
        * (
            (ctx.population[ctx.org_id, :] * ctx.gene_fitness) / current_org_fitness
            - 1 / ctx.population.M
        )
        * current_org_fitness
    )
    return delta_o

__init__(**kwargs)

Initialise the Balanced organism strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to OrgStrategy and stored in self.options.

{}
Source code in pikaia/strategies/os_strategies/balanced_strategy.py
def __init__(self, **kwargs):
    """Initialise the Balanced organism strategy.

    Args:
        **kwargs: Keyword options forwarded to `OrgStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Rank-1 D matrix exploiting gamma normalisation.

Because sum_j gamma_j = 1, a row-constant matrix D[j, k] = -2 * x_bar_j satisfies (D @ gamma)_j = -2 * x_bar_j for any normalised gamma, exactly reproducing the balanced-org contribution delta_j ≈ -2 * x_bar_j * gamma_j.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Unused.

required
org_similarity ndarray

Unused.

required
initial_org_fitness_range float

Unused.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is a rank-1 (M, M) matrix

ndarray | None

with D[j, k] = -2 * x_bar_j for all k.

Source code in pikaia/strategies/os_strategies/balanced_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Rank-1 D matrix exploiting gamma normalisation.

    Because ``sum_j gamma_j = 1``, a row-constant matrix
    ``D[j, k] = -2 * x_bar_j`` satisfies ``(D @ gamma)_j = -2 * x_bar_j``
    for any normalised ``gamma``, exactly reproducing the balanced-org
    contribution ``delta_j ≈ -2 * x_bar_j * gamma_j``.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Unused.
        org_similarity: Unused.
        initial_org_fitness_range: Unused.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is a rank-1 ``(M, M)`` matrix
        with ``D[j, k] = -2 * x_bar_j`` for all *k*.
    """
    x_bar = population.matrix.mean(axis=0)  # (M,)
    M = population.M
    # D[j, k] = -2*x_bar_j  for all k
    D = np.outer(-2.0 * x_bar, np.ones(M))
    return D, None

pikaia.strategies.os_strategies.altruistic_strategy.AltruisticOrgStrategy

Bases: OrgStrategy

An organism strategy that promotes altruistic behavior towards relatives.

Warning

This strategy is experimental and its behavior may change in future versions.

This strategy models altruism where an organism's fitness contribution is adjusted based on its interaction with related organisms (kin). The delta is calculated based on the fitness difference between the organism and its relatives, weighted by their similarity. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/os_strategies/altruistic_strategy.py
class AltruisticOrgStrategy(OrgStrategy):
    """
    An organism strategy that promotes altruistic behavior towards relatives.

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    This strategy models altruism where an organism's fitness contribution is
    adjusted based on its interaction with related organisms (kin). The delta
    is calculated based on the fitness difference between the organism and its
    relatives, weighted by their similarity. This implementation follows the
    logic from the original `alg.py`.
    """

    def __init__(self, **kwargs):
        """Initialise the Altruistic organism strategy.

        Keyword Args:
            kin_range (int): Maximum number of organisms to consider as kin
                when computing the interaction term.  Defaults to ``N``
                (the full population size).
            **kwargs: Additional options forwarded to `OrgStrategy`
                and stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Altruistic"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        """
        Computes deltas for an altruistic organism strategy.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
        """
        # Determine kin range
        kin_range = self.options.get("kin_range", ctx.population.N)
        if kin_range > 32:
            logger.warning(
                f"kin_range is very large ({kin_range}). "
                "This may severely impact performance."
            )

        # Get indices of most similar relatives, excluding self
        relatives = np.argsort(-ctx.org_similarity[ctx.org_id, :])
        relatives = relatives[:kin_range]
        relatives = relatives[relatives != ctx.org_id]

        # Early exit if no relatives or zero organism fitness
        if len(relatives) == 0 or ctx.org_fitness[ctx.org_id] == 0:
            return np.zeros(ctx.population.M)

        # Compute gene-specific term: (gene_contribution / org_fitness - 1/M)
        gene_contribution = ctx.population[ctx.org_id, :] * ctx.gene_fitness
        gene_term = (gene_contribution / ctx.org_fitness[ctx.org_id]) - (
            1 / ctx.population.M
        )

        # Compute relative weights: similarity * fitness difference
        org_similarity = ctx.org_similarity[ctx.org_id, relatives]
        fitness_diff = ctx.org_fitness[ctx.org_id] - ctx.org_fitness[relatives]
        rel_weights = org_similarity * fitness_diff

        # Vectorized computation: outer product and sum over relatives
        delta_o_matrix = np.outer(gene_term, rel_weights)
        summed_delta_o = np.sum(delta_o_matrix, axis=1)

        # Final delta calculation
        delta_o = (
            # constant factor
            (-2 / ctx.population.N)
            # normalization by kin range
            * (1 / kin_range)
            # scale by initial range
            * (summed_delta_o / ctx.initial_org_fitness_range)
        )

        return delta_o

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Full ``(M, M)`` D matrix for kin-altruistic org interactions.

        Identical computation to `SelfishOrgStrategy`'s kernel
        (``D_alt = D_sel``) because the formula is symmetric under the sign
        convention used in the replicator equation.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Unused.
            org_similarity: Organism similarity matrix of shape ``(N, N)``.
            initial_org_fitness_range: Used to normalise the D matrix.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix summing
            outer products of gene-expression vectors weighted by kin
            similarity differences, scaled by ``-2 / (N * R)``.
        """
        X = population.matrix  # (N, M)
        N = population.N
        R = initial_org_fitness_range
        kin_range = self.options.get("kin_range", N)

        D_acc = np.zeros((population.M, population.M))
        n_contributing = 0
        for i in range(N):
            sorted_idx = np.argsort(-org_similarity[i, :])
            relatives_i = sorted_idx[sorted_idx != i][:kin_range]
            if len(relatives_i) == 0:
                continue
            n_rel = len(relatives_i)
            s_il = org_similarity[i, relatives_i]
            x_diff = X[i, np.newaxis, :] - X[relatives_i, :]  # (n_rel, M)
            sum_l = s_il @ x_diff  # (M,)
            D_acc += np.outer(X[i, :], sum_l) / n_rel
            n_contributing += 1

        if n_contributing == 0:
            return np.zeros((population.M, population.M)), None

        D = D_acc / N * (-2.0 / R)
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> np.ndarray

Computes deltas for an altruistic organism strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Type Description
ndarray

np.ndarray: A vector of computed delta values Delta_O(i,j) of shape (m,).

Source code in pikaia/strategies/os_strategies/altruistic_strategy.py
def __call__(self, ctx: StrategyContext) -> np.ndarray:
    """
    Computes deltas for an altruistic organism strategy.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
    """
    # Determine kin range
    kin_range = self.options.get("kin_range", ctx.population.N)
    if kin_range > 32:
        logger.warning(
            f"kin_range is very large ({kin_range}). "
            "This may severely impact performance."
        )

    # Get indices of most similar relatives, excluding self
    relatives = np.argsort(-ctx.org_similarity[ctx.org_id, :])
    relatives = relatives[:kin_range]
    relatives = relatives[relatives != ctx.org_id]

    # Early exit if no relatives or zero organism fitness
    if len(relatives) == 0 or ctx.org_fitness[ctx.org_id] == 0:
        return np.zeros(ctx.population.M)

    # Compute gene-specific term: (gene_contribution / org_fitness - 1/M)
    gene_contribution = ctx.population[ctx.org_id, :] * ctx.gene_fitness
    gene_term = (gene_contribution / ctx.org_fitness[ctx.org_id]) - (
        1 / ctx.population.M
    )

    # Compute relative weights: similarity * fitness difference
    org_similarity = ctx.org_similarity[ctx.org_id, relatives]
    fitness_diff = ctx.org_fitness[ctx.org_id] - ctx.org_fitness[relatives]
    rel_weights = org_similarity * fitness_diff

    # Vectorized computation: outer product and sum over relatives
    delta_o_matrix = np.outer(gene_term, rel_weights)
    summed_delta_o = np.sum(delta_o_matrix, axis=1)

    # Final delta calculation
    delta_o = (
        # constant factor
        (-2 / ctx.population.N)
        # normalization by kin range
        * (1 / kin_range)
        # scale by initial range
        * (summed_delta_o / ctx.initial_org_fitness_range)
    )

    return delta_o

__init__(**kwargs)

Initialise the Altruistic organism strategy.

Other Parameters:

Name Type Description
kin_range int

Maximum number of organisms to consider as kin when computing the interaction term. Defaults to N (the full population size).

**kwargs

Additional options forwarded to OrgStrategy and stored in self.options.

Source code in pikaia/strategies/os_strategies/altruistic_strategy.py
def __init__(self, **kwargs):
    """Initialise the Altruistic organism strategy.

    Keyword Args:
        kin_range (int): Maximum number of organisms to consider as kin
            when computing the interaction term.  Defaults to ``N``
            (the full population size).
        **kwargs: Additional options forwarded to `OrgStrategy`
            and stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Full (M, M) D matrix for kin-altruistic org interactions.

Identical computation to SelfishOrgStrategy's kernel (D_alt = D_sel) because the formula is symmetric under the sign convention used in the replicator equation.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Unused.

required
org_similarity ndarray

Organism similarity matrix of shape (N, N).

required
initial_org_fitness_range float

Used to normalise the D matrix.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is an (M, M) matrix summing

ndarray | None

outer products of gene-expression vectors weighted by kin

tuple[ndarray | None, ndarray | None]

similarity differences, scaled by -2 / (N * R).

Source code in pikaia/strategies/os_strategies/altruistic_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Full ``(M, M)`` D matrix for kin-altruistic org interactions.

    Identical computation to `SelfishOrgStrategy`'s kernel
    (``D_alt = D_sel``) because the formula is symmetric under the sign
    convention used in the replicator equation.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Unused.
        org_similarity: Organism similarity matrix of shape ``(N, N)``.
        initial_org_fitness_range: Used to normalise the D matrix.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix summing
        outer products of gene-expression vectors weighted by kin
        similarity differences, scaled by ``-2 / (N * R)``.
    """
    X = population.matrix  # (N, M)
    N = population.N
    R = initial_org_fitness_range
    kin_range = self.options.get("kin_range", N)

    D_acc = np.zeros((population.M, population.M))
    n_contributing = 0
    for i in range(N):
        sorted_idx = np.argsort(-org_similarity[i, :])
        relatives_i = sorted_idx[sorted_idx != i][:kin_range]
        if len(relatives_i) == 0:
            continue
        n_rel = len(relatives_i)
        s_il = org_similarity[i, relatives_i]
        x_diff = X[i, np.newaxis, :] - X[relatives_i, :]  # (n_rel, M)
        sum_l = s_il @ x_diff  # (M,)
        D_acc += np.outer(X[i, :], sum_l) / n_rel
        n_contributing += 1

    if n_contributing == 0:
        return np.zeros((population.M, population.M)), None

    D = D_acc / N * (-2.0 / R)
    return D, None

pikaia.strategies.os_strategies.selfish_strategy.SelfishOrgStrategy

Bases: OrgStrategy

An organism strategy that promotes selfish behavior.

This strategy models selfishness where an organism aims to increase its own fitness, potentially at the expense of others. The delta is calculated based on the fitness difference between the organism and its relatives, weighted by their similarity. This is identical to the AltruisticOrgStrategy but is kept for semantic clarity and future independent development. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/os_strategies/selfish_strategy.py
class SelfishOrgStrategy(OrgStrategy):
    """
    An organism strategy that promotes selfish behavior.

    This strategy models selfishness where an organism aims to increase its
    own fitness, potentially at the expense of others. The delta is calculated
    based on the fitness difference between the organism and its relatives,
    weighted by their similarity. This is identical to the `AltruisticOrgStrategy`
    but is kept for semantic clarity and future independent development.
    This implementation follows the logic from the original `alg.py`.
    """

    def __init__(self, **kwargs):
        """Initialise the Selfish organism strategy.

        Keyword Args:
            kin_range (int): Maximum number of organisms to consider when
                computing the interaction term.  Defaults to ``N``
                (the full population size).
            **kwargs: Additional options forwarded to `OrgStrategy`
                and stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Selfish"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        """
        Computes deltas for a selfish organism strategy.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
        """
        # Determine kin range
        kin_range = self.options.get("kin_range", ctx.population.N)

        # Get indices of most similar relatives, excluding self
        relatives = np.argsort(-ctx.org_similarity[ctx.org_id, :])
        relatives = relatives[:kin_range]
        relatives = relatives[relatives != ctx.org_id]

        # Early exit if no relatives or zero organism fitness
        if len(relatives) == 0 or ctx.org_fitness[ctx.org_id] == 0:
            return np.zeros(ctx.population.M)

        # Compute gene-specific term: (gene_contribution / org_fitness - 1/M)
        gene_contribution = ctx.population[ctx.org_id, :] * ctx.gene_fitness
        gene_term = (gene_contribution / ctx.org_fitness[ctx.org_id]) - (
            1 / ctx.population.M
        )

        # Compute relative weights: similarity * fitness difference
        org_similarity = ctx.org_similarity[ctx.org_id, relatives]
        fitness_diff = ctx.org_fitness[ctx.org_id] - ctx.org_fitness[relatives]
        rel_weights = org_similarity * fitness_diff

        # Vectorized computation: outer product and sum over relatives
        delta_o_matrix = np.outer(gene_term, rel_weights)
        summed_delta_o = np.sum(delta_o_matrix, axis=1)

        # Final delta calculation
        delta_o = (
            # constant factor (negative for selfish)
            (-2 / ctx.population.N)
            # normalization by kin range
            * (1 / kin_range)
            # scale by initial range
            * (summed_delta_o / ctx.initial_org_fitness_range)
        )

        return delta_o

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Full ``(M, M)`` D matrix for kin-selfish org interactions.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Unused.
            org_similarity: Organism similarity matrix of shape ``(N, N)``.
            initial_org_fitness_range: Used to normalise the D matrix.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
            ``D[j, k] = (-2 / (N * R)) * sum_i[x_ij * sum_l(s^o_il * (x_ik - x_lk))]``,
            summed over kin neighbours of each organism.
        """
        X = population.matrix  # (N, M)
        N = population.N
        R = initial_org_fitness_range
        kin_range = self.options.get("kin_range", N)

        D_acc = np.zeros((population.M, population.M))
        n_contributing = 0
        for i in range(N):
            sorted_idx = np.argsort(-org_similarity[i, :])
            relatives_i = sorted_idx[sorted_idx != i][:kin_range]
            if len(relatives_i) == 0:
                continue
            n_rel = len(relatives_i)
            s_il = org_similarity[i, relatives_i]  # (n_rel,)
            # x_diff_lk[l, k] = X[i, k] - X[relatives_i[l], k]
            x_diff = X[i, np.newaxis, :] - X[relatives_i, :]  # (n_rel, M)
            # sum_l s_il * (x_ik - x_lk): (M,)
            sum_l = s_il @ x_diff
            D_acc += np.outer(X[i, :], sum_l) / n_rel
            n_contributing += 1

        if n_contributing == 0:
            return np.zeros((population.M, population.M)), None

        D = D_acc / N * (-2.0 / R)
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> np.ndarray

Computes deltas for a selfish organism strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Type Description
ndarray

np.ndarray: A vector of computed delta values Delta_O(i,j) of shape (m,).

Source code in pikaia/strategies/os_strategies/selfish_strategy.py
def __call__(self, ctx: StrategyContext) -> np.ndarray:
    """
    Computes deltas for a selfish organism strategy.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
    """
    # Determine kin range
    kin_range = self.options.get("kin_range", ctx.population.N)

    # Get indices of most similar relatives, excluding self
    relatives = np.argsort(-ctx.org_similarity[ctx.org_id, :])
    relatives = relatives[:kin_range]
    relatives = relatives[relatives != ctx.org_id]

    # Early exit if no relatives or zero organism fitness
    if len(relatives) == 0 or ctx.org_fitness[ctx.org_id] == 0:
        return np.zeros(ctx.population.M)

    # Compute gene-specific term: (gene_contribution / org_fitness - 1/M)
    gene_contribution = ctx.population[ctx.org_id, :] * ctx.gene_fitness
    gene_term = (gene_contribution / ctx.org_fitness[ctx.org_id]) - (
        1 / ctx.population.M
    )

    # Compute relative weights: similarity * fitness difference
    org_similarity = ctx.org_similarity[ctx.org_id, relatives]
    fitness_diff = ctx.org_fitness[ctx.org_id] - ctx.org_fitness[relatives]
    rel_weights = org_similarity * fitness_diff

    # Vectorized computation: outer product and sum over relatives
    delta_o_matrix = np.outer(gene_term, rel_weights)
    summed_delta_o = np.sum(delta_o_matrix, axis=1)

    # Final delta calculation
    delta_o = (
        # constant factor (negative for selfish)
        (-2 / ctx.population.N)
        # normalization by kin range
        * (1 / kin_range)
        # scale by initial range
        * (summed_delta_o / ctx.initial_org_fitness_range)
    )

    return delta_o

__init__(**kwargs)

Initialise the Selfish organism strategy.

Other Parameters:

Name Type Description
kin_range int

Maximum number of organisms to consider when computing the interaction term. Defaults to N (the full population size).

**kwargs

Additional options forwarded to OrgStrategy and stored in self.options.

Source code in pikaia/strategies/os_strategies/selfish_strategy.py
def __init__(self, **kwargs):
    """Initialise the Selfish organism strategy.

    Keyword Args:
        kin_range (int): Maximum number of organisms to consider when
            computing the interaction term.  Defaults to ``N``
            (the full population size).
        **kwargs: Additional options forwarded to `OrgStrategy`
            and stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Full (M, M) D matrix for kin-selfish org interactions.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Unused.

required
org_similarity ndarray

Organism similarity matrix of shape (N, N).

required
initial_org_fitness_range float

Used to normalise the D matrix.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is an (M, M) matrix with

ndarray | None

D[j, k] = (-2 / (N * R)) * sum_i[x_ij * sum_l(s^o_il * (x_ik - x_lk))],

tuple[ndarray | None, ndarray | None]

summed over kin neighbours of each organism.

Source code in pikaia/strategies/os_strategies/selfish_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Full ``(M, M)`` D matrix for kin-selfish org interactions.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Unused.
        org_similarity: Organism similarity matrix of shape ``(N, N)``.
        initial_org_fitness_range: Used to normalise the D matrix.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
        ``D[j, k] = (-2 / (N * R)) * sum_i[x_ij * sum_l(s^o_il * (x_ik - x_lk))]``,
        summed over kin neighbours of each organism.
    """
    X = population.matrix  # (N, M)
    N = population.N
    R = initial_org_fitness_range
    kin_range = self.options.get("kin_range", N)

    D_acc = np.zeros((population.M, population.M))
    n_contributing = 0
    for i in range(N):
        sorted_idx = np.argsort(-org_similarity[i, :])
        relatives_i = sorted_idx[sorted_idx != i][:kin_range]
        if len(relatives_i) == 0:
            continue
        n_rel = len(relatives_i)
        s_il = org_similarity[i, relatives_i]  # (n_rel,)
        # x_diff_lk[l, k] = X[i, k] - X[relatives_i[l], k]
        x_diff = X[i, np.newaxis, :] - X[relatives_i, :]  # (n_rel, M)
        # sum_l s_il * (x_ik - x_lk): (M,)
        sum_l = s_il @ x_diff
        D_acc += np.outer(X[i, :], sum_l) / n_rel
        n_contributing += 1

    if n_contributing == 0:
        return np.zeros((population.M, population.M)), None

    D = D_acc / N * (-2.0 / R)
    return D, None

pikaia.strategies.os_strategies.kin_selfish_strategy.KinSelfishOrgStrategy

Bases: OrgStrategy

An organism strategy that promotes selfish behavior towards non-kin.

Warning

This strategy is experimental and its behavior may change in future versions.

This strategy models selfish behavior where an organism's fitness is increased at the expense of less related organisms. The selfish effect is inversely proportional to similarity, meaning it acts more selfishly towards organisms that are less similar. This implementation follows the logic from the original alg.py.

Source code in pikaia/strategies/os_strategies/kin_selfish_strategy.py
class KinSelfishOrgStrategy(OrgStrategy):
    """
    An organism strategy that promotes selfish behavior towards non-kin.

    !!! warning
        This strategy is experimental and its behavior may change in future
        versions.

    This strategy models selfish behavior where an organism's fitness is
    increased at the expense of less related organisms. The selfish effect is
    inversely proportional to similarity, meaning it acts more selfishly
    towards organisms that are less similar. This implementation follows the
    logic from the original `alg.py`.
    """

    def __init__(self, **kwargs):
        """Initialise the KinSelfish organism strategy.

        Keyword Args:
            kin_range (int): Maximum number of organisms to consider as kin
                when computing the interaction term.  Defaults to ``N``
                (the full population size).
            **kwargs: Additional options forwarded to `OrgStrategy`
                and stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "KinSelfish"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        """
        Computes deltas for a kin-selfish organism strategy.

        Args:
            ctx (StrategyContext): Context object containing all required and optional fields.

        Returns:
            np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
        """
        # Determine kin range
        kin_range = self.options.get("kin_range", ctx.population.N)

        # Get indices of most similar relatives, excluding self
        relatives = np.argsort(-ctx.org_similarity[ctx.org_id, :])
        relatives = relatives[:kin_range]
        relatives = relatives[relatives != ctx.org_id]

        # Early exit if no relatives or zero organism fitness
        if len(relatives) == 0 or ctx.org_fitness[ctx.org_id] == 0:
            return np.zeros(ctx.population.M)

        # Compute gene-specific term: (gene_contribution / org_fitness - 1/M)
        gene_contribution = ctx.population[ctx.org_id, :] * ctx.gene_fitness
        gene_term = (gene_contribution / ctx.org_fitness[ctx.org_id]) - (
            1 / ctx.population.M
        )

        # Compute kin-selfish weights: (0.5 - similarity) * fitness difference
        org_similarity = ctx.org_similarity[ctx.org_id, relatives]
        kin_selfish_weight = 0.5 - org_similarity
        fitness_diff = ctx.org_fitness[ctx.org_id] - ctx.org_fitness[relatives]
        rel_weights = kin_selfish_weight * fitness_diff

        # Vectorized computation: outer product and sum over relatives
        delta_o_matrix = np.outer(gene_term, rel_weights)
        summed_delta_o = np.sum(delta_o_matrix, axis=1)

        # Final delta calculation
        delta_o = (
            # constant factor (positive for kin-selfish)
            (2 / ctx.population.N)
            # normalization by kin range
            * (1 / kin_range)
            # scale by initial range
            * (summed_delta_o / ctx.initial_org_fitness_range)
        )

        return delta_o

    def kernel(
        self,
        population: PikaiaPopulation,
        gene_similarity: np.ndarray,
        org_similarity: np.ndarray,
        initial_org_fitness_range: float,
        y: np.ndarray | None = None,
    ) -> tuple[np.ndarray | None, np.ndarray | None]:
        """Full ``(M, M)`` D matrix with kin-inverted similarity weights.

        Like `SelfishOrgStrategy`'s kernel but uses ``(0.5 - s^o_il)``
        as the similarity weight, flipping the sign for close kin.

        Args:
            population: Population providing the ``(N, M)`` data matrix.
            gene_similarity: Unused.
            org_similarity: Organism similarity matrix of shape ``(N, N)``.
            initial_org_fitness_range: Used to normalise the D matrix.
            y: Unused.

        Returns:
            Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
            ``D[j, k] = (+2 / (N * R)) * sum_i[x_ij * sum_l((0.5 - s^o_il) * (x_ik - x_lk))]``,
            summed over kin neighbours of each organism.
        """
        X = population.matrix  # (N, M)
        N = population.N
        R = initial_org_fitness_range
        kin_range = self.options.get("kin_range", N)

        D_acc = np.zeros((population.M, population.M))
        n_contributing = 0
        for i in range(N):
            sorted_idx = np.argsort(-org_similarity[i, :])
            relatives_i = sorted_idx[sorted_idx != i][:kin_range]
            if len(relatives_i) == 0:
                continue
            n_rel = len(relatives_i)
            s_il = org_similarity[i, relatives_i]
            x_diff = X[i, np.newaxis, :] - X[relatives_i, :]  # (n_rel, M)
            sum_l = (0.5 - s_il) @ x_diff  # (M,)
            D_acc += np.outer(X[i, :], sum_l) / n_rel
            n_contributing += 1

        if n_contributing == 0:
            return np.zeros((population.M, population.M)), None

        D = D_acc / N * (2.0 / R)
        return D, None

Attributes

name: str property

The name of the strategy.

Methods:

__call__(ctx: StrategyContext) -> np.ndarray

Computes deltas for a kin-selfish organism strategy.

Parameters:

Name Type Description Default
ctx StrategyContext

Context object containing all required and optional fields.

required

Returns:

Type Description
ndarray

np.ndarray: A vector of computed delta values Delta_O(i,j) of shape (m,).

Source code in pikaia/strategies/os_strategies/kin_selfish_strategy.py
def __call__(self, ctx: StrategyContext) -> np.ndarray:
    """
    Computes deltas for a kin-selfish organism strategy.

    Args:
        ctx (StrategyContext): Context object containing all required and optional fields.

    Returns:
        np.ndarray: A vector of computed delta values `Delta_O(i,j)` of shape `(m,)`.
    """
    # Determine kin range
    kin_range = self.options.get("kin_range", ctx.population.N)

    # Get indices of most similar relatives, excluding self
    relatives = np.argsort(-ctx.org_similarity[ctx.org_id, :])
    relatives = relatives[:kin_range]
    relatives = relatives[relatives != ctx.org_id]

    # Early exit if no relatives or zero organism fitness
    if len(relatives) == 0 or ctx.org_fitness[ctx.org_id] == 0:
        return np.zeros(ctx.population.M)

    # Compute gene-specific term: (gene_contribution / org_fitness - 1/M)
    gene_contribution = ctx.population[ctx.org_id, :] * ctx.gene_fitness
    gene_term = (gene_contribution / ctx.org_fitness[ctx.org_id]) - (
        1 / ctx.population.M
    )

    # Compute kin-selfish weights: (0.5 - similarity) * fitness difference
    org_similarity = ctx.org_similarity[ctx.org_id, relatives]
    kin_selfish_weight = 0.5 - org_similarity
    fitness_diff = ctx.org_fitness[ctx.org_id] - ctx.org_fitness[relatives]
    rel_weights = kin_selfish_weight * fitness_diff

    # Vectorized computation: outer product and sum over relatives
    delta_o_matrix = np.outer(gene_term, rel_weights)
    summed_delta_o = np.sum(delta_o_matrix, axis=1)

    # Final delta calculation
    delta_o = (
        # constant factor (positive for kin-selfish)
        (2 / ctx.population.N)
        # normalization by kin range
        * (1 / kin_range)
        # scale by initial range
        * (summed_delta_o / ctx.initial_org_fitness_range)
    )

    return delta_o

__init__(**kwargs)

Initialise the KinSelfish organism strategy.

Other Parameters:

Name Type Description
kin_range int

Maximum number of organisms to consider as kin when computing the interaction term. Defaults to N (the full population size).

**kwargs

Additional options forwarded to OrgStrategy and stored in self.options.

Source code in pikaia/strategies/os_strategies/kin_selfish_strategy.py
def __init__(self, **kwargs):
    """Initialise the KinSelfish organism strategy.

    Keyword Args:
        kin_range (int): Maximum number of organisms to consider as kin
            when computing the interaction term.  Defaults to ``N``
            (the full population size).
        **kwargs: Additional options forwarded to `OrgStrategy`
            and stored in ``self.options``.
    """
    super().__init__(**kwargs)

kernel(population: PikaiaPopulation, gene_similarity: np.ndarray, org_similarity: np.ndarray, initial_org_fitness_range: float, y: np.ndarray | None = None) -> tuple[np.ndarray | None, np.ndarray | None]

Full (M, M) D matrix with kin-inverted similarity weights.

Like SelfishOrgStrategy's kernel but uses (0.5 - s^o_il) as the similarity weight, flipping the sign for close kin.

Parameters:

Name Type Description Default
population PikaiaPopulation

Population providing the (N, M) data matrix.

required
gene_similarity ndarray

Unused.

required
org_similarity ndarray

Organism similarity matrix of shape (N, N).

required
initial_org_fitness_range float

Used to normalise the D matrix.

required
y ndarray | None

Unused.

None

Returns:

Type Description
ndarray | None

Tuple (D, None) where D is an (M, M) matrix with

ndarray | None

D[j, k] = (+2 / (N * R)) * sum_i[x_ij * sum_l((0.5 - s^o_il) * (x_ik - x_lk))],

tuple[ndarray | None, ndarray | None]

summed over kin neighbours of each organism.

Source code in pikaia/strategies/os_strategies/kin_selfish_strategy.py
def kernel(
    self,
    population: PikaiaPopulation,
    gene_similarity: np.ndarray,
    org_similarity: np.ndarray,
    initial_org_fitness_range: float,
    y: np.ndarray | None = None,
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Full ``(M, M)`` D matrix with kin-inverted similarity weights.

    Like `SelfishOrgStrategy`'s kernel but uses ``(0.5 - s^o_il)``
    as the similarity weight, flipping the sign for close kin.

    Args:
        population: Population providing the ``(N, M)`` data matrix.
        gene_similarity: Unused.
        org_similarity: Organism similarity matrix of shape ``(N, N)``.
        initial_org_fitness_range: Used to normalise the D matrix.
        y: Unused.

    Returns:
        Tuple ``(D, None)`` where ``D`` is an ``(M, M)`` matrix with
        ``D[j, k] = (+2 / (N * R)) * sum_i[x_ij * sum_l((0.5 - s^o_il) * (x_ik - x_lk))]``,
        summed over kin neighbours of each organism.
    """
    X = population.matrix  # (N, M)
    N = population.N
    R = initial_org_fitness_range
    kin_range = self.options.get("kin_range", N)

    D_acc = np.zeros((population.M, population.M))
    n_contributing = 0
    for i in range(N):
        sorted_idx = np.argsort(-org_similarity[i, :])
        relatives_i = sorted_idx[sorted_idx != i][:kin_range]
        if len(relatives_i) == 0:
            continue
        n_rel = len(relatives_i)
        s_il = org_similarity[i, relatives_i]
        x_diff = X[i, np.newaxis, :] - X[relatives_i, :]  # (n_rel, M)
        sum_l = (0.5 - s_il) @ x_diff  # (M,)
        D_acc += np.outer(X[i, :], sum_l) / n_rel
        n_contributing += 1

    if n_contributing == 0:
        return np.zeros((population.M, population.M)), None

    D = D_acc / N * (2.0 / R)
    return D, None

pikaia.strategies.os_strategies.buy_hard_strategy.BuyHardOrgStrategy

Bases: OrgStrategy

Trading buy-phase paired with SellHardGeneStrategy.

Each organism spends its sell capital (earned from hard genes) on genes it failed, weighted by how easy those genes are (mean_j). Organisms that solved many hard genes accumulate more capital and redistribute it to the easy genes they missed.

For organism i, the capital earned from selling is:

\[ C_i = \frac{1}{N} \sum_k x_{ik} \cdot \frac{\text{excl}_k}{1 - \text{excl}_k + \varepsilon} \]

normalised by the easy-weighted sum of failed genes:

\[ Z_i = \sum_k (1 - x_{ik}) \cdot \bar{x}_k \]

The buy contribution from organism i to gene j is:

\[ \Delta_{\text{buy\_hard}}(i, j) = (1 - x_{ij}) \cdot \frac{C_i}{Z_i} \cdot \bar{x}_j \]

Pair with SellHardGeneStrategy for the full hard-gene trading round.

Source code in pikaia/strategies/os_strategies/buy_hard_strategy.py
class BuyHardOrgStrategy(OrgStrategy):
    """
    Trading buy-phase paired with `SellHardGeneStrategy`.

    Each organism spends its sell capital (earned from hard genes) on genes it
    failed, weighted by how easy those genes are (``mean_j``).  Organisms that
    solved many hard genes accumulate more capital and redistribute it to the
    easy genes they missed.

    For organism *i*, the capital earned from selling is:

    $$
    C_i = \\frac{1}{N} \\sum_k x_{ik}
          \\cdot \\frac{\\text{excl}_k}{1 - \\text{excl}_k + \\varepsilon}
    $$

    normalised by the easy-weighted sum of failed genes:

    $$
    Z_i = \\sum_k (1 - x_{ik}) \\cdot \\bar{x}_k
    $$

    The buy contribution from organism *i* to gene *j* is:

    $$
    \\Delta_{\\text{buy\\_hard}}(i, j) =
        (1 - x_{ij}) \\cdot \\frac{C_i}{Z_i} \\cdot \\bar{x}_j
    $$

    Pair with `SellHardGeneStrategy` for the full hard-gene trading round.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        return "BuyHard"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        X = ctx.population.matrix
        N, M = X.shape
        gamma = ctx.gene_fitness
        mean_all = X.mean(axis=0)
        excl = 1.0 - mean_all
        sell_signal = excl / (1.0 - excl + 1e-8)

        max_capital = (X * (sell_signal * gamma)[np.newaxis, :]).sum(axis=1) / N
        excl_norm2 = ((1.0 - X) * mean_all[np.newaxis, :]).sum(axis=1)

        i = ctx.org_id
        if excl_norm2[i] < 1e-10:
            return np.zeros(M)
        buy_abs = (1.0 - X[i, :]) * max_capital[i] / excl_norm2[i] * mean_all
        # Proportional delta: buy_abs / gamma_j keeps the replicator at the correct
        # fixed point where gamma_j * sell_signal_j * mean_j = buy_abs_j.
        return buy_abs / (gamma + 1e-10)

pikaia.strategies.os_strategies.buy_uniform_strategy.BuyUniformOrgStrategy

Bases: OrgStrategy

Trading buy-phase paired with SellUniformGeneStrategy.

Each organism spends its uniform sell capital (proportional to average performance) on genes it failed, weighted by how hard those genes are (excl_j). Unlike BuyHardOrgStrategy, easy organisms and hard genes receive more attention here.

For organism i, the capital from uniform selling is:

\[ C_i = \frac{1}{N} \sum_k x_{ik} \]

normalised by the hard-weighted sum of failed genes:

\[ Z_i = \sum_k (1 - x_{ik}) \cdot \text{excl}_k \]

The buy contribution from organism i to gene j is:

\[ \Delta_{\text{buy\_uniform}}(i, j) = (1 - x_{ij}) \cdot \frac{C_i}{Z_i} \cdot \text{excl}_j \]

Pair with SellUniformGeneStrategy for the full uniform trading round.

Source code in pikaia/strategies/os_strategies/buy_uniform_strategy.py
class BuyUniformOrgStrategy(OrgStrategy):
    """
    Trading buy-phase paired with `SellUniformGeneStrategy`.

    Each organism spends its uniform sell capital (proportional to average
    performance) on genes it failed, weighted by how hard those genes are
    (``excl_j``).  Unlike `BuyHardOrgStrategy`, easy organisms and hard genes
    receive more attention here.

    For organism *i*, the capital from uniform selling is:

    $$
    C_i = \\frac{1}{N} \\sum_k x_{ik}
    $$

    normalised by the hard-weighted sum of failed genes:

    $$
    Z_i = \\sum_k (1 - x_{ik}) \\cdot \\text{excl}_k
    $$

    The buy contribution from organism *i* to gene *j* is:

    $$
    \\Delta_{\\text{buy\\_uniform}}(i, j) =
        (1 - x_{ij}) \\cdot \\frac{C_i}{Z_i} \\cdot \\text{excl}_j
    $$

    Pair with `SellUniformGeneStrategy` for the full uniform trading round.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        return "BuyUniform"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        X = ctx.population.matrix
        N, M = X.shape
        gamma = ctx.gene_fitness
        mean_all = X.mean(axis=0)
        excl = 1.0 - mean_all

        # Capital only from genes with non-trivial exclusiveness: excl ∉ {0, 1}
        sell_mask = ((excl > 1e-6) & (excl < 1.0 - 1e-6)).astype(float)
        max_capital = (X * (sell_mask * gamma)[np.newaxis, :]).sum(axis=1) / N
        excl_norm = ((1.0 - X) * excl[np.newaxis, :]).sum(axis=1)

        i = ctx.org_id
        if excl_norm[i] < 1e-10:
            return np.zeros(M)
        buy_abs = (1.0 - X[i, :]) * max_capital[i] / excl_norm[i] * excl
        return buy_abs / (gamma + 1e-10)

pikaia.strategies.os_strategies.buy_easy_strategy.BuyEasyOrgStrategy

Bases: OrgStrategy

Trading buy-phase paired with SellEasyGeneStrategy — mirror of BuyHardOrgStrategy.

Capital is earned with a negative sign (from the easy sell signal), so the redistribution flows in the opposite direction to BuyHardOrgStrategy: organisms that solved easy genes accumulate capital and redistribute it to genes they failed, weighted by how easy those genes are.

For organism i, the (negative) capital from easy selling is:

\[ C_i = -\frac{1}{N} \sum_k x_{ik} \cdot \frac{\text{excl}_k}{1 - \text{excl}_k + \varepsilon} \]

normalised by the easy-weighted sum of failed genes (same as BuyHard):

\[ Z_i = \sum_k (1 - x_{ik}) \cdot \bar{x}_k \]

The buy contribution from organism i to gene j is:

\[ \Delta_{\text{buy\_easy}}(i, j) = (1 - x_{ij}) \cdot \frac{C_i}{Z_i} \cdot \bar{x}_j = -\Delta_{\text{buy\_hard}}(i, j) \]

Pair with SellEasyGeneStrategy for the full easy-gene trading round.

Source code in pikaia/strategies/os_strategies/buy_easy_strategy.py
class BuyEasyOrgStrategy(OrgStrategy):
    """
    Trading buy-phase paired with `SellEasyGeneStrategy` — mirror of `BuyHardOrgStrategy`.

    Capital is earned with a negative sign (from the easy sell signal),
    so the redistribution flows in the opposite direction to `BuyHardOrgStrategy`:
    organisms that solved easy genes accumulate capital and redistribute it
    to genes they failed, weighted by how easy those genes are.

    For organism *i*, the (negative) capital from easy selling is:

    $$
    C_i = -\\frac{1}{N} \\sum_k x_{ik}
           \\cdot \\frac{\\text{excl}_k}{1 - \\text{excl}_k + \\varepsilon}
    $$

    normalised by the easy-weighted sum of failed genes (same as BuyHard):

    $$
    Z_i = \\sum_k (1 - x_{ik}) \\cdot \\bar{x}_k
    $$

    The buy contribution from organism *i* to gene *j* is:

    $$
    \\Delta_{\\text{buy\\_easy}}(i, j) =
        (1 - x_{ij}) \\cdot \\frac{C_i}{Z_i} \\cdot \\bar{x}_j
        = -\\Delta_{\\text{buy\\_hard}}(i, j)
    $$

    Pair with `SellEasyGeneStrategy` for the full easy-gene trading round.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        return "BuyEasy"

    def __call__(self, ctx: StrategyContext) -> np.ndarray:
        X = ctx.population.matrix
        N, M = X.shape
        gamma = ctx.gene_fitness
        mean_all = X.mean(axis=0)
        excl = 1.0 - mean_all
        sell_signal = excl / (1.0 - excl + 1e-8)

        # Negative capital — Inverse sell earns the opposite sign.
        max_capital = -(X * (sell_signal * gamma)[np.newaxis, :]).sum(axis=1) / N
        excl_norm2 = ((1.0 - X) * mean_all[np.newaxis, :]).sum(axis=1)

        i = ctx.org_id
        if excl_norm2[i] < 1e-10:
            return np.zeros(M)
        buy_abs = (1.0 - X[i, :]) * max_capital[i] / excl_norm2[i] * mean_all
        return buy_abs / (gamma + 1e-10)

Mix Strategies

pikaia.strategies.mix_strategies.fixed_strategy.FixedMixStrategy

Bases: MixStrategy

Applies a fixed set of mixing coefficients to a delta tensor.

This strategy multiplies the input delta tensor by the provided mixing coefficients using Einstein summation, without updating or adapting the coefficients.

Example

strategy = FixedMixStrategy() mixed_delta, coeffs = strategy(delta, mix_coeffs)

Source code in pikaia/strategies/mix_strategies/fixed_strategy.py
class FixedMixStrategy(MixStrategy):
    """
    Applies a fixed set of mixing coefficients to a delta tensor.

    This strategy multiplies the input delta tensor by the provided mixing coefficients
    using Einstein summation, without updating or adapting the coefficients.


    Example:
        strategy = FixedMixStrategy()
        mixed_delta, coeffs = strategy(delta, mix_coeffs)
    """

    def __init__(self, **kwargs):
        """Initialise the Fixed mix strategy.

        Args:
            **kwargs: Keyword options forwarded to `MixStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "Fixed"

    def __call__(
        self, delta: np.ndarray, mix_coeffs: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray]:
        """
        Apply fixed mixing coefficients to the input delta tensor.

        Args:
            delta (np.ndarray):
                A 3D tensor of shape (n, m, k) representing the deltas to be mixed.
            mix_coeffs (np.ndarray):
                A 1D array of length k containing the mixing coefficients to apply.

        Returns:
            tuple[np.ndarray, np.ndarray]:
                - The mixed delta array of shape (n, m) after applying the coefficients.
                - The unchanged mixing coefficients array.
        """
        # Weighted sum over the last axis (k) of delta using mix_coeffs,
        # resulting in shape (n, m)
        return np.einsum("ijk,k->ij", delta, mix_coeffs), mix_coeffs

Attributes

name: str property

The name of the strategy.

Methods:

__call__(delta: np.ndarray, mix_coeffs: np.ndarray) -> tuple[np.ndarray, np.ndarray]

Apply fixed mixing coefficients to the input delta tensor.

Parameters:

Name Type Description Default
delta ndarray

A 3D tensor of shape (n, m, k) representing the deltas to be mixed.

required
mix_coeffs ndarray

A 1D array of length k containing the mixing coefficients to apply.

required

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - The mixed delta array of shape (n, m) after applying the coefficients. - The unchanged mixing coefficients array.

Source code in pikaia/strategies/mix_strategies/fixed_strategy.py
def __call__(
    self, delta: np.ndarray, mix_coeffs: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
    """
    Apply fixed mixing coefficients to the input delta tensor.

    Args:
        delta (np.ndarray):
            A 3D tensor of shape (n, m, k) representing the deltas to be mixed.
        mix_coeffs (np.ndarray):
            A 1D array of length k containing the mixing coefficients to apply.

    Returns:
        tuple[np.ndarray, np.ndarray]:
            - The mixed delta array of shape (n, m) after applying the coefficients.
            - The unchanged mixing coefficients array.
    """
    # Weighted sum over the last axis (k) of delta using mix_coeffs,
    # resulting in shape (n, m)
    return np.einsum("ijk,k->ij", delta, mix_coeffs), mix_coeffs

__init__(**kwargs)

Initialise the Fixed mix strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to MixStrategy and stored in self.options.

{}
Source code in pikaia/strategies/mix_strategies/fixed_strategy.py
def __init__(self, **kwargs):
    """Initialise the Fixed mix strategy.

    Args:
        **kwargs: Keyword options forwarded to `MixStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

pikaia.strategies.mix_strategies.self_consistent_strategy.SelfConsistentMixStrategy

Bases: MixStrategy

Adaptively updates mixing coefficients based on the mean absolute delta.

This strategy computes a weighted sum of the input delta tensor using the current mixing coefficients, then updates the coefficients in a self-consistent manner based on the mean absolute value of the mixed deltas.

Example

strategy = SelfConsistentMixStrategy() mixed_delta, updated_coeffs = strategy(delta, mix_coeffs)

Source code in pikaia/strategies/mix_strategies/self_consistent_strategy.py
class SelfConsistentMixStrategy(MixStrategy):
    """
    Adaptively updates mixing coefficients based on the mean absolute delta.

    This strategy computes a weighted sum of the input delta tensor using the
    current mixing coefficients, then updates the coefficients in a
    self-consistent manner based on the mean absolute value of the mixed deltas.

    Example:
        strategy = SelfConsistentMixStrategy()
        mixed_delta, updated_coeffs = strategy(delta, mix_coeffs)
    """

    def __init__(self, **kwargs):
        """Initialise the SelfConsistent mix strategy.

        Args:
            **kwargs: Keyword options forwarded to `MixStrategy` and
                stored in ``self.options``.
        """
        super().__init__(**kwargs)

    @property
    def name(self) -> str:
        """The name of the strategy."""
        return "SelfConsistent"

    def __call__(
        self, delta: np.ndarray, mix_coeffs: np.ndarray
    ) -> tuple[np.ndarray, np.ndarray]:
        """
        Apply self-consistent mixing to the input delta tensor and update mixing
            coefficients.

        Args:
            delta (np.ndarray):
                A 3D tensor of shape (n, m, k) representing the deltas to be mixed.
            mix_coeffs (np.ndarray):
                A 1D array of length k containing the current mixing coefficients.

        Returns:
            tuple[np.ndarray, np.ndarray]:
                - The mixed delta array of shape (n, m) after applying the coefficients.
                - The updated mixing coefficients array, adjusted based on the mean
                  absolute delta.

        Notes:
            The mixing coefficients are updated by applying a function to the mean
            absolute value of the mixed deltas, scaled by the number of columns in
            the delta array.
        """
        # Compute per-strategy mean magnitude BEFORE mixing so that strategies with
        # larger deltas genuinely grow their coefficients relative to others.
        # Averaging over both the organism axis (0) and gene axis (1) gives a
        # (k,) vector — one magnitude per strategy.
        per_strategy_mean = np.mean(np.abs(delta), axis=(0, 1))  # (k,)

        # Weighted sum over the last axis (k) of delta using mix_coeffs,
        # resulting in shape (n, m)
        delta = np.einsum("ijk,k->ij", delta, mix_coeffs)

        # Applies delta in form of the central replicator equations
        mix_coeffs = mix_coeffs * (1 + per_strategy_mean * delta.shape[1])
        mix_coeffs /= np.sum(mix_coeffs)

        return delta, mix_coeffs

    @staticmethod
    def update_coeffs_d_matrix(
        D_list: list[np.ndarray | None],
        d_list: list[np.ndarray | None],
        gamma: np.ndarray,
        mix_coeffs: np.ndarray,
    ) -> np.ndarray:
        """Update mixing coefficients for the D-matrix iteration path.

        Replaces the ``(N, M, K)`` tensor magnitude used in ``__call__`` with
        per-strategy D-matrix magnitudes:

        - Bilinear strategy ``s``: ``mean_j(|gamma_j * (D_s @ gamma)_j|)``
        - Linear (balanced org) strategy: ``mean_j(|d_j|)`` — constant

        Args:
            D_list: Per-strategy ``(M, M)`` D matrices or ``None``.
            d_list: Per-strategy ``(M,)`` d-vectors or ``None``.
            gamma: Current gene fitness vector, shape ``(M,)``.
            mix_coeffs: Current mixing coefficients, shape ``(K,)``.

        Returns:
            Updated and renormalized mixing coefficients, shape ``(K,)``.
        """
        M = len(gamma)
        magnitudes = np.array(
            [
                np.mean(np.abs(gamma * (D_s @ gamma)))
                if D_s is not None
                else (np.mean(np.abs(d_s)) if d_s is not None else 0.0)
                for D_s, d_s in zip(D_list, d_list)
            ]
        )
        mix_coeffs = mix_coeffs * (1 + M * magnitudes)
        mix_coeffs /= mix_coeffs.sum()
        return mix_coeffs

Attributes

name: str property

The name of the strategy.

Methods:

__call__(delta: np.ndarray, mix_coeffs: np.ndarray) -> tuple[np.ndarray, np.ndarray]

Apply self-consistent mixing to the input delta tensor and update mixing coefficients.

Parameters:

Name Type Description Default
delta ndarray

A 3D tensor of shape (n, m, k) representing the deltas to be mixed.

required
mix_coeffs ndarray

A 1D array of length k containing the current mixing coefficients.

required

Returns:

Type Description
tuple[ndarray, ndarray]

tuple[np.ndarray, np.ndarray]: - The mixed delta array of shape (n, m) after applying the coefficients. - The updated mixing coefficients array, adjusted based on the mean absolute delta.

Notes

The mixing coefficients are updated by applying a function to the mean absolute value of the mixed deltas, scaled by the number of columns in the delta array.

Source code in pikaia/strategies/mix_strategies/self_consistent_strategy.py
def __call__(
    self, delta: np.ndarray, mix_coeffs: np.ndarray
) -> tuple[np.ndarray, np.ndarray]:
    """
    Apply self-consistent mixing to the input delta tensor and update mixing
        coefficients.

    Args:
        delta (np.ndarray):
            A 3D tensor of shape (n, m, k) representing the deltas to be mixed.
        mix_coeffs (np.ndarray):
            A 1D array of length k containing the current mixing coefficients.

    Returns:
        tuple[np.ndarray, np.ndarray]:
            - The mixed delta array of shape (n, m) after applying the coefficients.
            - The updated mixing coefficients array, adjusted based on the mean
              absolute delta.

    Notes:
        The mixing coefficients are updated by applying a function to the mean
        absolute value of the mixed deltas, scaled by the number of columns in
        the delta array.
    """
    # Compute per-strategy mean magnitude BEFORE mixing so that strategies with
    # larger deltas genuinely grow their coefficients relative to others.
    # Averaging over both the organism axis (0) and gene axis (1) gives a
    # (k,) vector — one magnitude per strategy.
    per_strategy_mean = np.mean(np.abs(delta), axis=(0, 1))  # (k,)

    # Weighted sum over the last axis (k) of delta using mix_coeffs,
    # resulting in shape (n, m)
    delta = np.einsum("ijk,k->ij", delta, mix_coeffs)

    # Applies delta in form of the central replicator equations
    mix_coeffs = mix_coeffs * (1 + per_strategy_mean * delta.shape[1])
    mix_coeffs /= np.sum(mix_coeffs)

    return delta, mix_coeffs

__init__(**kwargs)

Initialise the SelfConsistent mix strategy.

Parameters:

Name Type Description Default
**kwargs

Keyword options forwarded to MixStrategy and stored in self.options.

{}
Source code in pikaia/strategies/mix_strategies/self_consistent_strategy.py
def __init__(self, **kwargs):
    """Initialise the SelfConsistent mix strategy.

    Args:
        **kwargs: Keyword options forwarded to `MixStrategy` and
            stored in ``self.options``.
    """
    super().__init__(**kwargs)

update_coeffs_d_matrix(D_list: list[np.ndarray | None], d_list: list[np.ndarray | None], gamma: np.ndarray, mix_coeffs: np.ndarray) -> np.ndarray staticmethod

Update mixing coefficients for the D-matrix iteration path.

Replaces the (N, M, K) tensor magnitude used in __call__ with per-strategy D-matrix magnitudes:

  • Bilinear strategy s: mean_j(|gamma_j * (D_s @ gamma)_j|)
  • Linear (balanced org) strategy: mean_j(|d_j|) — constant

Parameters:

Name Type Description Default
D_list list[ndarray | None]

Per-strategy (M, M) D matrices or None.

required
d_list list[ndarray | None]

Per-strategy (M,) d-vectors or None.

required
gamma ndarray

Current gene fitness vector, shape (M,).

required
mix_coeffs ndarray

Current mixing coefficients, shape (K,).

required

Returns:

Type Description
ndarray

Updated and renormalized mixing coefficients, shape (K,).

Source code in pikaia/strategies/mix_strategies/self_consistent_strategy.py
@staticmethod
def update_coeffs_d_matrix(
    D_list: list[np.ndarray | None],
    d_list: list[np.ndarray | None],
    gamma: np.ndarray,
    mix_coeffs: np.ndarray,
) -> np.ndarray:
    """Update mixing coefficients for the D-matrix iteration path.

    Replaces the ``(N, M, K)`` tensor magnitude used in ``__call__`` with
    per-strategy D-matrix magnitudes:

    - Bilinear strategy ``s``: ``mean_j(|gamma_j * (D_s @ gamma)_j|)``
    - Linear (balanced org) strategy: ``mean_j(|d_j|)`` — constant

    Args:
        D_list: Per-strategy ``(M, M)`` D matrices or ``None``.
        d_list: Per-strategy ``(M,)`` d-vectors or ``None``.
        gamma: Current gene fitness vector, shape ``(M,)``.
        mix_coeffs: Current mixing coefficients, shape ``(K,)``.

    Returns:
        Updated and renormalized mixing coefficients, shape ``(K,)``.
    """
    M = len(gamma)
    magnitudes = np.array(
        [
            np.mean(np.abs(gamma * (D_s @ gamma)))
            if D_s is not None
            else (np.mean(np.abs(d_s)) if d_s is not None else 0.0)
            for D_s, d_s in zip(D_list, d_list)
        ]
    )
    mix_coeffs = mix_coeffs * (1 + M * magnitudes)
    mix_coeffs /= mix_coeffs.sum()
    return mix_coeffs