diff --git a/mapytex/__init__.py b/mapytex/__init__.py index 540c8ff..8fcd4cd 100644 --- a/mapytex/__init__.py +++ b/mapytex/__init__.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # encoding: utf-8 -from .calculus import Expression, Integer, Decimal, random_list, render, Polynomial, Fraction +from .calculus import Expression, Integer, Decimal, render, Polynomial, Fraction#, random_list, # Expression.set_render('tex') diff --git a/mapytex/calculus/API/expression.py b/mapytex/calculus/API/expression.py index 8588fde..6a9f695 100644 --- a/mapytex/calculus/API/expression.py +++ b/mapytex/calculus/API/expression.py @@ -12,13 +12,6 @@ Expression """ from functools import partial from ..core import AssocialTree, Tree, compute, typing, TypingError -#from ..core.random import ( -# extract_rdleaf, -# extract_rv, -# random_generator, -# compute_leafs, -# replace_rdleaf, -#) from ..core.MO import moify from .tokens import factory from .renders import render @@ -79,55 +72,6 @@ class Expression(object): return cls(t) - @classmethod - def random( - cls, - template, - conditions=[], - rejected=[0], - min_max=(-10, 10), - variables_scope={}, - shuffle=False, - ): - """ Initiate randomly the expression - - :param template: the template of the expression - :param conditions: conditions on randomly generate variable - :param rejected: Values to reject for all random variables - :param min_max: Min and max value for all random variables - :param variables_scope: Dictionnary for each random varaibles to fic rejected and min_max - :param shuffle: allowing to shuffle the tree - :returns: TODO - - :example: - >>> e = Expression.random("{a}/{a*k}") - >>> e # doctest: +SKIP - - >>> e = Expression.random("{a}/{a*k} - 3*{b}", variables_scope={'a':{'min_max':(10, 30)}}) - >>> e # doctest: +SKIP - - >>> e = Expression.random("{a}*x + {b}*x + 3", ["a>b"], rejected=[0, 1]) - >>> ee = e.simplify() - >>> print(e) # doctest: +SKIP - 10x - 6x + 3 - >>> print(ee) # doctest: +SKIP - 4x + 3 - - """ - rd_t = Tree.from_str(template, random=True) - leafs = extract_rdleaf(rd_t) - rd_varia = extract_rv(leafs) - generated = random_generator( - rd_varia, conditions, rejected, min_max, variables_scope - ) - computed = compute_leafs(leafs, generated) - t = replace_rdleaf(rd_t, computed).map_on_leaf(moify) - - if shuffle: - raise NotImplemented("Can't suffle expression yet") - - return cls._post_processing(t) - @classmethod def _post_processing(cls, t): """ Post process the tree by typing it """ diff --git a/mapytex/calculus/random/core/__init__.py b/mapytex/calculus/random/core/__init__.py index 9fe4275..4686114 100644 --- a/mapytex/calculus/random/core/__init__.py +++ b/mapytex/calculus/random/core/__init__.py @@ -37,254 +37,28 @@ Tree with RdLeaf replaced by generated values :example: ->>> from ...core.tree import Tree +>>> from .random_tree import RandomTree >>> from .leaf import RdLeaf ->>> rd_t = Tree("/", RdLeaf("a"), RdLeaf("a*k")) +>>> from .generate import extract_letters, random_generator, eval_words +>>> rd_t = RandomTree("/", RdLeaf("a"), RdLeaf("a*k")) >>> print(rd_t) / > {a} > {a*k} ->>> leafs = extract_rdleaf(rd_t) ->>> leafs -['a', 'a*k'] ->>> rd_varia = extract_rv(leafs) +>>> leafs = rd_t.random_leaf +>>> leafs = ['a', 'a*k'] +>>> rd_varia = extract_letters(leafs) >>> sorted(list(rd_varia)) ['a', 'k'] >>> generated = random_generator(rd_varia, conditions=['a%2+1']) >>> generated # doctest: +SKIP {'a': 7, 'k': 4} ->>> computed = compute_leafs(leafs, generated) +>>> computed = eval_words(leafs, generated) >>> computed # doctest: +SKIP {'a': 7, 'a*k': 28} ->>> replaced = replace_rdleaf(rd_t, computed) +>>> replaced = rd_t.eval_random_leaves(computed) >>> print(replaced) # doctest: +SKIP / > 7 > 28 - -List generator --------------- - -This function ignores tree structure and works with lists - ->>> values = list_generator(["a", "a*b", "b", "c"], conditions=["b%c==1"]) ->>> values # doctest: +SKIP -{'a': -8, 'a*b': -40, 'b': 5, 'c': 4} """ - -__all__ = ["list_generator"] - -from random import choice - - -def extract_rdleaf(tree): - """ Extract rdLeaf in a Tree - - :example: - >>> from ...core.tree import Tree - >>> from .leaf import RdLeaf - >>> rd_t = Tree("+", RdLeaf("a"), RdLeaf("a*k")) - >>> from .leaf import RdLeaf - >>> extract_rdleaf(rd_t) - ['a', 'a*k'] - >>> rd_t = Tree("+", RdLeaf("a"), 2) - >>> extract_rdleaf(rd_t) - ['a'] - """ - rd_leafs = [] - for leaf in tree.get_leafs(): - try: - leaf.rdleaf - except AttributeError: - pass - else: - rd_leafs.append(leaf.name) - return rd_leafs - - -def extract_rv(leafs): - """ Extract the set of random values from the leaf list - - :param leafs: list of leafs - :return: set of random values - - :example: - >>> leafs = ["a", "a*k"] - >>> extract_rv(leafs) == {'a', 'k'} - True - """ - rd_values = set() - for leaf in leafs: - for c in leaf: - if c.isalpha(): - rd_values.add(c) - return rd_values - - -def compute_leafs(leafs, generated_values): - """ Compute leafs from generated random values - - :param generated_values: Dictionnary of name:generated value - :param leafs: list of leafs - :return: Dictionnary of evaluated leafs from generated values - - :example: - >>> leafs = ["a", "a*k"] - >>> generated_values = {"a":2, "k":3} - >>> compute_leafs(leafs, generated_values) - {'a': 2, 'a*k': 6} - """ - return {leaf: eval(leaf, generated_values) for leaf in leafs} - - -def replace_rdleaf(tree, computed_leafs): - """ Replace RdLeaf by the corresponding computed value - - >>> from ...core.tree import Tree - >>> from .leaf import RdLeaf - >>> rd_t = Tree("+", RdLeaf("a"), RdLeaf("a*k")) - >>> computed_leafs = {'a': 2, 'a*k': 6} - >>> print(replace_rdleaf(rd_t, computed_leafs)) - + - > 2 - > 6 - """ - - def replace(leaf): - try: - return leaf.replace(computed_leafs) - except AttributeError: - return leaf - - return tree.map_on_leaf(replace) - - -def random_generator( - rd_variables, conditions=[], rejected=[0], min_max=(-10, 10), variables_scope={} -): - """ Generate random variables - - :param rd_variables: list of random variables to generate - :param conditions: condition over variables - :param rejected: Rejected values for the generator (default [0]) - :param min_max: (min, max) limits in between variables will be generated - :param variables_scope: rejected and min_max define for individual variables - :return: dictionnary of generated variables - - :example: - >>> gene = random_generator(["a", "b"], - ... ["a > 0"], - ... [0], (-10, 10), - ... {"a": {"rejected": [0, 1]}, - ... "b": {"min_max": (-5, 0)}}) - >>> gene["a"] > 0 - True - >>> gene["a"] != 0 - True - >>> gene["b"] < 0 - True - >>> gene = random_generator(["a", "b"], - ... ["a % b == 0"], - ... [0, 1], (-10, 10)) - >>> gene["a"] not in [0, 1] - True - >>> gene["b"] in list(range(-10, 11)) - True - >>> gene["a"] % gene["b"] - 0 - """ - complete_scope = build_variable_scope( - rd_variables, rejected, min_max, variables_scope - ) - choices_list = { - v: list( - set( - range( - complete_scope[v]["min_max"][0], complete_scope[v]["min_max"][1] + 1 - ) - ).difference(complete_scope[v]["rejected"]) - ) - for v in rd_variables - } - - # quantity_choices = reduce(lambda x,y : x*y, - # [len(choices_list[v]) for v in choices_list]) - # TODO: améliorer la méthode de rejet avec un cache |dim. mai 12 17:04:11 CEST 2019 - - generate_variable = {v: choice(choices_list[v]) for v in rd_variables} - - while not all([eval(c, __builtins__, generate_variable) for c in conditions]): - generate_variable = {v: choice(choices_list[v]) for v in rd_variables} - - return generate_variable - - -def build_variable_scope(rd_variables, rejected, min_max, variables_scope): - """ Build variables scope from incomplete one - - :param rd_variables: list of random variables to generate - :param rejected: Rejected values for the generator - :param min_max: (min, max) limits in between variables will be generated - :param variables_scope: rejected and min_max define for individual variables - :return: complete variable scope - - :example: - >>> completed = build_variable_scope(["a", "b", "c", "d"], [0], (-10, 10), - ... {"a": {"rejected": [0, 1]}, - ... "b": {"min_max": (-5, 0)}, - ... "c": {"rejected": [2], "min_max": (0, 5)}}) - >>> complete = {'a': {'rejected': [0, 1], 'min_max': (-10, 10)}, - ... 'b': {'rejected': [0], 'min_max': (-5, 0)}, - ... 'c': {'rejected': [2], 'min_max': (0, 5)}, - ... 'd': {'rejected': [0], 'min_max': (-10, 10)}} - >>> completed == complete - True - """ - complete_scope = variables_scope.copy() - for v in rd_variables: - try: - complete_scope[v] - except KeyError: - complete_scope[v] = {"rejected": rejected, "min_max": min_max} - else: - try: - complete_scope[v]["rejected"] - except KeyError: - complete_scope[v]["rejected"] = rejected - try: - complete_scope[v]["min_max"] - except KeyError: - complete_scope[v]["min_max"] = min_max - return complete_scope - - -def list_generator(var_list, conditions=[], rejected=[0], min_max=(-10, 10), variables_scope={}, dictionnary=False): - """ Generate random computed values from the list - - :param rd_variables: list of random variables to generate (can be computed value - "a*b") - :param conditions: condition over variables - :param rejected: Rejected values for the generator (default [0]) - :param min_max: (min, max) limits in between variables will be generated - :param variables_scope: rejected and min_max define for individual variables - :param dictionnary: the return value will be a dictionnary with var_list as keys (default False) - :return: dictionnary of generated variables - - :example: - >>> a, ab, b, c = list_generator(["a", "a*b", "b", "c"]) - >>> a, ab, b, c # doctest: +SKIP - (5, -20, -4, -3) - >>> a * b == ab - True - >>> ab # doctest: +SKIP - -20 - >>> a, b # doctest: +SKIP - 5, -4 - >>> list_generator(["a", "a*b", "b", "c"], dictionnary=True) # doctest: +SKIP - {'a': -3, 'a*b': 18, 'b': -6, 'c': -4} - """ - rv = extract_rv(var_list) - rv_gen = random_generator(rv, conditions, rejected, min_max, variables_scope) - generated = compute_leafs(var_list, rv_gen) - if dictionnary: - return generated - return [generated[v] for v in var_list] diff --git a/mapytex/calculus/random/core/generate.py b/mapytex/calculus/random/core/generate.py new file mode 100644 index 0000000..b9557f2 --- /dev/null +++ b/mapytex/calculus/random/core/generate.py @@ -0,0 +1,146 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# vim:fenc=utf-8 +# +# Copyright © 2017 lafrite +# +# Distributed under terms of the MIT license. + + +from random import choice + + +def extract_letters(words:list[str])->set[str]: + """ Extracts unique letters from a list of words + + :param words: list of leafs + :return: set of letters + + :example: + >>> leafs = ["a", "a*k"] + >>> extract_letters(leafs) == {'a', 'k'} + True + """ + letters = set() + for word in words: + for c in word: + if c.isalpha(): + letters.add(c) + return letters + + +def eval_words(words:list[str], values:dict[str,int]) -> dict[str, int]: + """ Evaluate words replacing letters with values + + :param words: list of words + :param values: Dictionary of letters:value + :return: Dictionary of evaluated words from generated values + + :example: + >>> leafs = ["a", "a*k"] + >>> generated_values = {"a":2, "k":3} + >>> eval_words(leafs, generated_values) + {'a': 2, 'a*k': 6} + """ + return {word: eval(word, values) for word in words} + + +def build_variable_scope(rd_variables, rejected, min_max, variables_scope): + """ Build variables scope from incomplete one + + :param rd_variables: list of random variables to generate + :param rejected: Rejected values for the generator + :param min_max: (min, max) limits in between variables will be generated + :param variables_scope: rejected and min_max define for individual variables + :return: complete variable scope + + :example: + >>> completed = build_variable_scope(["a", "b", "c", "d"], [0], (-10, 10), + ... {"a": {"rejected": [0, 1]}, + ... "b": {"min_max": (-5, 0)}, + ... "c": {"rejected": [2], "min_max": (0, 5)}}) + >>> complete = {'a': {'rejected': [0, 1], 'min_max': (-10, 10)}, + ... 'b': {'rejected': [0], 'min_max': (-5, 0)}, + ... 'c': {'rejected': [2], 'min_max': (0, 5)}, + ... 'd': {'rejected': [0], 'min_max': (-10, 10)}} + >>> completed == complete + True + """ + complete_scope = variables_scope.copy() + for v in rd_variables: + try: + complete_scope[v] + except KeyError: + complete_scope[v] = {"rejected": rejected, "min_max": min_max} + else: + try: + complete_scope[v]["rejected"] + except KeyError: + complete_scope[v]["rejected"] = rejected + try: + complete_scope[v]["min_max"] + except KeyError: + complete_scope[v]["min_max"] = min_max + return complete_scope + + +def random_generator( + rd_variables, conditions=[], rejected=[0], min_max=(-10, 10), variables_scope={} +): + """ Generate random variables + + :param rd_variables: list of random variables to generate + :param conditions: condition over variables + :param rejected: Rejected values for the generator (default [0]) + :param min_max: (min, max) limits in between variables will be generated + :param variables_scope: rejected and min_max define for individual variables + :return: dictionnary of generated variables + + :example: + >>> gene = random_generator(["a", "b"], + ... ["a > 0"], + ... [0], (-10, 10), + ... {"a": {"rejected": [0, 1]}, + ... "b": {"min_max": (-5, 0)}}) + >>> gene["a"] > 0 + True + >>> gene["a"] != 0 + True + >>> gene["b"] < 0 + True + >>> gene = random_generator(["a", "b"], + ... ["a % b == 0"], + ... [0, 1], (-10, 10)) + >>> gene["a"] not in [0, 1] + True + >>> gene["b"] in list(range(-10, 11)) + True + >>> gene["a"] % gene["b"] + 0 + """ + complete_scope = build_variable_scope( + rd_variables, rejected, min_max, variables_scope + ) + choices_list = { + v: list( + set( + range( + complete_scope[v]["min_max"][0], complete_scope[v]["min_max"][1] + 1 + ) + ).difference(complete_scope[v]["rejected"]) + ) + for v in rd_variables + } + + # quantity_choices = reduce(lambda x,y : x*y, + # [len(choices_list[v]) for v in choices_list]) + # TODO: améliorer la méthode de rejet avec un cache |dim. mai 12 17:04:11 CEST 2019 + + generate_variable = {v: choice(choices_list[v]) for v in rd_variables} + + while not all([eval(c, __builtins__, generate_variable) for c in conditions]): + generate_variable = {v: choice(choices_list[v]) for v in rd_variables} + + return generate_variable + + diff --git a/mapytex/calculus/random/core/random_tree.py b/mapytex/calculus/random/core/random_tree.py new file mode 100644 index 0000000..599bfb0 --- /dev/null +++ b/mapytex/calculus/random/core/random_tree.py @@ -0,0 +1,104 @@ +from ...core.tree import MutableTree, Tree +from .generate import extract_letters, random_generator +from .str2 import rdstr2 + +class RandomTree(MutableTree): + """ MutableTree that accept {a} syntax for random generation + + :example: + >>> t = RandomTree() + >>> type(t) + + """ + + @classmethod + def from_str(cls, expression): + """ Initiate a random tree from a string that need to be parsed + + :exemple: + >>> t = RandomTree.from_str("{b}*x+{c}") + >>> print(t) + + + > * + | > {b} + | > x + > {c} + >>> t = RandomTree.from_str("{a}*({b}*x+{c})") + >>> print(t) + * + > {a} + > + + | > * + | | > {b} + | | > x + | > {c} + """ + str_2_mut_tree = rdstr2(cls.sink) + return str_2_mut_tree(expression) + + def generate(self, conditions:list[str]=[], rejected:list[int]=[0, 1], min_max:tuple[int]=(-10, 10),scopes:dict={}) -> Tree: + """ Generate a random version of self """ + + pass + + @property + def random_leaf(self) -> list[str]: + """ Get list of random leaves + + :example: + >>> from .leaf import RdLeaf + >>> random_tree = RandomTree("+", RdLeaf("a"), RdLeaf("a*k")) + >>> random_tree.random_leaf + ['a', 'a*k'] + >>> random_tree = RandomTree("+", RdLeaf("a"), 2) + >>> random_tree.random_leaf + ['a'] + """ + rd_leafs = [] + for leaf in self.get_leafs(): + try: + leaf.rdleaf + except AttributeError: + pass + else: + rd_leafs.append(leaf.name) + return rd_leafs + + + @property + def random_value(self) -> set[str]: + """ Get set of random values to generate + + :example: + >>> from .leaf import RdLeaf + >>> random_tree = RandomTree("+", RdLeaf("a"), RdLeaf("a*k")) + >>> random_tree.random_value == {'a', 'k'} + True + >>> random_tree = RandomTree("+", RdLeaf("a"), 2) + >>> random_tree.random_value + {'a'} + """ + return extract_letters(self.random_leaf) + + + def eval_random_leaves(self, leaves_value:dict[str, int]): + """ Given random leaves value get the tree + + :example: + >>> from .leaf import RdLeaf + >>> rd_t = RandomTree("+", RdLeaf("a"), RdLeaf("a*k")) + >>> leaves_values = {'a': 2, 'a*k': 6} + >>> t = rd_t.eval_random_leaves(leaves_values) + >>> type(t) + + >>> print(t) + + + > 2 + > 6 + """ + def replace(leaf): + try: + return leaf.replace(leaves_value) + except AttributeError: + return leaf + return self.map_on_leaf(replace) diff --git a/mapytex/calculus/random/core/str2.py b/mapytex/calculus/random/core/str2.py index 92d3468..08ee776 100644 --- a/mapytex/calculus/random/core/str2.py +++ b/mapytex/calculus/random/core/str2.py @@ -4,7 +4,6 @@ from functools import partial from ...core.str2 import concurent_broadcast, lookforNumbers, pparser, missing_times, lookfor from ...core.coroutine import STOOOP from ...core.MO import moify_cor -from ...core.tree import MutableTree from .leaf import look_for_rdleaf def rdstr2(sink): @@ -35,31 +34,3 @@ def rdstr2(sink): return pipeline -class RandomTree(MutableTree): - """ MutableTree that accept {a} syntax for random generation """ - - @classmethod - def from_str(cls, expression): - """ Initiate a random tree from a string that need to be parsed - - :exemple: - >>> t = RandomTree.from_str("{b}*x+{c}") - >>> print(t) - + - > * - | > {b} - | > x - > {c} - >>> t = RandomTree.from_str("{a}*({b}*x+{c})") - >>> print(t) - * - > {a} - > + - | > * - | | > {b} - | | > x - | > {c} - """ - str_2_mut_tree = rdstr2(cls.sink) - return str_2_mut_tree(expression) - diff --git a/mapytex/calculus/random/expression.py b/mapytex/calculus/random/expression.py new file mode 100644 index 0000000..b3d85ef --- /dev/null +++ b/mapytex/calculus/random/expression.py @@ -0,0 +1,55 @@ +from ..API.expression import Expression +from ..core.tree import Tree + +def expression(): + """ Generate a random expression """ + pass + +@classmethod +def random( + cls, + template, + conditions=[], + rejected=[0], + min_max=(-10, 10), + variables_scope={}, + shuffle=False, +): + """ Initiate randomly the expression + + :param template: the template of the expression + :param conditions: conditions on randomly generate variable + :param rejected: Values to reject for all random variables + :param min_max: Min and max value for all random variables + :param variables_scope: Dictionnary for each random varaibles to fic rejected and min_max + :param shuffle: allowing to shuffle the tree + :returns: TODO + + :example: + >>> e = Expression.random("{a}/{a*k}") + >>> e # doctest: +SKIP + + >>> e = Expression.random("{a}/{a*k} - 3*{b}", variables_scope={'a':{'min_max':(10, 30)}}) + >>> e # doctest: +SKIP + + >>> e = Expression.random("{a}*x + {b}*x + 3", ["a>b"], rejected=[0, 1]) + >>> ee = e.simplify() + >>> print(e) # doctest: +SKIP + 10x - 6x + 3 + >>> print(ee) # doctest: +SKIP + 4x + 3 + + """ + rd_t = Tree.from_str(template) + leafs = extract_rdleaf(rd_t) + rd_varia = extract_rv(leafs) + generated = random_generator( + rd_varia, conditions, rejected, min_max, variables_scope + ) + computed = compute_leafs(leafs, generated) + t = replace_rdleaf(rd_t, computed).map_on_leaf(moify) + + if shuffle: + raise NotImplemented("Can't suffle expression yet") + + return cls._post_processing(t) diff --git a/mapytex/calculus/random/list.py b/mapytex/calculus/random/list.py new file mode 100644 index 0000000..a92b428 --- /dev/null +++ b/mapytex/calculus/random/list.py @@ -0,0 +1,43 @@ +""" +List generator +-------------- + +This function ignores tree structure and works with lists + +>>> values = list_generator(["a", "a*b", "b", "c"], conditions=["b%c==1"]) +>>> values # doctest: +SKIP +{'a': -8, 'a*b': -40, 'b': 5, 'c': 4} +""" + +from .core.generate import extract_letters, random_generator, eval_words + +def list_generator(var_list, conditions=[], rejected=[0], min_max=(-10, 10), variables_scope={}, dictionnary=False): + """ Generate random computed values from the list + + :param rd_variables: list of random variables to generate (can be computed value - "a*b") + :param conditions: condition over variables + :param rejected Rejected values for the generator (default [0]) + :param min_max: (min, max) limits in between variables will be generated + :param variables_scope: rejected and min_max define for individual variables + :param dictionnary: the return value will be a dictionnary with var_list as keys (default False) + :return: dictionnary of generated variables + + :example: + >>> a, ab, b, c = list_generator(["a", "a*b", "b", "c"]) + >>> a, ab, b, c # doctest: +SKIP + (5, -20, -4, -3) + >>> a * b == ab + True + >>> ab # doctest: +SKIP + -20 + >>> a, b # doctest: +SKIP + 5, -4 + >>> list_generator(["a", "a*b", "b", "c"], dictionnary=True) # doctest: +SKIP + {'a': -3, 'a*b': 18, 'b': -6, 'c': -4} + """ + rv = extract_letters(var_list) + rv_gen = random_generator(rv, conditions, rejected, min_max, variables_scope) + generated = eval_words(var_list, rv_gen) + if dictionnary: + return generated + return [generated[v] for v in var_list] diff --git a/test/calculus/__init__.py b/test/calculus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/calculus/api/__init__.py b/test/calculus/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/calculus/api/random/__init__.py b/test/calculus/api/random/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/calculus/api/random/test_random.py b/test/calculus/api/random/test_random.py new file mode 100644 index 0000000..7e8b156 --- /dev/null +++ b/test/calculus/api/random/test_random.py @@ -0,0 +1,6 @@ +import pytest +import mapytex + +def test_random_function(): + assert 1 == 1 + #mapytex.random("{a}")