Commit 44268bdc authored by Yann Boucher's avatar Yann Boucher
Browse files

Added a totalistic rule transition with its own description language

parent 5a4b0a91
Pipeline #78466 passed with stages
in 19 seconds
/**
\file mathexpr.hpp
\date 25/05/2021
\author Yann Boucher
\version 1
\brief MathExpr
Ce fichier contient une fonction permettant d'exécuter une expression mathématique à partir d'une chaîne de caractères et d'une liste de variables.
**/
#ifndef MATHEXPR_HPP
#define MATHEXPR_HPP
#include <map>
#include <exception>
class MathExprException : public std::exception
{
public:
MathExprException(const std::string& str)
: m_what(str)
{}
const char * what() const noexcept override
{ return m_what.c_str(); }
private:
std::string m_what;
};
//! Evalue une expression mathématique selon une liste de variables fournies, et retourne un entier.
//! Les opérateurs supportés sont les parenthèses, +, -, *, /, %.
//! \param expr L'expression mathématique, en tant que std::string
//! \param variables un std::map associant à chaque nom de variable une valeur entière
//! \returns La valeur de résultat de l'expression mathématique évaluée.
int eval_math(const std::string& expr, const std::map<std::string, int>& variables = {});
#endif // MATHEXPR_HPP
/**
\file totalistictransition.hpp
\date 25/05/2021
\author Yann Boucher
\version 1
\brief TotalisticTransitionRule
Cette classe représente une règle de transition totalistique configurable.
**/
#ifndef TOTALISTICTRANSITIONRULE_HPP
#define TOTALISTICTRANSITIONRULE_HPP
#include <exception>
#include <string>
#include <map>
#include <limits>
#include "neighborhood.hpp"
#include "mathexpr.hpp"
#include "transitionrule.hpp"
class TotalisticRuleException : public std::exception
{
public:
TotalisticRuleException(const std::string& str)
: m_what(str)
{}
const char * what() const noexcept override
{ return m_what.c_str(); }
private:
std::string m_what;
};
//! Représente un intervalle [low;high] composé d'expressions mathématiques
struct Interval
{
std::string low, high;
//! Evalue l'intervalle pour vérifier si val est inclus dedans
//! \returns true si inclus, false sinon
bool contains(unsigned val) const
{
unsigned low_v = eval_math(low , {});
unsigned high_v = !high.empty() ? eval_math(high, {}) : std::numeric_limits<unsigned>::max();
// sanity check
if (low_v > high_v)
std::swap(low_v, high_v);
return val >= low_v && val <= high_v;
}
};
//! Cette classe représente une entrée de règle de transition totalistique configurable,
//! c'est à dire d'une entrée dans la table de transition permettant de déterminer si une transition s'effectue selon un état de départ et un voisinage donnés.
class TotalisticRuleEntry
{
public:
//! Construit la règle à partir d'une chaîne contenant la définition de la règle.
TotalisticRuleEntry(std::string rule_string);
//! Détermine si l'entrée accepte l'état et le voisinage donnés.
//! \param initial_state L'état de la cellule concernée.
//! \param neighborhood Le voisinage de la cellule concernée.
//! \param next Référence vers la valeur du prochain état de la cellule si l'entrée est acceptée.
//! \returns true si accepté, false sinon.
bool accept(unsigned initial_state, const Neighborhood& neighborhood, unsigned& next) const;
private:
bool m_initial_state_is_variable;
// could use a union here, but it's a bit tricky as we'd have to call the correct destructor
std::string m_initial_variable;
unsigned m_initial_state;
std::map<std::string, Interval> m_constraints;
std::string m_result_state;
};
//! Cette classe représente une règle de transition totalistique configurable textuellement.
//! Le format d'une règle est tel que suit:
/**
## format d'une règle de transition isotropique:
etat_cellule, x:intervalle_voisins_d'etat_x, ... -> nouvel_etat
expression := <expression mathématique pouvant contenir une variable qu'on associerait à l'état initial>
intervalle := [expression..expression] | [expression] | '*'
Exemple automate cellulaire cyclique de Griffith:
i, (i+1)%3:[3] -> (i+1)%3
On pourrait aussi ajouter une constante 'N' qui correspondrait au nombre d'états de la règle:
i, (i+1)%N:[3] -> (i+1)%N
Exemple jeu de la vie:
0,0:* ,1:[3]->1 (une cellule morte devient vivante si elle a exactement 3 voisisin vivants)
1,0:[0..1],1:* ->0 (une cellule vivante meurt si elle a 0 ou 1 voisins vivants)
1,0:[4..*],1:* ->0 (une cellule vivante meurt si elle a 4 ou plus voisins vivants)
Exemple Wireworld: (https://xalava.github.io/WireWorld/)
1->2
2->3
3,1:[1..2]->1
Exemple Brain's Brain: (https://www.conwaylife.com/wiki/OCA:Brian%27s_Brain)
0,1:[2]->1
2->0
Si il n'y a pas de règle correspondat à une cellule et son voisinage, alors cette cellule ne change pas d'état.
Les intervalles non mentionnés dans la règle sont considérés par défaut comme des '*'.
Les règles sont traitées dans l'ordre de leur écriture, si deux règles pourraient s'appliquer à une cellule et à un voisinage, on applique la première dans l'ordre des lignes, du haut vers le bas.
*/
class TotalisticTransition : public TransitionRule
{
public:
bool acceptFormat(const std::vector<NeighborhoodFormat>&) const override
{ return true; }
unsigned int getState(unsigned int initial, const Neighborhood &neighborhood) const override;
private:
void generate_entries() const;
private:
mutable std::vector<TotalisticRuleEntry> m_entries;
DEFINE_CONFIGURABLE_PROPERTY(StringProperty, rule_string, "Rule String");
};
#endif // TOTALISTICTRANSITIONRULE_HPP
/**
\file mathexpr.cpp
\date 25/05/2021
\author Yann Boucher
\version 1
\brief MathExpr
Ce fichier contient une fonction permettant d'exécuter une expression mathématique à partir d'une chaîne de caractères et d'une liste de variables.
**/
// Basé sur l'algorithme du Shunting Yard, suivi d'une évaluation de l'expression sous notation polonaise inversée résultante.
#include "mathexpr.hpp"
#include <stack>
#include <vector>
#include <algorithm>
#include <cctype>
struct rpl_token_t
{
enum
{
LITERAL,
OP
} type;
union
{
int value;
char op;
};
};
//! Priorité de l'opérateur
static int precedence(char op)
{
if (op == '*' || op == '/' || op == '%')
return 3;
else if (op == '+' || op == '-')
return 2;
else
return 1;
}
//! Evalue une opération binaire entre x et y, dénotée par op.
static int eval_int_binop(char op, int x, int y)
{
if ((op == '/' || op == '%') && y == 0)
throw MathExprException("Divsion by zero error");
switch (op)
{
case '+':
return x+y;
case '-':
return x-y;
case '*':
return x*y;
case '/':
return x/y;
case '%':
return x%y;
default:
throw MathExprException("Unknown operator");
}
}
//! Evalue une expression RPL
static int evaluate_rpl_input(const std::vector<rpl_token_t>& rpl_stack)
{
std::vector<int> data_stack;
for (unsigned i = 0; i < rpl_stack.size(); ++i)
{
if (rpl_stack[i].type == rpl_token_t::LITERAL)
data_stack.push_back(rpl_stack[i].value);
else
{
int op = rpl_stack[i].op;
if (data_stack.size() <= 1)
throw MathExprException("Invalid math expression error");
int x = data_stack[data_stack.size()-2];
int y = data_stack[data_stack.size()-1];
int result = eval_int_binop(op, x, y);
data_stack.pop_back();
data_stack[data_stack.size()-1] = result;
}
}
if (data_stack.size() != 1)
throw MathExprException("Invalid math expression error");
return data_stack[0];
}
int eval_math(const std::string &in_expr, const std::map<std::string, int> &variables)
{
std::string expr = in_expr;
// remove whitespace out of the equation altogether
expr.erase(std::remove_if(expr.begin(), expr.end(), ::isspace), expr.end());
std::vector<rpl_token_t> rpl_stack;
std::stack<char> op_stack;
rpl_token_t rpl_token;
rpl_token.type = rpl_token_t::LITERAL; rpl_token.value = 0; // silence warnings
char* end_ptr;
unsigned idx = 0;
while (idx < expr.size())
{
switch (expr[idx])
{
case '0'...'9':
{
int value = std::strtol(&expr.c_str()[idx], &end_ptr, 10);
int read_len = end_ptr - &expr.c_str()[idx];
idx += read_len;
rpl_token.type = rpl_token_t::LITERAL;
rpl_token.value = value;
rpl_stack.push_back(rpl_token);
break;
}
case '-':
case '+':
case '*':
case '/':
case '%':
// binary operator
{
while (!op_stack.empty() &&
(op_stack.top() != '(') &&
(precedence(op_stack.top()) > precedence(expr[idx])))
{
rpl_token.type = rpl_token_t::OP;
rpl_token.op = op_stack.top();
rpl_stack.push_back(rpl_token);
op_stack.pop();
}
op_stack.push(expr[idx]);
++idx;
}
break;
case '(':
op_stack.push('(');
++idx;
break;
case ')':
while (!op_stack.empty() &&
op_stack.top() != '(')
{
rpl_token.type = rpl_token_t::OP;
rpl_token.op = op_stack.top();
rpl_stack.push_back(rpl_token);
op_stack.pop();
}
if (op_stack.empty())
throw MathExprException("Parenthesis mismatch");
op_stack.pop();
++idx;
break;
default:
// assume it's a variable name then
{
std::string var_name;
while (isalnum(expr[idx]))
{
var_name += expr[idx];
++idx;
}
if (!variables.count(var_name))
throw MathExprException("No such variable '" + var_name + "' found");
rpl_token.type = rpl_token_t::LITERAL;
rpl_token.value = variables.at(var_name);
rpl_stack.push_back(rpl_token);
break;
}
}
}
// while there are remaining operators on the op stack
while (!op_stack.empty())
{
rpl_token.type = rpl_token_t::OP;
rpl_token.op = op_stack.top();
op_stack.pop();
rpl_stack.push_back(rpl_token);
}
return evaluate_rpl_input(rpl_stack);
}
......@@ -13,6 +13,7 @@ INCLUDEPATH += ../include ../include/transition_rules ../include/neighborhood_ru
SOURCES += \
alphabet.cpp \
mathexpr.cpp \
neighborhood_rules/arbitraryneighborhoodrule.cpp \
automaton.cpp \
gridview.cpp \
......@@ -31,7 +32,8 @@ SOURCES += \
neighborhood_rules/vonNeumannNeighborhoodRule.cpp \
structurewriter.cpp \
structurelibraryview.cpp \
modelloadingdialog.cpp
modelloadingdialog.cpp \
transition_rules/totalistictransition.cpp
HEADERS += \
......@@ -60,6 +62,7 @@ HEADERS += \
../include/structurelibraryview.hpp \
../include/transitionrule.hpp \
../include/transition_rules/circulartransition.hpp \
../include/transition_rules/totalistictransition.hpp \
../include/modelloadingdialog.hpp
FORMS += \
......
/**
\file totalistictransition.cpp
\date 25/05/2021
\author Yann Boucher
\version 1
\brief TotalisticTransitionRule
Cette classe représente une règle de transition totalistique configurable.
**/
#include "totalistictransition.hpp"
#include <vector>
#include <algorithm>
#include <sstream>
static std::vector<std::string> split(std::string str, std::string token){
std::vector<std::string>result;
while(str.size()){
size_t index = str.find(token);
if(index != std::string::npos){
result.push_back(str.substr(0,index));
str = str.substr(index+token.size());
if(str.size()==0)result.push_back(str);
}else{
result.push_back(str);
str = "";
}
}
return result;
}
static Interval read_interval( std::string str)
{
// remove whitespace
str.erase(std::remove_if(str.begin(), str.end(), ::isspace), str.end());
Interval interval = Interval{"0", ""}; // full interval by default
if (str.size() < 1)
throw TotalisticRuleException("Invalid rule entry format");
if (str == "*")
return Interval{"0", ""}; // lowest bound, no higher bound
if (str[0] != '[' || str.back() != ']')
throw TotalisticRuleException("Invalid rule entry format");
std::string inside = str.substr(1, str.size()-2);
auto interval_split = split(inside, "..");
if (interval_split.empty() || interval_split.size() > 2)
throw TotalisticRuleException("Invalid rule entry format");
if (interval_split.size() == 1)
{
if (interval_split[0] == "*")
return Interval{"0", ""}; // lowest bound, no higher bound
else
interval.low = interval.high = interval_split[0]; // single-point interval
}
else
{
interval.low = interval_split[0];
if (interval_split[1] == "*")
interval.high = "";
else
interval.high = interval_split[1];
}
return interval;
}
TotalisticRuleEntry::TotalisticRuleEntry(std::string rule_string)
{
rule_string.erase(std::remove_if(rule_string.begin(), rule_string.end(), ::isspace), rule_string.end());
auto arrow_tokens = split(rule_string, "->");
if (arrow_tokens.size() != 2)
throw TotalisticRuleException("Invalid rule entry format");
auto comma_tokens = split(arrow_tokens[0], ",");
if (comma_tokens.empty())
throw TotalisticRuleException("Invalid rule entry format");
if (comma_tokens[0].empty())
throw TotalisticRuleException("Invalid rule entry format");
if (isdigit(comma_tokens[0][0]))
{
m_initial_state_is_variable = false;
m_initial_state = std::stol(comma_tokens[0]);
}
else
{
m_initial_state_is_variable = true;
m_initial_variable = comma_tokens[0];
}
for (size_t i = 1; i < comma_tokens.size(); ++i)
{
auto colon_tokens = split(comma_tokens[1], ":");
if (colon_tokens.size() != 2)
throw TotalisticRuleException("Invalid rule entry format");
m_constraints[colon_tokens[0]] = read_interval(colon_tokens[1]);
}
m_result_state = arrow_tokens[1];
}
bool TotalisticRuleEntry::accept(unsigned initial_state, const Neighborhood &neighborhood, unsigned &next) const
{
if (!m_initial_state_is_variable && initial_state != m_initial_state)
return false;
bool entry_accepted = true;
for (const auto& constraint : m_constraints)
{
unsigned state_to_test = eval_math(constraint.first, {{m_initial_variable, initial_state}});
if (!constraint.second.contains(neighborhood.getNb(state_to_test)))
{
entry_accepted = false;
break;
}
}
if (entry_accepted)
{
next = eval_math(m_result_state, {{m_initial_variable, initial_state}});
return true;
}
else
return false;
}
unsigned int TotalisticTransition::getState(unsigned int initial, const Neighborhood &neighborhood) const
{
// Si la rule string n'a pas encore été lue, la lire et générer les TotalistricRuleEntry correspondantes
if (m_entries.empty())
generate_entries();
// test all possibles entries related to the current cell's state
for (const TotalisticRuleEntry& entry : m_entries)
{
unsigned next;
bool entry_accepted = entry.accept(initial, neighborhood, next);
if (entry_accepted)
return next;
}
// no match, don't change the state
return initial;
}
void TotalisticTransition::generate_entries() const
{
std::istringstream iss(rule_string.str);
// pour chaque ligne = entrée de la chaîne de la règle :
for (std::string line; std::getline(iss, line); )
{
// on enlève les espaces inutiles
line.erase(std::remove_if(line.begin(), line.end(), ::isspace), line.end());
if (!line.empty())
m_entries.emplace_back(line);
}
}
......@@ -28,6 +28,7 @@ private slots:
void test_circulartransition();
void test_lifegametransition();
void test_totalistictransition();
};
#endif // CELLULUT_TESTS_HPP
......@@ -24,6 +24,8 @@ SOURCES += \
../src/neighborhood_rules/mooreNeighborhoodRule.cpp \
../src/transition_rules/lifegametransition.cpp \
../src/transition_rules/circulartransition.cpp \
../src/transition_rules/totalistictransition.cpp \
../src/mathexpr.cpp \
alphabet_test.cpp \
arbitraryneighborhoodrule_test.cpp \
circulartransition_test.cpp \
......@@ -38,7 +40,8 @@ SOURCES += \
cellulut_tests.cpp \
structure_test.cpp \
structurereader_tests.cpp \
structurewriter_tests.cpp
structurewriter_tests.cpp \
totalistictransition_test.cpp
HEADERS += \
cellulut_tests.hpp
......
/*
* <32021 by Stellaris. Copying Art is an act of love. Love is not subject to law.
*/
#include "cellulut_tests.hpp"
#include "totalistictransition.hpp"
#include "propertyvisitors.hpp"
void CellulutTests::test_totalistictransition()
{
QCOMPARE(eval_math("5 + 6*3") , 23);
QCOMPARE(eval_math("5 * 6+3"), 33);
QCOMPARE(eval_math("(5+6) * 3"), 33);
QCOMPARE(eval_math("(i+1)%4", {{"i", 2}}), 3);
QCOMPARE(eval_math("(i+1)%4", {{"i", 3}}), 0);
{