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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ jobs:
python -m pip install --upgrade pip
pip install -e .$DEP
pip install pytest-cov coveralls
- name: Code checks
run: |
black --check .
flake8 --config=.flake8 .
mypy cgp
isort --check-only cgp examples test
- name: run tests
run: |
pytest --cov=cgp
Expand Down
84 changes: 84 additions & 0 deletions cgp/cartesian_graph.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import collections
import copy
import math # noqa: F401
import os
import re
from typing import TYPE_CHECKING, Callable, Dict, List, Optional, Set

Expand All @@ -12,6 +13,7 @@
try:
import sympy
from sympy.core import expr as sympy_expr # noqa: F401
from sympy.utilities.codegen import codegen

sympy_available = True
except ModuleNotFoundError:
Expand Down Expand Up @@ -435,3 +437,85 @@ def to_sympy(self, simplify: Optional[bool] = True):
return sympy_exprs[0]
else:
return sympy_exprs

def to_c(self, path):
"""Create a C containing the function described by this graph.

Writes header and source into files to the given path.
Currently only available for a single output node.
Comment thread
jakobj marked this conversation as resolved.

Returns
----------
None
"""

if not sympy_available:
raise ModuleNotFoundError("No sympy module available. Required for exporting C module")

if not self._n_outputs == 1:
raise ValueError("C module export only available for single output node.")

function_name = "rule"
filename = "individual"

sympy_expression = self.to_sympy()

[(filename_source, code_source), (filename_header, code_header)] = codegen(
(function_name, sympy_expression), "C99", filename, header=False, empty=False
)

def replace_func_signature_in_source_and_header_with_full_variable_set(
code_source, code_header, function_name
):
"""Replaces function signature in source and header
with a signature containing all input variables of the graph

Sympy generates function signatures based on the variables used in the expressions,
but our callers expect a fixed signature. Thus we have to replace the signature in
code source and code header with a signature using all of the input variables to the
computational graph to ensure consistency across individuals.

Returns code_source and code_header string with updated function signature

Returns
----------
(str, str):
code_source and code_header signatures
"""

# generate signature with all input variables
arg_string_list = [f"double x_{idx}" for idx in range(self._n_inputs)]
permanent_signature = f"{function_name}(" + ", ".join(arg_string_list) + ")"

# update signature in code_source
c_replace_start_idx = code_source.find(function_name)
c_replace_end_idx = (
code_source.find(")", c_replace_start_idx) + 1
) # +1 offset for to account for ")"
code_source = code_source.replace(
code_source[c_replace_start_idx:c_replace_end_idx], permanent_signature
)

# update signature in code_header
h_replace_start_idx = code_header.find(function_name)
h_replace_end_idx = code_header.find(")", h_replace_start_idx) + 1
code_header = code_header.replace(
code_header[h_replace_start_idx:h_replace_end_idx], permanent_signature
)

return code_source, code_header

# assert function signature consistency - replace signature in header and code
(
code_source,
code_header,
) = replace_func_signature_in_source_and_header_with_full_variable_set(
code_source, code_header, function_name
)

if not os.path.exists(path):
os.makedirs(path)
with open("%s/%s" % (path, filename_source), "w") as f:
f.write(f"{code_source}")
with open("%s/%s" % (path, filename_header), "w") as f:
f.write(f"{code_header}")
45 changes: 45 additions & 0 deletions examples/c_code/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "individual.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>


double target(const double x_0, const double x_1) {
return x_0 * x_1 + 1.0;
}

/* generate a random floating point number from min to max */
double rand_from_to(double min, double max)
{
const double range = (max - min);
double div = RAND_MAX / range;
return min + (rand() / div);
}


double loss() {
int n_samples = 100;
srand(1234); // fix seed

double sum_l2_difference = 0.0;

const double min = -1.0;
const double max = 1.0;

for(int i=0;i<n_samples;i++){
/* generate two random values for x_0, x_1 */
const double x_0_rand=rand_from_to(min, max);
const double x_1_rand=rand_from_to(min, max);

const double target_value=target(x_0_rand, x_1_rand);
const double rule_output=rule(x_0_rand, x_1_rand);

sum_l2_difference += pow(target_value-rule_output, 2);
}
return sum_l2_difference/(double)n_samples;
}

int main(){
printf("%f", loss());
return 0;
}
103 changes: 103 additions & 0 deletions examples/example_evaluate_in_c.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Example for evolutionary regression, with evaluation in C
=========================================================
"""

# The docopt str is added explicitly to ensure compatibility with
# sphinx-gallery.
docopt_str = """
Usage:
example_evaluate_in_c.py

Options:
-h --help
"""
import functools
import pathlib
import subprocess

from docopt import docopt

import cgp

args = docopt(docopt_str)

# %%
# We first define a helper function for compiling the C code. It creates
# object files from the file and main script and creates an executable


def compile_c_code(path):

# assert all necessary files exist
path_file_c = pathlib.Path(f"{path}/individual.c")
path_file_h = pathlib.Path(f"{path}/individual.h")
path_script_o = pathlib.Path(f"{path}/main.o")
assert path_file_c.is_file() & path_file_h.is_file() & path_script_o.is_file()

# compile file with rule
subprocess.check_call(
["gcc", "-c", "-fPIC", f"{path}/individual.c", "-o", f"{path}/individual.o"]
)

# create executable
subprocess.check_call(
["gcc", f"{path}/main.o", f"{path}/individual.o", "-o", f"{path}/main"]
)


# %%
# We define the objective function for the evolution. It creates a
# C module and header from the computational graph. The module
# and the main source file for evaluation are compiled using the above
# helper function. Here the objective obtains the fitness by reading
# the screen output of the C program.

def objective(individual, path):

if not individual.fitness_is_None():
return individual

graph = cgp.CartesianGraph(individual.genome)

graph.to_c(path=path)

compile_c_code(path=path)

result = subprocess.check_output(pathlib.Path().absolute() / f"{path}/individual")
assert result

individual.fitness = -1.0 * float(result)

return individual


# %%
# Next, we set up the evolutionary search. We first define the parameters of the
# genome. We then create a population of individuals with matching genome parameters.


genome_params = {"n_inputs": 2, "primitives": (cgp.Add, cgp.Mul, cgp.ConstantFloat)}

pop = cgp.Population(genome_params=genome_params)

# compile C script
path = "c_code"
assert pathlib.Path(f"{path}/main.c")
subprocess.check_call(["gcc", "-c", "-fPIC", f"{path}/main.c", "-o", f"{path}/main.o"])

# the objective passed to evolve should only accept one argument,
# the individual
obj = functools.partial(objective, path=path)

# %%
# and finally perform the evolution relying on the libraries default
# hyperparameters except that we terminate the evolution as soon as one
# individual has reached fitness zero.
pop = cgp.evolve(objective=obj, pop=pop, termination_fitness=0.0, print_progress=True)

# %%
# After finishing the evolution, we print the final evolved expression and assert it is the target
# expression.
print(pop.champion.to_sympy())
Comment thread
HenrikMettler marked this conversation as resolved.
assert str(pop.champion.to_sympy()) == "x_0*x_1 + 1.0"
98 changes: 98 additions & 0 deletions test/test_cartesian_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -588,3 +588,101 @@ def test_repr(rng, genome_params):
genome.randomize(rng)
# Assert that the CartesianGraph.__repr__ doesn't raise an error
str(cgp.CartesianGraph(genome))


# def test_to_c():
# sympy = pytest.importorskip("sympy")
#
# # test addition, multiplication, single input, constant: f = 2 * x_0 + 1
# primitives = (cgp.Add, cgp.ConstantFloat)
# genome = cgp.Genome(1, 1, 2, 2, primitives, 1)
#
# genome.dna = [
# ID_INPUT_NODE,
# ID_NON_CODING_GENE,
# ID_NON_CODING_GENE,
# 0,
# 0,
# 0,
# 1,
# 0,
# 0,
# 0,
# 1,
# 2,
# 0,
# 0,
# 1,
# ID_OUTPUT_NODE,
# 3,
# ID_NON_CODING_GENE,
# ]
#
# function_name = 'test_function'
# filename = 'test0'
# graph = cgp.CartesianGraph(genome)
# [(filename_c, code_c), (filename_header, code_header)] =
# graph.to_c(function_name=function_name, filename=filename, path='test_cpp')
#
# filename_c_target = 'test0.c'
# assert filename_c == filename_c_target
#
# # todo: rewrite targets to display more readable cpp code; avoid duplicates
# code_c_target = f'#include "{filename}.h"'\
# f'\n#include <math.h>\ndouble {function_name}(double x_0) ' \
# f'{{\n double {function_name}_result;' \
# f'\n {function_name}_result = 2*x_0 + 1.0;\n
# return {function_name}_result;\n}}\n'
#
# assert code_c_target == code_c
#
# filename_header_target = 'test0.h'
# assert filename_header == filename_header_target
#
# code_header_target = f'#ifndef PROJECT__{filename.upper()}__H'\
# f'\n#define PROJECT__{filename.upper()}__H'\
# f'\ndouble {function_name}(double x_0);\n#endif\n'
#
# assert code_header_target == code_header
#
# # test exponential, subtraction, multiple inputs f = x_0^2 - x_1
# primitives = (cgp.Mul, cgp.Sub)
# genome = cgp.Genome(2, 1, 2, 1, primitives, 1)
#
# genome.dna = [
# ID_INPUT_NODE,
# ID_NON_CODING_GENE,
# ID_NON_CODING_GENE,
# ID_INPUT_NODE,
# ID_NON_CODING_GENE,
# ID_NON_CODING_GENE,
# 0, # cgp.Mul
# 0, # x_0
# 0, # x_0
# 1, # cpg.Sub
# 2, # x_0^2
# 1, # x_1
# ID_OUTPUT_NODE,
# 3,
# ID_NON_CODING_GENE,
# ]
#
# function_name = 'test_function'
# filename = 'test1'
# graph = cgp.CartesianGraph(genome)
# [(filename_c, code_c), (filename_header, code_header)] =
# graph.to_c(function_name=function_name, filename=filename, path='test_cpp')
#
# code_c_target = f'#include "{filename}.h"'\
# f'\n#include <math.h>\ndouble {function_name}(double x_0, double x_1) ' \
# f'{{\n double {function_name}_result;' \
# f'\n {function_name}_result = pow(x_0, 2) - x_1;\n
# return {function_name}_result;\n}}\n'
#
# assert code_c_target == code_c
#
# code_header_target = f'#ifndef PROJECT__{filename.upper()}__H'\
# f'\n#define PROJECT__{filename.upper()}__H'\
# f'\ndouble {function_name}(double x_0, double x_1);\n#endif\n'
#
# assert code_header_target == code_header