#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Linopy module for solving lp files with different solvers.
"""

import contextlib
import io
import logging
import os
import re
import subprocess as sub
from pathlib import Path

import numpy as np
import pandas as pd

from linopy.constants import (
    Result,
    Solution,
    SolverStatus,
    Status,
    TerminationCondition,
)

quadratic_solvers = ["gurobi", "xpress", "cplex", "highs"]

available_solvers = []

which = "where" if os.name == "nt" else "which"

# the first available solver will be the default solver
with contextlib.suppress(ImportError):
    import gurobipy

    available_solvers.append("gurobi")
with contextlib.suppress(ImportError):
    import highspy

    available_solvers.append("highs")
if sub.run([which, "glpsol"], stdout=sub.DEVNULL, stderr=sub.STDOUT).returncode == 0:
    available_solvers.append("glpk")

if sub.run([which, "cbc"], stdout=sub.DEVNULL, stderr=sub.STDOUT).returncode == 0:
    available_solvers.append("cbc")

with contextlib.suppress(ImportError):
    import cplex

    available_solvers.append("cplex")
with contextlib.suppress(ImportError):
    import xpress

    available_solvers.append("xpress")
with contextlib.suppress(ImportError):
    import coptpy

    available_solvers.append("copt")
logger = logging.getLogger(__name__)

io_structure = dict(
    lp_file={"gurobi", "xpress", "cbc", "glpk", "cplex"}, blocks={"pips"}
)

def safe_get_solution(status, func):
    """
    Get solution from function call, if status is unknown still try to run it.
    """
    if status.is_ok:
        return func()
    elif status.status == SolverStatus.unknown:
        with contextlib.suppress(Exception):
            logger.warning("Solution status unknown. Trying to parse solution.")
            return func()
    return Solution()

def maybe_adjust_objective_sign(solution, sense, io_api, solver_name):
    if sense == "min":
        return

    if np.isnan(solution.objective):
        return

    if io_api == "mps":
        logger.info(
            "Adjusting objective sign due to switched coefficients in MPS file."
        )
        solution.objective *= -1

def set_int_index(series):
    """
    Convert string index to int index.
    """
    series.index = series.index.str[1:].astype(int)
    return series

def maybe_convert_path(path):
    """
    Convert a pathlib.Path to a string.
    """
    return str(path.resolve()) if isinstance(path, Path) else path

def run_cbc(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the cbc solver.

    The function reads the linear problem file and passes it to the cbc
    solver. If the solution is successful it returns variable solutions
    and constraint dual values. For more information on the solver
    options, run 'cbc' in your shell
    """
    if io_api is not None and io_api not in ["lp", "mps"]:
        logger.warning(
            f"IO setting '{io_api}' not available for cbc solver. "
            "Falling back to `lp`."
        )

    problem_fn = model.to_file(problem_fn)

    # printingOptions is about what goes in solution file
    command = f"cbc -printingOptions all -import {problem_fn} "

    if warmstart_fn:
        command += f"-basisI {warmstart_fn} "

    if solver_options:
        command += (
            " ".join("-" + " ".join([k, str(v)]) for k, v in solver_options.items())
            + " "
        )
    command += f"-solve -solu {solution_fn} "

    if basis_fn:
        command += f"-basisO {basis_fn} "

    if not os.path.exists(solution_fn):
        os.mknod(solution_fn)

    command = command.strip()

    if log_fn is None:
        p = sub.Popen(command.split(" "), stdout=sub.PIPE, stderr=sub.PIPE)
        for line in iter(p.stdout.readline, b""):
            print(line.decode(), end="")
        p.stdout.close()
        p.wait()
    else:
        log_f = open(log_fn, "w")
        p = sub.Popen(command.split(" "), stdout=log_f, stderr=log_f)
        p.wait()

    with open(solution_fn, "r") as f:
        data = f.readline()

    if data.startswith("Optimal - objective value"):
        status = Status.from_termination_condition("optimal")
    elif "Infeasible" in data:
        status = Status.from_termination_condition("infeasible")
    else:
        status = Status(SolverStatus.warning, TerminationCondition.unknown)
    status.legacy_status = data

    def get_solver_solution():
        objective = float(data[len("Optimal - objective value ") :])

        with open(solution_fn, "rb") as f:
            trimmed_sol_fn = re.sub(rb"\*\*\s+", b"", f.read())

        df = pd.read_csv(
            io.BytesIO(trimmed_sol_fn),
            header=None,
            skiprows=[0],
            sep=r"\s+",
            usecols=[1, 2, 3],
            index_col=0,
        )
        variables_b = df.index.str[0] == "x"

        sol = df[variables_b][2].pipe(set_int_index)
        dual = df[~variables_b][3].pipe(set_int_index)
        return Solution(sol, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "cbc")

    return Result(status, solution)

def run_glpk(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the glpk solver.

    This function reads the linear problem file and passes it to the
    glpk
    solver. If the solution is successful it returns variable solutions
    and
    constraint dual values.

    For more information on the glpk solver options, see

    https://kam.mff.cuni.cz/~elias/glpk.pdf
    """
    CONDITION_MAP = {
        "integer optimal": "optimal",
        "undefined": "infeasible_or_unbounded",
    }

    if io_api is not None and io_api not in ["lp", "mps"]:
        logger.warning(
            f"IO setting '{io_api}' not available for glpk solver. "
            "Falling back to `lp`."
        )

    problem_fn = model.to_file(problem_fn)
    suffix = problem_fn.suffix[1:]

    # TODO use --nopresol argument for non-optimal solution output
    command = f"glpsol --{suffix} {problem_fn} --output {solution_fn} "
    if log_fn is not None:
        command += f"--log {log_fn} "
    if warmstart_fn:
        command += f"--ini {warmstart_fn} "
    if basis_fn:
        command += f"-w {basis_fn} "
    if solver_options:
        command += (
            " ".join("--" + " ".join([k, str(v)]) for k, v in solver_options.items())
            + " "
        )
    command = command.strip()

    p = sub.Popen(command.split(" "), stdout=sub.PIPE, stderr=sub.PIPE)
    if log_fn is None:
        for line in iter(p.stdout.readline, b""):
            print(line.decode(), end="")
        p.stdout.close()
        p.wait()
    else:
        p.wait()

    if not os.path.exists(solution_fn):
        status = Status(SolverStatus.warning, TerminationCondition.unknown)
        return Result(status, Solution())

    f = open(solution_fn)

    def read_until_break(f):
        while True:
            line = f.readline()
            if line in ["\n", ""]:
                break
            yield line

    info = io.StringIO("".join(read_until_break(f))[:-2])
    info = pd.read_csv(info, sep=":", index_col=0, header=None)[1]
    condition = info.Status.lower().strip()
    objective = float(re.sub(r"[^0-9\.\+\-e]+", "", info.Objective))

    termination_condition = CONDITION_MAP.get(condition, condition)
    status = Status.from_termination_condition(termination_condition)
    status.legacy_status = condition

    def get_solver_solution() -> Solution:
        dual_ = io.StringIO("".join(read_until_break(f))[:-2])
        dual_ = pd.read_fwf(dual_)[1:].set_index("Row name")
        if "Marginal" in dual_:
            dual = (
                pd.to_numeric(dual_["Marginal"], "coerce").fillna(0).pipe(set_int_index)
            )
        else:
            logger.warning("Dual values of MILP couldn't be parsed")
            dual = pd.Series(dtype=float)

        sol = io.StringIO("".join(read_until_break(f))[:-2])
        sol = (
            pd.read_fwf(sol)[1:]
            .set_index("Column name")["Activity"]
            .astype(float)
            .pipe(set_int_index)
        )
        f.close()
        return Solution(sol, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "glpk")

    return Result(status, solution)

def run_highs(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Highs solver function. Reads a linear problem file and passes it to the
    highs solver. If the solution is feasible the function returns the
    objective, solution and dual constraint variables. Highs must be installed
    for usage. Find the documentation at https://www.maths.ed.ac.uk/hall/HiGHS/

    . The full list of solver options is documented at
    https://www.maths.ed.ac.uk/hall/HiGHS/HighsOptions.set .

    Some exemplary options are:

        * presolve : "choose" by default - "on"/"off" are alternatives.
        * solver :"choose" by default - "simplex"/"ipm" are alternatives.
        * parallel : "choose" by default - "on"/"off" are alternatives.
        * time_limit : inf by default.

    Returns
    -------
    status : string,
        SolverStatus.ok or SolverStatus.warning
    termination_condition : string,
        Contains "optimal", "infeasible",
    variables_sol : series
    constraints_dual : series
    objective : float
    """
    CONDITION_MAP = {}

    if warmstart_fn:
        logger.warning("Warmstart not available with HiGHS solver. Ignore argument.")

    if io_api is None or io_api in ["lp", "mps"]:
        model.to_file(problem_fn)
        h = highspy.Highs()
        h.readModel(maybe_convert_path(problem_fn))
    elif io_api == "direct":
        h = model.to_highspy()
    else:
        raise ValueError(
            "Keyword argument `io_api` has to be one of `lp`, `mps`, `direct` or None"
        )

    if log_fn is None:
        log_fn = model.solver_dir / "highs.log"
    solver_options["log_file"] = maybe_convert_path(log_fn)
    logger.info(f"Log file at {solver_options['log_file']}.")

    for k, v in solver_options.items():
        h.setOptionValue(k, v)

    h.run()

    condition = h.modelStatusToString(h.getModelStatus()).lower()
    termination_condition = CONDITION_MAP.get(condition, condition)
    status = Status.from_termination_condition(termination_condition)
    status.legacy_status = condition

    def get_solver_solution() -> Solution:
        objective = h.getObjectiveValue()
        solution = h.getSolution()

        if io_api == "direct":
            sol = pd.Series(solution.col_value, model.matrices.vlabels, dtype=float)
            dual = pd.Series(solution.row_dual, model.matrices.clabels, dtype=float)
        else:
            sol = pd.Series(solution.col_value, h.getLp().col_names_, dtype=float).pipe(
                set_int_index
            )
            dual = pd.Series(solution.row_dual, h.getLp().row_names_, dtype=float).pipe(
                set_int_index
            )

        return Solution(sol, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "highs")

    return Result(status, solution, h)

def run_cplex(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the cplex solver.

    This function reads the linear problem file and passes it to the cplex
    solver. If the solution is successful it returns variable solutions and
    constraint dual values. Cplex must be installed for using this function.

    Note if you pass additional solver_options, the key can specify deeper
    layered parameters, use a dot as a separator here,
    i.e. `**{'aa.bb.cc' : x}`.
    """
    CONDITION_MAP = {
        "integer optimal solution": "optimal",
        "integer optimal, tolerance": "optimal",
    }

    if io_api is not None and io_api not in ["lp", "mps"]:
        logger.warning(
            f"IO setting '{io_api}' not available for cplex solver. "
            "Falling back to `lp`."
        )

    model.to_file(problem_fn)

    m = cplex.Cplex()

    problem_fn = maybe_convert_path(problem_fn)
    log_fn = maybe_convert_path(log_fn)
    warmstart_fn = maybe_convert_path(warmstart_fn)
    basis_fn = maybe_convert_path(basis_fn)

    if log_fn is not None:
        log_f = open(log_fn, "w")
        m.set_results_stream(log_f)
        m.set_warning_stream(log_f)
        m.set_error_stream(log_f)
        m.set_log_stream(log_f)

    if solver_options is not None:
        for key, value in solver_options.items():
            param = m.parameters
            for key_layer in key.split("."):
                param = getattr(param, key_layer)
            param.set(value)

    m.read(problem_fn)

    if warmstart_fn:
        m.start.read_basis(warmstart_fn)

    is_lp = m.problem_type[m.get_problem_type()] == "LP"

    with contextlib.suppress(cplex.exceptions.errors.CplexSolverError):
        m.solve()
    condition = m.solution.get_status_string()
    termination_condition = CONDITION_MAP.get(condition, condition)
    status = Status.from_termination_condition(termination_condition)
    status.legacy_status = condition

    if log_fn is not None:
        log_f.close()

    def get_solver_solution() -> Solution:
        if basis_fn and is_lp:
            try:
                m.solution.basis.write(basis_fn)
            except cplex.exceptions.errors.CplexSolverError:
                logger.info("No model basis stored")

        objective = m.solution.get_objective_value()

        solution = pd.Series(
            m.solution.get_values(), m.variables.get_names(), dtype=float
        )
        solution = set_int_index(solution)

        if is_lp:
            dual = pd.Series(
                m.solution.get_dual_values(),
                m.linear_constraints.get_names(),
                dtype=float,
            )
            dual = set_int_index(dual)
        else:
            logger.warning("Dual values of MILP couldn't be parsed")
            dual = pd.Series(dtype=float)
        return Solution(solution, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "cplex")

    return Result(status, solution, m)

def run_gurobi(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the gurobi solver.

    This function communicates with gurobi using the gurubipy package.
    """
    # see https://www.gurobi.com/documentation/10.0/refman/optimization_status_codes.html
    CONDITION_MAP = {
        1: "unknown",
        2: "optimal",
        3: "infeasible",
        4: "infeasible_or_unbounded",
        5: "unbounded",
        6: "other",
        7: "iteration_limit",
        8: "terminated_by_limit",
        9: "time_limit",
        10: "optimal",
        11: "user_interrupt",
        12: "other",
        13: "suboptimal",
        14: "unknown",
        15: "terminated_by_limit",
        16: "internal_solver_error",
        17: "internal_solver_error",
    }

    log_fn = maybe_convert_path(log_fn)
    warmstart_fn = maybe_convert_path(warmstart_fn)
    basis_fn = maybe_convert_path(basis_fn)

    with contextlib.ExitStack() as stack:
        if env is None:
            env = stack.enter_context(gurobipy.Env())

        if io_api is None or io_api in ["lp", "mps"]:
            problem_fn = model.to_file(problem_fn)
            problem_fn = maybe_convert_path(problem_fn)
            m = gurobipy.read(problem_fn, env=env)
        elif io_api == "direct":
            problem_fn = None
            m = model.to_gurobipy(env=env)
        else:
            raise ValueError(
                "Keyword argument `io_api` has to be one of `lp`, `mps`, `direct` or None"
            )

        if solver_options is not None:
            for key, value in solver_options.items():
                m.setParam(key, value)
        if log_fn is not None:
            m.setParam("logfile", log_fn)

        if warmstart_fn:
            m.read(warmstart_fn)
        m.optimize()

    if basis_fn:
        try:
            m.write(basis_fn)
        except gurobipy.GurobiError as err:
            logger.info("No model basis stored. Raised error: ", err)

    condition = m.status
    termination_condition = CONDITION_MAP.get(condition, condition)
    status = Status.from_termination_condition(termination_condition)
    status.legacy_status = condition

    def get_solver_solution() -> Solution:
        objective = m.ObjVal

        sol = pd.Series({v.VarName: v.x for v in m.getVars()}, dtype=float)
        sol = set_int_index(sol)

        try:
            dual = pd.Series({c.ConstrName: c.Pi for c in m.getConstrs()}, dtype=float)
            dual = set_int_index(dual)
        except AttributeError:
            logger.warning("Dual values of MILP couldn't be parsed")
            dual = pd.Series(dtype=float)

        return Solution(sol, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "gurobi")

    return Result(status, solution, m)

def run_xpress(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the xpress solver.

    This function reads the linear problem file and passes it to
    the Xpress solver. If the solution is successful it returns
    variable solutions and constraint dual values. The xpress module
    must be installed for using this function.

    For more information on solver options, see
    https://www.fico.com/fico-xpress-optimization/docs/latest/solver/GUID-ACD7E60C-7852-36B7-A78A-CED0EA291CDD.html
    """
    CONDITION_MAP = {
        "lp_optimal": "optimal",
        "mip_optimal": "optimal",
        "lp_infeasible": "infeasible",
        "lp_infeas": "infeasible",
        "mip_infeasible": "infeasible",
        "lp_unbounded": "unbounded",
        "mip_unbounded": "unbounded",
    }

    if io_api is not None and io_api not in ["lp", "mps"]:
        logger.warning(
            f"IO setting '{io_api}' not available for xpress solver. "
            "Falling back to `lp`."
        )

    problem_fn = model.to_file(problem_fn)

    m = xpress.problem()

    problem_fn = maybe_convert_path(problem_fn)
    log_fn = maybe_convert_path(log_fn)
    warmstart_fn = maybe_convert_path(warmstart_fn)
    basis_fn = maybe_convert_path(basis_fn)

    m.read(problem_fn)
    m.setControl(solver_options)

    if log_fn is not None:
        m.setlogfile(log_fn)

    if warmstart_fn:
        m.readbasis(warmstart_fn)

    m.solve()

    if basis_fn:
        try:
            m.writebasis(basis_fn)
        except Exception as err:
            logger.info("No model basis stored. Raised error: ", err)

    condition = m.getProbStatusString()
    termination_condition = CONDITION_MAP.get(condition, condition)
    status = Status.from_termination_condition(termination_condition)
    status.legacy_status = condition

    def get_solver_solution() -> Solution:
        objective = m.getObjVal()

        var = [str(v) for v in m.getVariable()]

        sol = pd.Series(m.getSolution(var), index=var, dtype=float)
        sol = set_int_index(sol)

        try:
            dual = [str(d) for d in m.getConstraint()]
            dual = pd.Series(m.getDual(dual), index=dual, dtype=float)
            dual = set_int_index(dual)
        except xpress.SolverError:
            logger.warning("Dual values of MILP couldn't be parsed")
            dual = pd.Series(dtype=float)

        return Solution(sol, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "xpress")

    return Result(status, solution, m)

def run_copt(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the COPT solver.

    https://guide.coap.online/copt/en-doc/index.html

    For more information on solver options, see
    https://guide.coap.online/copt/en-doc/parameter.html
    """
    # conditions: https://guide.coap.online/copt/en-doc/constant.html#chapconst-solstatus
    CONDITION_MAP = {
        0: "unstarted",
        1: "optimal",
        2: "infeasible",
        3: "unbounded",
        4: "infeasible_or_unbounded",
        5: "numerical",
        6: "node_limit",
        7: "imprecise",
        8: "time_limit",
        9: "unfinished",
        10: "interrupted",
    }

    if io_api is not None and io_api not in ["lp", "mps"]:
        logger.warning(
            f"IO setting '{io_api}' not available for COPT solver. "
            "Falling back to `lp`."
        )

    problem_fn = model.to_file(problem_fn)

    problem_fn = maybe_convert_path(problem_fn)
    log_fn = maybe_convert_path(log_fn)
    warmstart_fn = maybe_convert_path(warmstart_fn)
    basis_fn = maybe_convert_path(basis_fn)

    env = coptpy.Envr()

    m = env.createModel()

    m.read(str(problem_fn))

    if log_fn:
        m.setLogFile(log_fn)

    for k, v in solver_options.items():
        m.setParam(k, v)

    if warmstart_fn:
        m.readBasis(warmstart_fn)

    m.solve()

    if basis_fn and m.HasBasis:
        try:
            m.write(basis_fn)
        except Exception as err:
            logger.info("No model basis stored. Raised error: ", err)

    condition = m.LpStatus if model.type == "LP" else m.MipStatus
    termination_condition = CONDITION_MAP.get(condition, condition)
    status = Status.from_termination_condition(termination_condition)
    status.legacy_status = condition

    def get_solver_solution() -> Solution:
        objective = m.LpObjval if model.type == "LP" else m.BestObj

        sol = pd.Series({v.name: v.x for v in m.getVars()}, dtype=float)
        sol = set_int_index(sol)

        try:
            dual = pd.Series({v.name: v.pi for v in m.getConstrs()}, dtype=float)
            dual = set_int_index(dual)
        except coptpy.CoptError:
            logger.warning("Dual values of MILP couldn't be parsed")
            dual = pd.Series(dtype=float)

        return Solution(sol, dual, objective)

    solution = safe_get_solution(status, get_solver_solution)
    maybe_adjust_objective_sign(solution, model.objective.sense, io_api, "copt")

    env.close()

    return Result(status, solution, m)

def run_pips(
    model,
    io_api=None,
    problem_fn=None,
    solution_fn=None,
    log_fn=None,
    warmstart_fn=None,
    basis_fn=None,
    keep_files=False,
    env=None,
    **solver_options,
):
    """
    Solve a linear problem using the PIPS solver.
    """
    raise NotImplementedError("The PIPS++ solver interface is not yet implemented.")


import os
from datetime import datetime
from omegaconf import OmegaConf
from time import sleep

from picamera import PiCamera
# if os.uname()[1] == 'raspberrypi':
#     from picamera import PiCamera
# else:
#     from mock_picamera.camera import PiCamera

def get_path(base_dir):
    full_path = base_dir + '/' + datetime.now().strftime('%Y/%m/%d/%H')
    os.makedirs(full_path, exist_ok=True)
    return full_path

# print(os.uname()[1])
conf = OmegaConf.load('./config.yaml')
base_dir = '/var/image'
camera = PiCamera(resolution=(conf.resolution.width, conf.resolution.height))
# Set ISO to the desired value
camera.iso = conf.iso
# Rotate the images taken by the camera. Possible value are 0, 90, 180 or 270
camera.rotation = conf.rotation
# Wait for the automatic gain control to settle
sleep(2)
# Now fix the values
camera.shutter_speed = camera.exposure_speed
camera.exposure_mode = 'off'
g = camera.awb_gains
camera.awb_mode = 'off'
camera.awb_gains = g
camera.start_preview()
try:
    for filename in camera.capture_continuous(get_path(base_dir) + '/img{counter:05d}.jpg'):
        print('Captured %s' % filename)
        sleep(conf.wait)  # wait 30 seconds
finally:
    camera.stop_preview()


import math

import numpy as np
import pytest

import cgp
from cgp.cartesian_graph import CartesianGraph
from cgp.genome import ID_INPUT_NODE, ID_NON_CODING_GENE, ID_OUTPUT_NODE

def test_check_dna_consistency():
    params = {"n_inputs": 2, "n_outputs": 1, "n_columns": 1, "n_rows": 1}

    primitives = (cgp.Add,)
    genome = cgp.Genome(
        params["n_inputs"], params["n_outputs"], params["n_columns"], params["n_rows"], primitives,
    )
    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,
        0,
        1,
        ID_OUTPUT_NODE,
        0,
        ID_NON_CODING_GENE,
    ]

    # invalid length
    with pytest.raises(ValueError):
        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,
            ID_OUTPUT_NODE,
            ID_INPUT_NODE,
            ID_OUTPUT_NODE,
            0,
            ID_NON_CODING_GENE,
            0,
        ]

    # invalid function gene for input node
    with pytest.raises(ValueError):
        genome.dna = [
            0,
            ID_NON_CODING_GENE,
            ID_NON_CODING_GENE,
            ID_INPUT_NODE,
            ID_NON_CODING_GENE,
            ID_NON_CODING_GENE,
            0,
            ID_OUTPUT_NODE,
            0,
            ID_OUTPUT_NODE,
            0,
            ID_NON_CODING_GENE,
        ]

    # invalid address gene for input node
    with pytest.raises(ValueError):
        genome.dna = [
            ID_INPUT_NODE,
            0,
            ID_NON_CODING_GENE,
            ID_INPUT_NODE,
            ID_NON_CODING_GENE,
            ID_NON_CODING_GENE,
            0,
            ID_OUTPUT_NODE,
            0,
            ID_OUTPUT_NODE,
            0,
            ID_NON_CODING_GENE,
        ]

    # invalid function gene for hidden node
    with pytest.raises(ValueError):
        genome.dna = [
            ID_INPUT_NODE,
            ID_NON_CODING_GENE,
            ID_NON_CODING_GENE,
            ID_INPUT_NODE,
            ID_NON_CODING_GENE,
            ID_NON_CODING_GENE,
            2,
            0,
            1,
            ID_OUTPUT_NODE,
            0,
            ID_NON_CODING_GENE,
        ]

    # invalid address gene for hidden node
    with pytest.raises(ValueError):
        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,
            2,
            1,
            ID_OUTPUT_NODE,
            0,
            ID_NON_CODING_GENE,
        ]

    # invalid function gene for output node
    with pytest.raises(ValueError):
        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,
            0,
            1,
            0,
            0,
            ID_NON_CODING_GENE,
        ]

    # invalid address gene for input node
    with pytest.raises(ValueError):
        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,
            0,
            1,
            ID_OUTPUT_NODE,
            3,
            ID_NON_CODING_GENE,
        ]

    # invalid inactive address gene for output node
    with pytest.raises(ValueError):
        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,
            0,
            1,
            ID_OUTPUT_NODE,
            0,
            0,
        ]

def test_permissible_addresses(rng):
    params = {"n_inputs": 2, "n_outputs": 1, "n_columns": 4, "n_rows": 3, "levels_back": 2}

    primitives = (cgp.Add,)
    genome = cgp.Genome(
        params["n_inputs"],
        params["n_outputs"],
        params["n_columns"],
        params["n_rows"],
        primitives,
        params["levels_back"],
    )
    genome.randomize(rng)

    for input_idx in range(params["n_inputs"]):
        region_idx = input_idx
        with pytest.raises(AssertionError):
            genome._permissible_addresses(region_idx)

    expected_for_hidden = [
        [0, 1],
        [0, 1, 2, 3, 4],
        [0, 1, 2, 3, 4, 5, 6, 7],
        [0, 1, 5, 6, 7, 8, 9, 10],
    ]

    for column_idx in range(params["n_columns"]):
        region_idx = params["n_inputs"] + params["n_rows"] * column_idx
        assert expected_for_hidden[column_idx] == genome._permissible_addresses(region_idx)

    expected_for_output = list(range(params["n_inputs"] + params["n_rows"] * params["n_columns"]))

    for output_idx in range(params["n_outputs"]):
        region_idx = params["n_inputs"] + params["n_rows"] * params["n_columns"] + output_idx
        assert expected_for_output == genome._permissible_addresses(region_idx)

def test_region_iterators():
    params = {"n_inputs": 2, "n_outputs": 1, "n_columns": 1, "n_rows": 1}

    primitives = (cgp.Add,)
    genome = cgp.Genome(
        params["n_inputs"], params["n_outputs"], params["n_columns"], params["n_rows"], primitives,
    )
    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,
        0,
        1,
        ID_OUTPUT_NODE,
        0,
        ID_NON_CODING_GENE,
    ]

    for region_idx, region in genome.iter_input_regions():
        assert region == [ID_INPUT_NODE, ID_NON_CODING_GENE, ID_NON_CODING_GENE]

    for region_idx, region in genome.iter_hidden_regions():
        assert region == [0, 0, 1]

    for region_idx, region in genome.iter_output_regions():
        assert region == [ID_OUTPUT_NODE, 0, ID_NON_CODING_GENE]

def test_check_levels_back_consistency():
    params = {"n_inputs": 2, "n_outputs": 1, "n_columns": 4, "n_rows": 3, "levels_back": None}

    primitives = (cgp.Add,)

    params["levels_back"] = 0
    with pytest.raises(ValueError):
        cgp.Genome(
            params["n_inputs"],
            params["n_outputs"],
            params["n_columns"],
            params["n_rows"],
            primitives,
            params["levels_back"],
        )

    params["levels_back"] = params["n_columns"] + 1
    with pytest.raises(ValueError):
        cgp.Genome(
            params["n_inputs"],
            params["n_outputs"],
            params["n_columns"],
            params["n_rows"],
            primitives,
            params["levels_back"],
        )

    params["levels_back"] = params["n_columns"] - 1
    cgp.Genome(
        params["n_inputs"],
        params["n_outputs"],
        params["n_columns"],
        params["n_rows"],
        primitives,
        params["levels_back"],
    )

def test_catch_invalid_allele_in_inactive_region():
    primitives = (cgp.ConstantFloat,)
    genome = cgp.Genome(1, 1, 1, 1, primitives)

    # should raise error: ConstantFloat node has no addresses, but silent
    # address gene should still specify valid address
    with pytest.raises(ValueError):
        genome.dna = [ID_INPUT_NODE, ID_NON_CODING_GENE, 0, ID_NON_CODING_GENE, ID_OUTPUT_NODE, 1]

    # correct
    genome.dna = [ID_INPUT_NODE, ID_NON_CODING_GENE, 0, 0, ID_OUTPUT_NODE, 1]

def test_individuals_have_different_genome(population_params, genome_params, ea_params):
    def objective(ind):
        ind.fitness = 1.0
        return ind

    pop = cgp.Population(**population_params, genome_params=genome_params)
    ea = cgp.ea.MuPlusLambda(**ea_params)

    pop._generate_random_parent_population()

    ea.initialize_fitness_parents(pop, objective)

    ea.step(pop, objective)

    for i, parent_i in enumerate(pop._parents):
        for j, parent_j in enumerate(pop._parents):
            if i != j:
                assert parent_i is not parent_j
                assert parent_i.genome is not parent_j.genome
                assert parent_i.genome.dna is not parent_j.genome.dna

def test_is_gene_in_input_region(rng):
    genome = cgp.Genome(2, 1, 2, 1, (cgp.Add,))
    genome.randomize(rng)

    assert genome._is_gene_in_input_region(0)
    assert not genome._is_gene_in_input_region(6)

def test_is_gene_in_hidden_region(rng):
    genome = cgp.Genome(2, 1, 2, 1, (cgp.Add,))
    genome.randomize(rng)

    assert genome._is_gene_in_hidden_region(6)
    assert genome._is_gene_in_hidden_region(9)
    assert not genome._is_gene_in_hidden_region(5)
    assert not genome._is_gene_in_hidden_region(12)

def test_is_gene_in_output_region(rng):
    genome = cgp.Genome(2, 1, 2, 1, (cgp.Add,))
    genome.randomize(rng)

    assert genome._is_gene_in_output_region(12)
    assert not genome._is_gene_in_output_region(11)

def test_mutation_rate(rng, mutation_rate):
    n_inputs = 1
    n_outputs = 1
    n_columns = 4
    n_rows = 3
    genome = cgp.Genome(n_inputs, n_outputs, n_columns, n_rows, (cgp.Add, cgp.Sub))
    genome.randomize(rng)

    def count_n_immutable_genes(n_inputs, n_outputs, n_rows):
        length_per_region = genome.primitives.max_arity + 1  # function gene + address gene
        n_immutable_genes = n_inputs * length_per_region  # none of the input genes are mutable
        n_immutable_genes += n_outputs * (
            length_per_region - 1
        )  # only one gene per output can be mutated
        if n_inputs == 1:
            n_immutable_genes += (
                n_rows * genome.primitives.max_arity
            )  # address gene in the first (hidden) column can't be mutated
            # if only one input node exists
        return n_immutable_genes

    def count_mutations(dna0, dna1):
        n_differences = 0
        for (allele0, allele1) in zip(dna0, dna1):
            if allele0 != allele1:
                n_differences += 1
        return n_differences

    n_immutable_genes = count_n_immutable_genes(n_inputs, n_outputs, n_rows)
    n_mutations_mean_expected = mutation_rate * (len(genome.dna) - n_immutable_genes)
    n_mutations_std_expected = np.sqrt(
        (len(genome.dna) - n_immutable_genes) * mutation_rate * (1 - mutation_rate)
    )

    n = 10000
    n_mutations = []
    for _ in range(n):
        dna_old = genome.dna
        genome.mutate(mutation_rate, rng)
        n_mutations.append(count_mutations(dna_old, genome.dna))

    assert np.mean(n_mutations) == pytest.approx(n_mutations_mean_expected, rel=0.04)
    assert np.std(n_mutations) == pytest.approx(n_mutations_std_expected, rel=0.04)

def test_only_silent_mutations(genome_params, mutation_rate, rng):
    genome = cgp.Genome(**genome_params)
    genome.randomize(rng)

    only_silent_mutations = genome.mutate(mutation_rate=0, rng=rng)
    assert only_silent_mutations is True

    only_silent_mutations = genome.mutate(mutation_rate=1, rng=rng)
    assert not only_silent_mutations

    dna_fixed = [
        ID_INPUT_NODE,
        ID_NON_CODING_GENE,
        ID_NON_CODING_GENE,
        ID_INPUT_NODE,
        ID_NON_CODING_GENE,
        ID_NON_CODING_GENE,
        2,
        1,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        0,
        ID_OUTPUT_NODE,
        2,
        ID_NON_CODING_GENE,
    ]
    genome.dna = dna_fixed
    graph = CartesianGraph(genome)
    active_regions = graph.determine_active_regions()
    length_per_region = genome.primitives.max_arity + 1  # function gene + address gene
    gene_to_be_mutated_non_active = (
        3 * length_per_region
    )  # operator gene of 2nd hidden node, a silent node in this dna configuration

    def select_gene_indices_silent(mutation_rate, rng):
        selected_gene_indices = [gene_to_be_mutated_non_active]
        return selected_gene_indices

    genome._select_gene_indices_for_mutation = (
        select_gene_indices_silent  # monkey patch the selection of indices to select a silent gene
    )
    genome.dna = dna_fixed
    only_silent_mutations = genome.mutate(mutation_rate, rng)
    assert only_silent_mutations is True

    gene_to_be_mutated_active = (
        active_regions[-1] * length_per_region
    )  # operator gene of the 1st active hidden node,
    # should always be mutable and result in a non-silent-mutation

    def select_gene_indices_non_silent(mutation_rate, rng):
        selected_gene_indices = [gene_to_be_mutated_active]
        return selected_gene_indices

    genome._select_gene_indices_for_mutation = select_gene_indices_non_silent
    # monkey patch the selection of indices to select a non-silent gene
    only_silent_mutations = genome.mutate(mutation_rate, rng)
    assert not only_silent_mutations

def test_permissible_values(genome_params):
    n_inputs = 2
    n_outputs = 1
    n_columns = 3
    n_rows = 3
    levels_back = 2
    primitives = (cgp.Add, cgp.Sub, cgp.ConstantFloat)
    genome = cgp.Genome(n_inputs, n_outputs, n_columns, n_rows, primitives, levels_back)

    permissible_function_gene_values = np.arange(len(genome._primitives._primitives))
    permissible_addresses_first_internal_column = np.array([0, 1])
    permissible_addresses_second_internal_column = np.array([0, 1, 2, 3, 4])
    permissible_addresses_third_internal_column = np.array([0, 1, 2, 3, 4, 5, 6, 7])
    permissible_addresses_output = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])

    permissible_values_expected = [
        np.array(ID_INPUT_NODE),
        np.array(ID_NON_CODING_GENE),
        np.array(ID_NON_CODING_GENE),
        np.array(ID_INPUT_NODE),
        np.array(ID_NON_CODING_GENE),
        np.array(ID_NON_CODING_GENE),
        permissible_function_gene_values,
        permissible_addresses_first_internal_column,
        permissible_addresses_first_internal_column,
        permissible_function_gene_values,
        permissible_addresses_first_internal_column,
        permissible_addresses_first_internal_column,
        permissible_function_gene_values,
        permissible_addresses_first_internal_column,
        permissible_addresses_first_internal_column,
        permissible_function_gene_values,
        permissible_addresses_second_internal_column,
        permissible_addresses_second_internal_column,
        permissible_function_gene_values,
        permissible_addresses_second_internal_column,
        permissible_addresses_second_internal_column,
        permissible_function_gene_values,
        permissible_addresses_second_internal_column,
        permissible_addresses_second_internal_column,
        permissible_function_gene_values,
        permissible_addresses_third_internal_column,
        permissible_addresses_third_internal_column,
        permissible_function_gene_values,
        permissible_addresses_third_internal_column,
        permissible_addresses_third_internal_column,
        permissible_function_gene_values,
        permissible_addresses_third_internal_column,
        permissible_addresses_third_internal_column,
        np.array(ID_OUTPUT_NODE),
        permissible_addresses_output,
        np.array(ID_NON_CODING_GENE),
    ]

    for (pv_per_gene_expected, pv_per_gene) in zip(
        permissible_values_expected, genome._permissible_values
    ):
        assert np.all(pv_per_gene_expected == pv_per_gene)

def test_mutate_does_not_reinitialize_parameters(genome_params, rng, mutation_rate):
    genome_params["primitives"] = (cgp.Parameter,)
    genome = cgp.Genome(**genome_params)
    genome.randomize(rng)
    genome._parameter_names_to_values["<p2>"] = math.pi
    parameter_names_to_values_before = genome._parameter_names_to_values.copy()
    genome.mutate(mutation_rate, rng)
    assert genome._parameter_names_to_values["<p2>"] == pytest.approx(
        parameter_names_to_values_before["<p2>"]
    )

def test_genome_reordering_empirically(rng):
    # empirically test that reordering does not change the output function of a genome

    pytest.importorskip("sympy")

    genome_params = {
        "n_inputs": 2,
        "n_outputs": 1,
        "n_columns": 14,
        "n_rows": 1,
        "primitives": (cgp.Mul, cgp.Sub, cgp.Add, cgp.ConstantFloat, cgp.Parameter),
    }

    genome = cgp.Genome(**genome_params)

    # f(x_0, x_1) = x_0 ** 2 - x_1 + 1 + 0.5
    dna_fixed = [
        ID_INPUT_NODE,  # x_0 (address 0)
        ID_NON_CODING_GENE,
        ID_NON_CODING_GENE,
        ID_INPUT_NODE,  # x_1 (address 1)
        ID_NON_CODING_GENE,
        ID_NON_CODING_GENE,
        0,  # Mul ->  x_0^2 (address 2)
        0,  # x
        0,  # x
        1,  # Sub -> x_0^2 - x_1 (address 3)
        2,  # x^2
        1,  # y
        1,  # Sub ->  0 (address 4)
        0,  # x
        0,  # x
        3,  # const -> 1 (address 5)
        2,
        3,
        3,  # const -> 1 (address 6)
        0,
        0,
        4,  # param -> 0.9 (address 7)
        4,
        3,
        4,  # param -> 0.4 (address 8)
        7,
        2,
        2,  # Add -> x_0^2 - x_1 + 1 (address 9)
        3,  # x_0^2 - x_1
        5,  # 1
        1,  # Sub -> x_0^2 - x_1 + 1 - 0.9 (address 10)
        9,  # x_0^2 - x_1 + 1
        7,  # 0.9
        2,  # Add -> x_0^2 - x_1 + 1 - 0.9 + 0.4 (address 11)
        10,  # x_0^2 - x_1 + 1 - 0.9
        8,  # 0.4
        3,  # const (address 12)
        0,
        1,
        3,  # const (address 13)
        0,
        1,
        3,  # const (address 14)
        0,
        1,
        3,  # const (address 15)
        0,
        1,
        ID_OUTPUT_NODE,
        11,
        ID_NON_CODING_GENE,
    ]

    genome.dna = dna_fixed
    genome._parameter_names_to_values["<p7>"] = 0.9
    genome._parameter_names_to_values["<p8>"] = 0.4

    sympy_expression = cgp.CartesianGraph(genome).to_sympy()
    n_reorderings = 100
    for _ in range(n_reorderings):
        genome.reorder(rng)
        new_graph = cgp.CartesianGraph(genome)
        sympy_expression_after_reorder = new_graph.to_sympy()
        assert sympy_expression_after_reorder == sympy_expression

def test_genome_reordering_parameterization_consistency(rng):

    genome_params = {
        "n_inputs": 2,
        "n_outputs": 1,
        "n_columns": 10,
        "n_rows": 2,
        "primitives": (cgp.Mul, cgp.Sub, cgp.Add, cgp.ConstantFloat),
    }

    genome = cgp.Genome(**genome_params)

    with pytest.raises(ValueError):
        genome.reorder(rng)

    genome_params = {
        "n_inputs": 2,
        "n_outputs": 1,
        "n_columns": 10,
        "n_rows": 1,
        "primitives": (cgp.Mul, cgp.Sub, cgp.Add, cgp.ConstantFloat),
        "levels_back": 5,
    }

    genome = cgp.Genome(**genome_params)

    with pytest.raises(ValueError):
        genome.reorder(rng)

def test_parameters_to_numpy_array():

    primitives = (cgp.Parameter,)
    genome = cgp.Genome(1, 1, 2, 1, primitives)
    # [f0(x), f1(x)] = [c1, c2]
    genome.dna = [
        ID_INPUT_NODE,
        ID_NON_CODING_GENE,
        0,
        0,
        0,
        0,
        ID_OUTPUT_NODE,
        1,
    ]

    genome._parameter_names_to_values["<p1>"] = 0.8
    genome._parameter_names_to_values["<p2>"] = 0.9

    # all parameters
    expected_params = np.array([0.8, 0.9])
    expected_params_names = ["<p1>", "<p2>"]
    params, params_names = genome.parameters_to_numpy_array()
    assert np.all(params == pytest.approx(expected_params))
    assert params_names == expected_params_names

    # only parameters of active nodes
    expected_params = np.array([0.8])
    expected_params_names = ["<p1>"]
    params, params_names = genome.parameters_to_numpy_array(only_active_nodes=True)
    assert np.all(params == pytest.approx(expected_params))

def test_update_parameters_from_numpy_array():

    primitives = (cgp.Parameter,)
    genome = cgp.Genome(1, 1, 2, 1, primitives)
    # [f0(x), f1(x)] = [c1, c2]
    genome.dna = [
        ID_INPUT_NODE,
        ID_NON_CODING_GENE,
        0,
        0,
        0,
        0,
        ID_OUTPUT_NODE,
        1,
    ]

    params = np.array([0.8, 0.9])
    params_names = ["<p1>", "<p2>"]
    any_parameter_changed = genome.update_parameters_from_numpy_array(params, params_names)

    assert any_parameter_changed
    assert genome._parameter_names_to_values["<p1>"] == pytest.approx(0.8)
    assert genome._parameter_names_to_values["<p2>"] == pytest.approx(0.9)

    # parameters do not change value
    any_parameter_changed = genome.update_parameters_from_numpy_array(params, params_names)

    assert not any_parameter_changed
    assert genome._parameter_names_to_values["<p1>"] == pytest.approx(0.8)
    assert genome._parameter_names_to_values["<p2>"] == pytest.approx(0.9)

def test_parameters_numpy_array_consistency():
    primitives = (cgp.Parameter,)
    genome = cgp.Genome(1, 1, 2, 1, primitives)
    # [f0(x), f1(x)] = [c1, c2]
    genome.dna = [
        ID_INPUT_NODE,
        ID_NON_CODING_GENE,
        0,
        0,
        0,
        0,
        ID_OUTPUT_NODE,
        1,
    ]

    genome._parameter_names_to_values["<p1>"] = 0.8
    genome._parameter_names_to_values["<p2>"] = 0.9

    genome.update_parameters_from_numpy_array(*genome.parameters_to_numpy_array())

    assert genome._parameter_names_to_values["<p1>"] == pytest.approx(0.8)
    assert genome._parameter_names_to_values["<p2>"] == pytest.approx(0.9)

    genome._parameter_names_to_values["<p1>"] = 1.1
    genome._parameter_names_to_values["<p2>"] = 1.2

    genome.update_parameters_from_numpy_array(
        *genome.parameters_to_numpy_array(only_active_nodes=True)
    )

    assert genome._parameter_names_to_values["<p1>"] == pytest.approx(1.1)
    assert genome._parameter_names_to_values["<p2>"] == pytest.approx(1.2)

def test_ncolumns_zero(rng):

    genome_params = {
        "n_inputs": 1,
        "n_outputs": 1,
        "n_columns": 0,
        "n_rows": 1,
        "primitives": (cgp.Mul, cgp.Sub, cgp.Add, cgp.ConstantFloat),
    }
    genome = cgp.Genome(**genome_params)
    genome.randomize(rng)

    CartesianGraph(genome).to_func()
    CartesianGraph(genome).to_numpy()

def test_uninitialized_genome_throws_exception(genome_params):
    genome = cgp.Genome(**genome_params)
    with pytest.raises(RuntimeError):
        CartesianGraph(genome).to_func()

def test_splice_dna(genome_params, rng):
    genome = cgp.Genome(**genome_params)

    with pytest.raises(RuntimeError):
        genome.splice_dna(new_dna=[1, 2, 3])

    genome.randomize(rng)
    new_dna = []

    with pytest.raises(ValueError):
        [] = genome.splice_dna(new_dna)

    new_dna = [0, 0, 1]

    with pytest.raises(ValueError):
        [] = genome.splice_dna(new_dna, hidden_start_node=-1)
    with pytest.raises(ValueError):
        [] = genome.splice_dna(new_dna, hidden_start_node=9)  # only 9 internal nodes

    address_node = genome.splice_dna(new_dna)
    assert address_node == 2  # 0 + n_inputs

    address_node = genome.splice_dna(new_dna, hidden_start_node=2)
    assert address_node == 4  # 2 + n_inputs

    dna_insert = [0, 0, 1, 1, 1, 1]
    address_node = genome.splice_dna(dna_insert)
    assert address_node == 3  # 1 + n_inputs

    dna_insert = [0, 0, 1, 0]
    with pytest.raises(ValueError):
        [] = genome.splice_dna(dna_insert)

def test_change_address_gene_of_output_node(genome_params, rng):
    genome = cgp.Genome(**genome_params)
    genome.randomize(rng)

    with pytest.raises(ValueError):
        genome.change_address_gene_of_output_node(new_address=-1, output_node_idx=0)
        genome.change_address_gene_of_output_node(new_address=10, output_node_idx=0)
        genome.change_address_gene_of_output_node(new_address=0, output_node_idx=1)

    for new_address in range(9):
        genome.change_address_gene_of_output_node(new_address)
        assert genome.dna[-2] == new_address

    genome_params["n_outputs"] = 2
    genome = cgp.Genome(**genome_params)
    genome.randomize(rng)

    new_address = 2

    genome.change_address_gene_of_output_node(new_address, output_node_idx=0)
    assert genome.dna[-5] == new_address

    genome.change_address_gene_of_output_node(new_address, output_node_idx=1)
    assert genome.dna[-2] == new_address

def test_set_expression_for_output(genome_params, rng):
    sympy = pytest.importorskip("sympy")

    genome = cgp.Genome(**genome_params)
    genome.randomize(rng)

    new_dna = [0, 0, 1]
    genome.set_expression_for_output(new_dna)

    x_0 = sympy.symbols("x_0")
    x_1 = sympy.symbols("x_1")
    assert CartesianGraph(genome).to_sympy() == x_0 + x_1

    new_dna = [1, 0, 1]
    genome.set_expression_for_output(new_dna)
    assert CartesianGraph(genome).to_sympy() == x_0 - x_1

    new_dna = [0, 0, 1, 2, 0, 0, 1, 0, 0, 0, 2, 3]  # x_0+x_1; 1.0; 0; x_0+x_1 + 1.0
    genome.set_expression_for_output(new_dna)
    assert CartesianGraph(genome).to_sympy() == x_0 + x_1 + 1.0


from flask import Flask, request, flash, url_for, redirect, render_template, Blueprint
from flask_sqlalchemy import SQLAlchemy
from flask import Flask, request, flash, url_for, redirect, render_template, Blueprint
from config import Config
from exts import db
from model import Student, Film, Genre, Country, Actor, Director, Language, Production, Country, Genre
import math
import pymysql
from ViewModel.search import Searcher

# from Model import db_search_interface
app = Flask(__name__, template_folder='./templates', static_folder='./templates/static')
app.config.from_object(Config)
db.init_app(app)
db.create_all(app=app)
p = 0

import nltk

# nltk.download()

@app.route('/')
def homepage_films():
    s = Searcher()
    ranking_films = s.search_top_film()
    return render_template('homepage.html', ranking_films=ranking_films)

@app.route('/searchfilmsDefault', methods=['POST', 'GET'])
def searched_films_default():
    if request.method == 'POST':
        search_keywords = request.form
        search_str = ''
        for value in search_keywords.values():
            search_str += value
        s = Searcher()
        search_results = s.default_search(search_str).keys()
        print(search_results)
        film_info = []
        for id in search_results:
            film_info.append(s.search_by_id(id))
            # film_info.append(db_search_interface.film_information(id))
        for film in film_info:
            if len(film[-3]) != 1:
                film[-3] = " / ".join(film[-3])
            if len(film[-6]) != 1:
                film[-6] = " / ".join(film[-6])
            if len(film[5]) == 1:
                film[5] = 'No Records'
            else:
                film[5] = " / ".join(film[5])
        return render_template('search results.html', film_info=film_info)

@app.route('/searchfilmsName', methods=['POST', 'GET'])
def searched_films_name():
    if request.method == 'POST':
        search_keywords = request.form
        search_str = ''
        for value in search_keywords.values():
            search_str += value
        s = Searcher()
        search_results = s.search_by_film_name(search_str).keys()
        film_info = []
        for id in search_results:
            film_info.append(s.search_by_id(id))
        for film in film_info:
            if len(film[-3]) != 1:
                film[-3] = " / ".join(film[-3])
            if len(film[-6]) != 1:
                film[-6] = " / ".join(film[-6])
            if len(film[5]) == 1:
                film[5] = 'No Records'
            else:
                film[5] = " / ".join(film[5])
        return render_template('search results.html', film_info=film_info)

@app.route('/searchfilmsDirector', methods=['POST', 'GET'])
def searched_films_director():
    if request.method == 'POST':
        search_keywords = request.form
        search_str = ''
        for value in search_keywords.values():
            search_str += value
        print(search_str)
        s = Searcher()
        search_results = s.search_by_director(search_str).keys()
        film_info = []
        for id in search_results:
            film_info.append(s.search_by_id(id))
        for film in film_info:
            if len(film[-3]) != 1:
                film[-3] = " / ".join(film[-3])
            if len(film[-6]) != 1:
                film[-6] = " / ".join(film[-6])
            if len(film[5]) == 1:
                film[5] = 'No Records'
            else:
                film[5] = " / ".join(film[5])
        return render_template('search results.html', film_info=film_info)

@app.route('/searchfilmsActor', methods=['POST', 'GET'])
def searched_films_actor():
    if request.method == 'POST':
        search_keywords = request.form
        search_str = ''
        for value in search_keywords.values():
            search_str += value
        print(search_str)
        s = Searcher()
        search_results = s.search_by_actor(search_str).keys()
        film_info = []
        for id in search_results:
            film_info.append(s.search_by_id(id))
        for film in film_info:
            if len(film[-3]) != 1:
                film[-3] = " / ".join(film[-3])
            if len(film[-6]) != 1:
                film[-6] = " / ".join(film[-6])
            if len(film[5]) == 1:
                film[5] = 'No Records'
            else:
                film[5] = " / ".join(film[5])
        return render_template('search results.html', film_info=film_info)

@app.route('/searchfilmsKeywords', methods=['POST', 'GET'])
def searched_films_keywords():
    if request.method == 'POST':
        search_keywords = request.form
        search_str = ''
        for value in search_keywords.values():
            search_str += value
        print(search_str)
        s = Searcher()
        search_results = s.search_by_description(search_str).keys()
        film_info = []
        for id in search_results:
            film_info.append(s.search_by_id(id))
        for film in film_info:
            print(film[-3])
            if len(film[-3]) != 1:
                film[-3] = " / ".join(film[-3])
            if len(film[-6]) != 1:
                film[-6] = " / ".join(film[-6])
            if len(film[5]) == 1:
                film[5] = 'No Records'
            else:
                film[5] = " / ".join(film[5])
        return render_template('search results.html', film_info=film_info)

@app.route('/<genre>')
def show_genre_country_films(genre):
    genre_movies = Film.query.join(Genre, Film.film_id == Genre.film_id).filter_by(genres=genre)
    countryName = 'All'
    deter_number = 0
    for i in genre_movies:
        deter_number += 1
    return render_template('genres details.html', movies=genre_movies[0:18], number=0, countryName=countryName,
                           genre=genre, deter_number=len(genre_movies[0:18]))

@app.route('/<genre>and<countryName>page<number>')
def show_nextpage(number, countryName, genre):
    p = int(number)
    if countryName == 'All':
        deter_number = 0
        genre_movies = Film.query.join(Genre, Film.film_id == Genre.film_id).filter_by(genres=genre)
        for i in genre_movies:
            deter_number += 1
        return render_template('genres details.html', movies=genre_movies[19 * p:19 * p + 18], number=p,
                               countryName=countryName, genre=genre, deter_number=len(genre_movies[19 * p:19 * p + 18]))
    else:
        deter_number = 0
        country_all_movies = (
            Film.query.join(Country, Film.film_id == Country.film_id).filter_by(country=countryName)).join(Genre,
                                                                                Film.film_id == Genre.film_id).filter_by(
            genres=genre)
        for i in country_all_movies:
            deter_number += 1
        return render_template('genres details.html', movies=country_all_movies[19 * p:19 * p + 18], number=p,
                               countryName=countryName, genre=genre,
                               deter_number=len(country_all_movies[19 * p:19 * p + 18]))

@app.route('/<genre>and<countryName>page<number>')
def show_prevpage(number, countryName, genre):
    p = int(number)
    if countryName == 'All':
        genre_movies = Film.query.join(Genre, Film.film_id == Genre.film_id).filter_by(genres=genre)
        return render_template('genres details.html', movies=genre_movies[19 * p:19 * p + 18], number=p,
                               countryName=countryName, genre=genre)
    else:
        country_all_movies = (
            Film.query.join(Country, Film.film_id == Country.film_id).filter_by(country=countryName)).join(Genre,
                                                                                Film.film_id == Genre.film_id).filter_by(
            genres=genre)
        return render_template('genres details.html', movies=country_all_movies[19 * p:19 * p + 18], number=p,
                               countryName=countryName, genre=genre)

@app.route('/<genre>and<countryName>')
def show_country_films(countryName, genre):
    deter_number = 0
    country_all_movies = (
        Film.query.join(Country, Film.film_id == Country.film_id).filter_by(country=countryName)).join(Genre,
                                                                                Film.film_id == Genre.film_id).filter_by(
        genres=genre)
    for i in country_all_movies:
        deter_number += 1
    return render_template('genres details.html', movies=country_all_movies[0:18], number=0, countryName=countryName,
                           genre=genre, deter_number=len(country_all_movies[0:18]))

@app.route('/moviedetail<film_id>')
def show_movie_detail(film_id):
    film_actors = ''
    film_genres = ''
    film_country = ''
    film_languages = ''
    film_productions = ''
    detail_film = Film.query.filter_by(film_id=film_id).first()

    genres = Genre.query.filter_by(film_id=film_id)
    for genre in genres:
        film_genres = film_genres + genre.genres + ','
    film_genres = film_genres[:-1]
    if film_genres is '':
        film_genres = 'No records'

    countrys = Country.query.filter_by(film_id=film_id)
    for country in countrys:
        film_country = film_country + country.country + ','
    film_country = film_country[:-1]
    if film_country is '':
        film_actors = 'No records'

    actors = Actor.query.filter_by(film_id=film_id)
    for actor in actors:
        film_actors = film_actors + actor.actor_name + ','
    film_actors = film_actors[:-1]
    if film_actors is '':
        film_actors = 'No records'

    film_director = Director.query.filter_by(film_id=film_id).first()

    languages = Language.query.filter_by(film_id=film_id)
    for language in languages:
        film_languages = film_languages + language.language + ','
    film_languages = film_languages[:-1]
    if film_languages is '':
        film_languages = 'No records'

    productions = Production.query.filter_by(film_id=film_id)
    for production in productions:
        film_productions = film_productions + production.production_name + ','
    film_productions = film_productions[:-1]
    if film_productions is '':
        film_productions = 'No records'

    keywods = detail_film.keywords.split('_keyword_spilt_sign_')
    film_keywords = ' / '.join(keywods)
    if film_keywords == ' / ':
        film_keywords = 'No Records'
    film_photos = detail_film.photos.split('_photo_url_spilt_sign_')
    for i in range(len(film_photos)):
        film_photos[i] = 'https://www.imdb.com' + film_photos[i]
    photo_num = len(film_photos)
    carousel_num = math.ceil(photo_num / 3)

    return render_template('movie details.html', film_id=film_id, detail_film=detail_film,
                           film_genres=film_genres, film_country=film_country, film_actors=film_actors,
                           film_director=film_director, film_languages=film_languages,
                           film_productions=film_productions,
                           film_keywords=film_keywords, film_photos=film_photos, photo_num=photo_num,
                           carousel_num=carousel_num)

if __name__ == '__main__':
    app.run()


import uuid
from deck import Card

class Hand:
    def __init__(self, cards=None):
        self.cards =  cards if cards is not None else []

    # list of cards is used for the evaluation of the hand, this will format for that process
    def toEvalList(self):
        '''
        Get cards as list of strings for evaluation
        '''
        result = []
        for card in self.cards:
            result.append(card.rank+card.suit)
        return result

    def setHandFromEvalList(self, evalList):
        self.cards = []
        for cardString in evalList:
            self.cards.append(Card.stringToCard(cardString))

class PlayerAction():
    NONE = 'none'
    CALL_CHECK_RAISE = 'call, check or raise'
    FOLD = 'fold'

class Player:
    def __init__(self, name="A Player with no name", hand = None, playerId = None,  chips=100, dealer=False, folded = False, currentBet = 0, currentAction=PlayerAction.NONE, turn=False, turnStartTime=None, isInformedHandComplete=False):
        self.hand = hand if hand is not None else Hand()
        self.name = name
        self.playerId=playerId if playerId is not None else str(uuid.uuid4())
        self.chips = chips
        self.dealer = dealer
        self.folded = folded
        self.currentBet = currentBet
        self.currentAction = currentAction
        self.turn = turn
        self.turnStartTime = turnStartTime
        self.isInformedHandComplete = isInformedHandComplete

    def showHand(self):
        myCards = self.name + ' : '
        if len(self.hand.cards) == 0:
            myCards += " No cards"
        else:
            for card in self.hand.cards[:-1]:
                myCards  += str(card)
                myCards  +=  ", "
            myCards  += str(self.hand.cards[-1])
        return myCards

    def __str__(self):
        result = 'Player: {0} ({1}), {2} chip(s).'.format(self.name, self.playerId, self.chips)
        if self.dealer:
            result += ' DEALER '
        return result


import os
import numpy as np
from scipy import signal
from torchvision import datasets, transforms
import itertools

# Network configuration
neurons   = (
    (28, 0, 5, 1, (1, 10)),  # (dimension, padding, kernel, stride, (input channels, output channels))
    (24, 0, 5, 1, (10, 20)),
    8000,
    256,
    10)
layer_cnt = len(neurons) - 1
tstep_cnt = 20
image_cnt = 60000
epoch_cnt = 1

train_ntest   = True
debug         = True
debug_period  = 5000
mempot_thres  = 1
learning_rate = 5e-4 / tstep_cnt
decay         = 1
desire_thres  = {"hidden": 0.1, "output": 0.1}

# Network parameters
weights = [np.empty(0)] * layer_cnt
mempot  = [np.empty(0)] * (layer_cnt + 1)
spikes  = [np.empty(0)] * (layer_cnt + 1)
traces  = [np.empty(0)] * layer_cnt
desire  = [np.empty(0)] * layer_cnt

np.random.seed(0)
for layer in range(layer_cnt):
    if train_ntest:
        # Kaiming initialization
        if type(neurons[layer]) == tuple:
            weights[layer] = np.random.randn(neurons[layer][4][1], neurons[layer][4][0],
                             neurons[layer][2], neurons[layer][2]).astype(np.float32) * \
                             np.sqrt(2 / (neurons[layer][4][0] * neurons[layer][2] ** 2))
        elif type(neurons[layer]) == int:
            weights[layer] = np.random.randn(neurons[layer], neurons[layer+1]).astype(np.float32) * \
                             np.sqrt(2 / neurons[layer])
    else:
        weights[layer] = np.load(f"model/weights_{layer}.npy")

# Iterate over MNIST images
dataset = datasets.MNIST("data", download=True, train=train_ntest, transform=transforms.ToTensor())
logfile = open(os.path.splitext(os.path.basename(__file__))[0] + ("_train" if train_ntest else "_test") + ".log", "w")
for epoch in range(epoch_cnt):
    if debug: print(f"Epoch: {epoch}")

    for (image_idx, (image, label)) in enumerate(itertools.islice(dataset, image_cnt)):
        if debug: print(f"Image {image_idx}: {label}")
        if image_idx % debug_period == 0:
            results = np.zeros(2, dtype=np.int)

        # Reset spikes and membrane potentials
        for layer in range(layer_cnt + 1):
            if type(neurons[layer]) == tuple:
                mempot[layer] = np.zeros((neurons[layer][4][0], neurons[layer][0], neurons[layer][0]), dtype=np.float32)
                spikes[layer] = np.zeros((tstep_cnt + 1, neurons[layer][4][0], neurons[layer][0], neurons[layer][0]), dtype=np.bool)
                if layer < layer_cnt:
                    traces[layer] = np.zeros((tstep_cnt + 1, neurons[layer][4][0], neurons[layer][0], neurons[layer][0]), dtype=np.float32)
                if layer > 0:
                    desire[layer-1] = np.zeros((neurons[layer][4][0], neurons[layer][0], neurons[layer][0], 2), dtype=np.bool)
            elif type(neurons[layer]) == int:
                mempot[layer] = np.zeros(neurons[layer], dtype=np.float32)
                spikes[layer] = np.zeros((tstep_cnt + 1, neurons[layer]), dtype=np.bool)
                if layer < layer_cnt:
                    traces[layer] = np.zeros((tstep_cnt + 1, neurons[layer]), dtype=np.float32)
                if layer > 0:
                    desire[layer-1] = np.zeros((neurons[layer], 2), dtype=np.bool)

        # Input image and forward pass
        image = np.array(image, dtype=np.float32)

        for tstep in range(tstep_cnt):
            # Generate input spike train from image
            for chn_in in range(neurons[0][4][0]):
                for neu_row in range(neurons[0][0]):
                    for neu_col in range(neurons[0][0]):
                        mempot[0][chn_in, neu_row, neu_col] += image[chn_in, neu_row, neu_col]
                        if mempot[0][chn_in, neu_row, neu_col] >= mempot_thres:
                            spikes[0][tstep][chn_in, neu_row, neu_col] = True
                            mempot[0][chn_in, neu_row, neu_col] -= mempot_thres

            # Propagate spikes forward
            for layer in range(layer_cnt):
                if type(neurons[layer]) == tuple:
                    dim_out = int((neurons[layer][0] + 2 * neurons[layer][1] - neurons[layer][2]) / neurons[layer][3] + 1)
                    mempot_temp = mempot[layer+1] if type(neurons[layer+1]) == tuple else mempot[layer+1].reshape((neurons[layer][4][1], dim_out, dim_out))
                    spikes_temp = np.zeros((neurons[layer][4][1], dim_out, dim_out), dtype=np.bool)

                    for chn_out in range(neurons[layer][4][1]):
                        # Update membrane potential
                        for chn_in in range(neurons[layer][4][0]):
                            mempot_temp[chn_out] += signal.correlate2d(spikes[layer][tstep, chn_in], weights[layer][chn_out][chn_in], mode="valid")

                        # Calculate output spikes and decay membrane potential
                        for neu_row in range(dim_out):
                            for neu_col in range(dim_out):
                                if mempot_temp[chn_out, neu_row, neu_col] >= mempot_thres:
                                    spikes_temp[chn_out, neu_row, neu_col] = True
                                    mempot_temp[chn_out, neu_row, neu_col] -= mempot_thres
                                else:
                                    mempot_old = mempot_temp[chn_out, neu_row, neu_col]
                                    mempot_temp[chn_out, neu_row, neu_col] = ((mempot_old * 2 ** decay) - mempot_old) / 2 ** decay

                    if type(neurons[layer+1]) == tuple:
                        mempot[layer+1] = mempot_temp
                        spikes[layer+1][tstep+1] = spikes_temp
                    else:
                        mempot[layer+1] = mempot_temp.flatten()
                        spikes[layer+1][tstep+1] = spikes_temp.flatten()

                    # Update spike traces
                    for chn_in in range(neurons[layer][4][0]):
                        for neu_row in range(neurons[layer][0]):
                            for neu_col in range(neurons[layer][0]):
                                trace = traces[layer][tstep, chn_in, neu_row, neu_col]
                                traces[layer][tstep+1, chn_in, neu_row, neu_col] = ((trace * 2 ** decay) - trace) / 2 ** decay

                                if spikes[layer][tstep, chn_in, neu_row, neu_col]:
                                    traces[layer][tstep+1, chn_in, neu_row, neu_col] += 1

                elif type(neurons[layer]) == int:
                    for neu_post in range(neurons[layer+1]):
                        # Update membrane potential
                        for neu_pre in range(neurons[layer]):
                            if spikes[layer][tstep][neu_pre]:
                                mempot[layer+1][neu_post] += weights[layer][neu_pre][neu_post]

                        # Calculate output spikes and decay membrane potential
                        if mempot[layer+1][neu_post] >= mempot_thres:
                            spikes[layer+1][tstep+1][neu_post] = True
                            mempot[layer+1][neu_post] -= mempot_thres
                        else:
                            mempot_old = mempot[layer+1][neu_post]
                            mempot[layer+1][neu_post] = ((mempot_old * 2 ** decay) - mempot_old) / 2 ** decay

                    # Update spike traces
                    for neu_pre in range(neurons[layer]):
                        trace = traces[layer][tstep][neu_pre]
                        traces[layer][tstep+1][neu_pre] = ((trace * 2 ** decay) - trace) / 2 ** decay

                        if spikes[layer][tstep][neu_pre]:
                            traces[layer][tstep+1][neu_pre] += 1

        if debug:
            print(np.sum(spikes[-1], axis=0))

        # Compare output with label
        spikes_out = np.sum(spikes[-1], axis=0)
        spikes_max = np.max(spikes_out)
        spikes_cnt = np.bincount(spikes_out)
        if spikes_out[label] == spikes_max and spikes_cnt[spikes_max] == 1:
            results[0] += 1
        else:
            results[1] += 1

        if image_idx % debug_period == debug_period - 1:
            accuracy = results[0] / results.sum() * 100
            print("Epoch {:2d} Image {:5d}: {:.2f}%".format(epoch, image_idx, accuracy), file=logfile)
            logfile.flush()

        # Training portion
        if train_ntest:
            # Backpropagate desire
            for layer in [layer_cnt]:
                # Output layer
                for neu_pre in range(neurons[layer]):
                    if neu_pre == label:
                        desire[layer-1][neu_pre][0] = (1 - np.sum(spikes[layer][:,neu_pre]) / tstep_cnt) > desire_thres["output"]
                        desire[layer-1][neu_pre][1] = True
                    else:
                        desire[layer-1][neu_pre][0] = (np.sum(spikes[layer][:,neu_pre]) / tstep_cnt) > desire_thres["output"]
                        desire[layer-1][neu_pre][1] = False

            for layer in range(layer_cnt - 1, 0, -1):
                # Hidden layers
                if type(neurons[layer]) == int:
                    for neu_pre in range(neurons[layer]):
                        desire_sum = 0
                        for neu_post in range(neurons[layer+1]):
                            if desire[layer][neu_post][0]:
                                if desire[layer][neu_post][1]:
                                    target_mult = 1 - np.sum(spikes[layer+1][:,neu_post]) / tstep_cnt
                                    desire_sum += weights[layer][neu_pre][neu_post] * target_mult
                                else:
                                    target_mult = np.sum(spikes[layer+1][:,neu_post]) / tstep_cnt
                                    desire_sum -= weights[layer][neu_pre][neu_post] * target_mult
                        desire[layer-1][neu_pre][0] = np.abs(desire_sum) >= desire_thres["hidden"]
                        desire[layer-1][neu_pre][1] = desire_sum > 0

                elif type(neurons[layer]) == tuple:
                    if type(neurons[layer+1]) == int:
                        dim_out = int((neurons[layer][0] + 2 * neurons[layer][1] - neurons[layer][2]) / neurons[layer][3] + 1)
                        desire_temp = desire[layer].reshape((neurons[layer][4][1], dim_out, dim_out, 2))
                        spikes_temp = spikes[layer+1].reshape((tstep_cnt + 1, neurons[layer][4][1], dim_out, dim_out))
                    else:
                        desire_temp = desire[layer]
                        spikes_temp = spikes[layer+1]

                    for chn_in in range(neurons[layer][4][0]):
                        desire_sum = np.zeros_like(desire[layer-1][chn_in,:,:,0], dtype=np.float32)
                        for chn_out in range(neurons[layer][4][1]):
                            sign = desire_temp[chn_out,:,:,0] * (desire_temp[chn_out,:,:,1] * 2 - 1)
                            spikes_sum = np.sum(spikes_temp[:,chn_out], axis=0)
                            error = np.abs(spikes_sum / tstep_cnt - desire_temp[chn_out,:,:,1].astype(np.float32))
                            desire_sum += signal.convolve2d(np.pad(sign * error, neurons[layer][2] - 1), weights[layer][chn_out, chn_in], mode="valid")
                        desire[layer-1][chn_in,:,:,0] = np.abs(desire_sum) >= desire_thres["hidden"]
                        desire[layer-1][chn_in,:,:,1] = desire_sum > 0

            # Update weights
            for tstep in range(tstep_cnt):
                for layer in range(layer_cnt):
                    if type(neurons[layer]) == tuple:
                        dim_out = int((neurons[layer][0] + 2 * neurons[layer][1] - neurons[layer][2]) / neurons[layer][3] + 1)
                        if type(neurons[layer+1]) == int:
                            desire_temp = desire[layer].reshape((neurons[layer][4][1], dim_out, dim_out, 2))
                            spikes_temp = spikes[layer+1].reshape((tstep_cnt + 1, neurons[layer][4][1], dim_out, dim_out))
                        else:
                            desire_temp = desire[layer]
                            spikes_temp = spikes[layer+1]

                        for chn_in in range(neurons[layer][4][0]):
                            for chn_out in range(neurons[layer][4][1]):
                                sign = desire_temp[chn_out,:,:,0] * (desire_temp[chn_out,:,:,1] * 2 - 1)
                                update = signal.correlate2d(traces[layer][tstep+1, chn_in], sign * spikes_temp[tstep+1, chn_out], mode="valid")
                                weights[layer][chn_out, chn_in] += learning_rate * update / dim_out ** 2

                    elif type(neurons[layer]) == int:
                        for neu_post in range(neurons[layer+1]):
                            if spikes[layer+1][tstep+1][neu_post] and desire[layer][neu_post][0]:
                                for neu_pre in range(neurons[layer]):
                                    update = learning_rate * traces[layer][tstep+1][neu_pre]
                                    weights[layer][neu_pre][neu_post] += (1 if desire[layer][neu_post][1] else -1) * update

    # Save network model
    if train_ntest:
        if not os.path.exists(f"model_{epoch}"):
            os.makedirs(f"model_{epoch}")
        for layer in range(layer_cnt):
            np.save(f"model_{epoch}/weights_{layer}.npy", weights[layer])

logfile.close()


#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
First run split_dataset.py to split dataset in train|valid|test parts.
Then run this script.
"""

from __future__ import print_function, division
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
from torch.utils.data import DataLoader, random_split
import torchvision
from torchvision import datasets, models, transforms
from torchvision.datasets import ImageFolder
from torchvision.transforms import ToTensor

import math
import numpy as np
import matplotlib.pyplot as plt
import time
import os
import sys
import copy
import progressbar
import pickle
from nn_models import get_torchvision_model, get_resnet18_classifier
from nn_models import CNN_Net
import data_factory
from data_factory import ObjDetDataset
from loss_function import bboxes_loss
SHOW_BAR = False

import settings
model_name = settings.model_name
num_colors = settings.num_colors
image_width = settings.image_width
batch_size = settings.batch_size # = 32
data_path = settings.data_path
data_parts = ['train', 'valid']
pickle_dataset_path = "dataset_{}.pickle".format(image_width)

#start_lr = 0.00128
#num_epochs = 150; start_lr = 0.00256; step_size = 20
#num_epochs = 25; start_lr = 0.00256; step_size = 4
#num_epochs = 25; start_lr = 0.00128; step_size = 5
num_epochs = 12; start_lr = 0.00256; step_size = 4

if os.path.exists(pickle_dataset_path):
    with open(pickle_dataset_path, 'rb') as fp:
        dataset = pickle.load(fp)
        print("Dataset has been loaded succesfully from {}".format(pickle_dataset_path))
else:
    #data_path = "/data/5_patexia/3_scanned/6_dataset_v2/7_ADS_obj_det"
    #data_path = "/data/5_patexia/3_scanned/6_dataset_v2/9_ADS_obj_get_generated"
    #data_path = "/storage/work/cv/obj_det/ads_dataset/9_ADS_obj_get_generated"

    dataset = {}
    dataset['train'] = ObjDetDataset(
        #images_dir="../dataset/train/",
        #json_dir="../dataset/json_annotations/train/",
        images_dir=os.path.join(data_path, "train"),
        json_dir=os.path.join(data_path, "json_annotations/train"),
        data_type="train",
        num_colors=num_colors,
        image_width=image_width,
    )
    dataset['valid'] = ObjDetDataset(
        #images_dir="../dataset/valid/",
        #json_dir="../dataset/json_annotations/valid/",
        images_dir=os.path.join(data_path, "valid"),
        json_dir=os.path.join(data_path, "json_annotations/valid"),
        data_type="valid",
        num_colors=num_colors,
        image_width=image_width,
    )

    if len(dataset['train']) > 0 and len(dataset['valid']) > 0:
        with open(pickle_dataset_path, 'wb') as fp:
            pickle.dump(dataset, fp)
            print("Dataset has been saved in {}".format(pickle_dataset_path))
    else:
        raise Exception("Dataset is empty.")

dataset_sizes = {'train': len(dataset['train']), 'valid': len(dataset['valid']) }
num_batch = dict()
num_batch['train'] = math.ceil(dataset_sizes['train'] / batch_size)
num_batch['valid'] = math.ceil(dataset_sizes['valid'] / batch_size)
print('train_num_batch:', num_batch['train'])
print('valid_num_batch:', num_batch['valid'])
dataloaders = {
    'train': DataLoader(dataset['train'], batch_size=batch_size, shuffle=True),
    'valid': DataLoader(dataset['valid'], batch_size=batch_size, shuffle=True)
}

#print(data_parts)
#print('train size:', dataset_sizes['train'])
#print('valid size:', dataset_sizes['valid'])
#print('classes:', class_names)
#print('class_to_idx:', dataset.class_to_idx)

#for i, (x, y) in enumerate(dataloaders['valid']):
#    print(x) # image
#    print(i, y) # image label

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    since = time.time()
    history = dict()

    best_model_wts = copy.deepcopy(model.state_dict())
    best_acc = 0.0

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)
        history[epoch] = {}

        # Each epoch has a training and validation phase
        for phase in data_parts:
            if phase == 'train':
                scheduler.step()
                model.train()  # Set model to training mode
            else:
                model.eval()   # Set model to evaluate mode

            running_loss = 0.0
            running_corrects = 0

            if SHOW_BAR:
                bar = progressbar.ProgressBar(maxval=num_batch[phase]).start()

            # Iterate over data.
            for i_batch, (inputs, labels) in enumerate(dataloaders[phase]):
                inputs = inputs.to(device)
                labels = labels.to(device)

                if SHOW_BAR:
                    bar.update(i_batch)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    _, preds = torch.max(outputs, 1)
                    loss = criterion(outputs, labels)

                    #print("outputs:", outputs)
                    #print("labels:", labels) # labels: tensor([1, 2, 2, 2])
                    #print("loss:", loss) # tensor(1.1003, grad_fn=<NllLossBackward>)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                #print('preds: ', preds)
                #print('labels:', labels.data)
                #print('match: ', int(torch.sum(preds == labels.data)))

                running_loss += loss.item() * inputs.size(0)

            if SHOW_BAR:
                bar.finish()

            epoch_loss = running_loss / dataset_sizes[phase]

            print('{} loss: {:.4f}'.format(phase, epoch_loss))
            history[epoch][phase] = {'loss': epoch_loss}
            if phase == 'valid':
                ln_rate = scheduler.get_last_lr()
                history[epoch]['ln_rate'] = ln_rate
                print("ln_rate: {}".format(ln_rate))

            # deep copy the model
            #if phase == 'valid' and epoch_acc > best_acc:
            #    best_acc = epoch_acc
            #    best_model_wts = copy.deepcopy(model.state_dict())

        print()

    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))
    #print('Best val Acc: {:4f}'.format(best_acc))

    # load best model weights
    #model.load_state_dict(best_model_wts)

    print("\nEpoch ln_rate train valid")
    for epoch in range(num_epochs):
        train_loss = history[epoch]['train']['loss']
        valid_loss = history[epoch]['valid']['loss']
        ln_rate = history[epoch]['ln_rate']
        print('{}: {} | {:.4f} {:.4f}'.format(epoch, ln_rate, train_loss, valid_loss))

    return model

if __name__ == "__main__":

    if model_name == "resnet":
        #model = get_resnet18_classifier(output_size=5, pretrained=False)
        model = get_torchvision_model(output_size=5, pretrained=True)
    elif model_name == "custom":
        model = CNN_Net(output_size=5, num_input_channels=1)

    device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
    model = model.to(device)

    #criterion = nn.CrossEntropyLoss()
    criterion = bboxes_loss

    # Observe that all parameters are being optimized
    #optimizer_ft = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
    optimizer_ft = optim.SGD(model.parameters(), lr=start_lr, momentum=0.9)

    # Decay LR by a factor of 0.1 every 7 epochs
    #exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=3, gamma=0.1)
    exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=step_size, gamma=0.5)

    model = train_model(model, criterion, optimizer_ft, exp_lr_scheduler,
        num_epochs=num_epochs)

    # save model
    torch.save(model.state_dict(), "model_state.pt")
    torch.save(model, "model_full.pt", _use_new_zipfile_serialization=False)

# Results:

# 19: 0.0474 0.0354

# resnet18:
# 18: 0.0098 0.0477
# 19: 0.0099 0.0474
# --
# 19: 0.0097 0.0491

# custom:
#2: 0.0143 0.0057
#3: 0.0131 0.0053

"""

18: 0.0335 0.1952
19: 0.0407 0.1900

Epoch Train Valid
0: 0.1269 0.1348
1: 0.0517 0.0342
2: 0.0310 0.0498
3: 0.0301 0.0281
4: 0.0198 0.0147

lr=0.003
Epoch Train Valid
0: 0.0974 0.0842
1: 0.0533 0.0288
2: 0.0357 0.0469
3: 0.0199 0.0417
4: 0.0156 0.0213
5: 0.0128 0.0191
6: 0.0076 0.0111
7: 0.0069 0.0120

Epoch Train Valid
0: 0.1968 0.0760
1: 0.0462 0.0407
2: 0.0369 0.1018
3: 0.0233 0.0427
4: 0.0203 0.0203
5: 0.0125 0.0291
6: 0.0076 0.0107
7: 0.0055 0.0109

Epoch Train Valid
0: 0.0711 0.0152
1: 0.0160 0.0091
2: 0.0080 0.0089
3: 0.0068 0.0090
4: 0.0062 0.0095
5: 0.0059 0.0095
6: 0.0061 0.0087
7: 0.0059 0.0088

Epoch Train Valid
0: 0.0504 0.0171
1: 0.0145 0.0203
2: 0.0079 0.0088

Epoch Train Valid
0: 0.0401 0.0170
1: 0.0185 0.0108
2: 0.0084 0.0070
3: 0.0077 0.0076
4: 0.0076 0.0072

Mobile
Epoch Train Valid
17: 0.0403 0.0198
18: 0.0431 0.0197
19: 0.0366 0.0196

17: 0.0149 0.0039
18: 0.0153 0.0041
19: 0.0157 0.0041

==============

new data

Epoch Train Valid
0: 0.2733 0.1828
1: 0.1991 0.3840
2: 0.1533 0.0404
18: 0.0362 0.0189
19: 0.0342 0.0175

0: 1.3119 0.1615
1: 0.4458 0.5909
18: 0.0647 0.0036
19: 0.0645 0.0036

----------
with augmentation:
custom:
Epoch 9/9
----------
train loss: 0.0398
valid loss: 0.0352
ln_rate: [0.0001]

mobile:

Training complete in 32m 37s

Epoch Train Valid
0: 0.1080 0.0357
1: 0.0334 0.0179
2: 0.0262 0.0238
3: 0.0174 0.0098
4: 0.0168 0.0094

"""


from core.afdet_dataset import Dataset
from core.models.afdet import AFDet
from core.modified_losses import AFDetLoss

from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
import torch
import os
import time
from tqdm import tqdm

class AFDetAgent():
    def __init__(self, config):
        self.config = config
        self.device = config["device"]
        self.prev_val_loss = 1e6

    def prepare_loaders(self):
        config = self.config["data"]
        aug_config = self.config["augmentation"]
        train_dataset = Dataset( self.config["train"]["data"], config, aug_config, "train")
        self.train_loader = DataLoader(train_dataset, shuffle=True, batch_size=self.config["train"]["batch_size"], num_workers=4, pin_memory=True)

        val_dataset = Dataset(self.config["val"]["data"], config, aug_config, "sth")
        self.val_loader = DataLoader(val_dataset, shuffle=True, batch_size=self.config["val"]["batch_size"], num_workers=4, pin_memory=True)

    def build_model(self):
        geometry = self.config["data"]["kitti"]["geometry"]
        learning_rate = self.config["train"]["learning_rate"]
        momentum = self.config["train"]["momentum"]
        weight_decay = self.config["train"]["weight_decay"]
        lr_decay_at = self.config["train"]["lr_decay_at"]
        self.model = AFDet(self.config["data"]["num_classes"])

        self.model.to(self.device)
        self.loss = AFDetLoss()
        if self.config["train"]["use_differential_learning"]:
            dif_learning_rate = self.config["train"]["differential_learning_rate"]
            self.optimizer = torch.optim.Adam([{'params': self.model.backbone.parameters(), 'lr': dif_learning_rate[1]},
                                             {'params': self.model.header.parameters(), 'lr': dif_learning_rate[0]}],
                                             lr=learning_rate, weight_decay=weight_decay)
        else:
            self.optimizer = torch.optim.Adam(self.model.parameters(), lr=learning_rate, weight_decay=weight_decay)
        #self.optimizer = torch.optim.SGD(self.model.parameters(), lr=learning_rate, momentum=momentum, weight_decay=weight_decay)
        self.scheduler = torch.optim.lr_scheduler.MultiStepLR(self.optimizer, milestones=lr_decay_at, gamma=0.1)

    def train_one_epoch(self, epoch):
        offset_loss = 0
        cls_loss = 0
        train_loss = 0
        size_loss = 0
        yaw_loss = 0
        start_time = time.time()
        self.model.train()
        for data in tqdm(self.train_loader):
            voxel = data["voxel"].to(self.device)
            cls_label = data["cls"].to(self.device)
            offset_label = data["offset"].to(self.device)
            size_label = data["size"].to(self.device)
            yaw_label = data["yaw"].to(self.device)
            reg_mask = data["reg_mask"].to(self.device)

            target = {
                "cls": cls_label,
                "offset": offset_label,
                "size": size_label,
                "yaw": yaw_label,
                "reg_mask": reg_mask
            }
            self.optimizer.zero_grad()

            pred = self.model(voxel)
            loss, cls, offset, size, yaw = self.loss(pred, target)

            loss.backward()
            self.optimizer.step()

            cls_loss += cls
            offset_loss += offset
            size_loss += size
            yaw_loss += yaw
            train_loss += loss.item()

        self.writer.add_scalar("cls_loss/train", cls_loss / len(self.train_loader), epoch)
        self.writer.add_scalar("offset_loss/train", offset_loss / len(self.train_loader), epoch)
        self.writer.add_scalar("size_loss/train", size_loss / len(self.train_loader), epoch)
        self.writer.add_scalar("yaw_loss/train", yaw_loss / len(self.train_loader), epoch)
        self.writer.add_scalar("loss/train", train_loss / len(self.train_loader), epoch)

        print("Epoch {}|Time {}|Training Loss: {:.5f}".format(
            epoch, time.time() - start_time, train_loss / len(self.train_loader)))

    def train(self):
        self.prepare_loaders()
        self.build_model()
        self.make_experiments_dirs()
        self.writer = SummaryWriter(log_dir = self.runs_dir)

        start_epoch = 0
        if self.config["resume_training"]:
            model_path = os.path.join(self.checkpoints_dir, str(self.config["resume_from"]) + "epoch")
            self.model.load_state_dict(torch.load(model_path, map_location=self.config["device"]))
            start_epoch = self.config["resume_from"]
            print("successfully loaded model starting from " + str(self.config["resume_from"]) + " epoch")

        for epoch in range(start_epoch + 1, self.config["train"]["epochs"]):
            self.train_one_epoch(epoch)

            if epoch % self.config["train"]["save_every"] == 0:
                path = os.path.join(self.checkpoints_dir, str(epoch) + "epoch")
                torch.save(self.model.state_dict(), path)

            if (epoch + 1) % self.config["val"]["val_every"] == 0:
                self.validate(epoch)

            self.scheduler.step()

    def validate(self, epoch):
        offset_loss = 0
        cls_loss = 0
        val_loss = 0
        size_loss = 0
        yaw_loss = 0
        start_time = time.time()
        self.model.eval()
        with torch.no_grad():
            for data in self.val_loader:
                voxel = data["voxel"].to(self.device)
                cls_label = data["cls"].to(self.device)
                offset_label = data["offset"].to(self.device)
                size_label = data["size"].to(self.device)
                yaw_label = data["yaw"].to(self.device)
                reg_mask = data["reg_mask"].to(self.device)

                target = {
                    "cls": cls_label,
                    "offset": offset_label,
                    "size": size_label,
                    "yaw": yaw_label,
                    "reg_mask": reg_mask
                }

                pred = self.model(voxel)
                loss, cls, offset, size, yaw = self.loss(pred, target)

                cls_loss += cls
                offset_loss += offset
                size_loss += size
                yaw_loss += yaw
                val_loss += loss.item()

        self.model.train()

        self.writer.add_scalar("cls_loss/val", cls_loss / len(self.val_loader), epoch)
        self.writer.add_scalar("offset_loss/val", offset_loss / len(self.val_loader), epoch)
        self.writer.add_scalar("size_loss/val", size_loss / len(self.val_loader), epoch)
        self.writer.add_scalar("yaw_loss/val", yaw_loss / len(self.val_loader), epoch)
        self.writer.add_scalar("loss/val", val_loss / len(self.val_loader), epoch)

        print("Epoch {}|Time {}|Validation Loss: {:.5f}".format(
            epoch, time.time() - start_time, val_loss / len(self.val_loader)))

        if val_loss / len(self.val_loader) < self.prev_val_loss:
            self.prev_val_loss = val_loss / len(self.val_loader)
            path = os.path.join(self.best_checkpoints_dir, str(epoch) + "epoch")
            torch.save(self.model.state_dict(), path)

    def make_experiments_dirs(self):
        base = self.config["model"] + "_" + self.config["note"] + "_" + self.config["date"] + "_" + str(self.config["ver"])
        path = os.path.join(self.config["experiments"], base)
        if not os.path.exists(path):
            os.mkdir(path)
        self.checkpoints_dir = os.path.join(path, "checkpoints")
        self.best_checkpoints_dir = os.path.join(path, "best_checkpoints")
        self.runs_dir = os.path.join(path, "runs")

        if not os.path.exists(self.checkpoints_dir):
            os.mkdir(self.checkpoints_dir)

        if not os.path.exists(self.best_checkpoints_dir):
            os.mkdir(self.best_checkpoints_dir)

        if not os.path.exists(self.runs_dir):
            os.mkdir(self.runs_dir)