# -*- coding: utf-8 -*-
# Mathmaker creates automatically maths exercises sheets
# with their answers
# Copyright 2006-2017 Nicolas Hainaux <nh.techn@gmail.com>
# This file is part of Mathmaker.
# Mathmaker is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# any later version.
# Mathmaker is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with Mathmaker; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
import random
from math import gcd
from string import ascii_uppercase as alphabet
from abc import ABCMeta, abstractmethod
from mathmakerlib import required
from mathmakerlib.calculus import is_integer, is_number
from mathmaker import settings
from mathmaker.lib import shared
from mathmaker.lib.tools.maths import sign_of_product
from mathmaker.lib.constants import RANDOMLY
from mathmaker.lib.core.utils import gather_literals
from mathmaker.lib.core.base import Printable
from mathmaker.lib.core.root_calculus import (Exponented, Value, Calculable,
Substitutable)
from mathmaker.lib.core.base_calculus import (Monomial, Sum, Item, Polynomial,
Fraction, Expandable, Product,
Quotient, Function, SquareRoot,
AngleItem, CommutativeOperation)
from mathmaker.lib.constants.latex import MARKUP
MAX_VALUE = 20
# The following tables come from the fractions' calculations sheets...
# which is a shame: this code must be factorized
FRACTIONS_SUMS_TABLE = [([1, 2], 15),
([1, 3], 15),
([1, 4], 15),
([1, 5], 15),
([1, 6], 15),
([1, 7], 15),
([2, 3], 17),
([2, 5], 10),
([2, 7], 7),
([3, 4], 9),
([3, 5], 7),
([3, 7], 5),
([4, 5], 5),
([4, 7], 3),
([5, 6], 4),
([5, 7], 4),
([6, 7], 3)]
FRACTIONS_SUMS_SCALE_TABLE = [0.02, # (1, 2)
0.02,
0.02,
0.01,
0.005,
0.005, # (1, 7)
0.21, # (2, 3)
0.14, # (2, 5)
0.02, # (2, 7)
0.21, # (3, 4)
0.14, # (3, 5)
0.01, # (3, 7)
0.16, # (4, 5)
0.01, # (4, 7)
0.01, # (5, 6)
0.005, # (5, 7)
0.005] # (6, 7)
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @package core.calculus
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class ComposedCalculable
# @brief Abstract mother class of objects composed of Calculable=Calculable=...
[docs]class ComposedCalculable(Printable, metaclass=ABCMeta):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @warning Must be redefined
@abstractmethod
def __init__(self):
pass
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Expression
# @brief These are object of the kind: Name = Exponented
[docs]class Expression(ComposedCalculable):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param integer_or_letter A string or an integer
# @param objct The Exponented that will be at the right hand side
# @return One instance of Expression
def __init__(self, integer_or_letter, objct):
# just check if the given arguments are right
if not (type(integer_or_letter) is str
or (is_number(integer_or_letter)
and is_integer(integer_or_letter))):
# __
raise TypeError('Got: ' + str(type(integer_or_letter))
+ ' instead of a str or integer number')
if not (isinstance(objct, Exponented) or objct is None):
raise TypeError('Got: ' + str(type(integer_or_letter))
+ ' instead of Exponented|None')
self._name = integer_or_letter
self._right_hand_side = objct.clone()
# --------------------------------------------------------------------------
##
# @brief Returns the right hand side of the Expression e.g.
# the Expression in itself
[docs] def get_right_hand_side(self):
return self._right_hand_side
right_hand_side = property(get_right_hand_side,
doc="Right hand side of the object")
# --------------------------------------------------------------------------
##
# @brief Sets the right hand side of the Expression
[docs] def set_right_hand_side(self, arg):
if not (isinstance(arg, Exponented)):
raise TypeError('Got: ' + str(type(arg)) + ' instead of '
'Exponented')
self._right_hand_side = arg.clone()
# --------------------------------------------------------------------------
##
# @brief Creates a string of the given object in the given ML
# @param options Any options
# @return The formated string
[docs] def into_str(self, **options):
global expression_begins
required.package['amsmath'] = True
# Expression objects displaying
if is_number(self.name) and is_integer(self.name):
i = self.name
if i < len(alphabet):
final_name = MARKUP['open_text_in_maths'] \
+ alphabet[i] \
+ MARKUP['close_text_in_maths']
else:
nb_letters = len(alphabet)
final_name = MARKUP['open_text_in_maths'] \
+ alphabet[i - nb_letters * int(i / nb_letters)] \
+ MARKUP['close_text_in_maths'] \
+ MARKUP['opening_subscript'] \
+ MARKUP['open_text_in_maths'] \
+ str(int(i / nb_letters)) \
+ MARKUP['close_text_in_maths'] \
+ MARKUP['closing_subscript']
elif type(self.name) is str:
final_name = MARKUP['open_text_in_maths'] \
+ self.name \
+ MARKUP['close_text_in_maths']
expression_begins = True
options.update({'force_expression_begins': True})
return final_name \
+ MARKUP['equal'] \
+ self.right_hand_side.into_str(**options)
# --------------------------------------------------------------------------
##
# @brief Create a string of the expression's exp. & red. in the given ML
# @param options Any options
# @return The formated string of the expression's resolution
[docs] def auto_expansion_and_reduction(self, **options):
global expression_begins
aux_expr = self.right_hand_side
result = ""
# Complete expansion & reduction of any expression:o)
while aux_expr is not None:
result += MARKUP['opening_math_style2'] \
+ Expression(self.name,
aux_expr).into_str() \
+ MARKUP['closing_math_style2'] \
+ MARKUP['newline'] + "\n"
aux_expr = aux_expr.expand_and_reduce_next_step(**options)
return result
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Equality
# @brief These are object of the kind: Exponented = Exponented [= ...]
[docs]class Equality(ComposedCalculable, Substitutable):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param objcts is a [Exponented] of 2 elements at least
# @option equal_signs Contains a list of equal/not equal signs. Must be
# @option as long as len(objcts) - 1. The signs are "=" or
# "neq"
# @return One instance of Equality
def __init__(self, objcts, subst_dict=None, **options):
# just check if the given arguments are right
if not (isinstance(objcts, list)):
raise TypeError('Got: ' + str(type(objcts)) + ' instead of '
'a list (of two Exponenteds at least)')
if not len(objcts) >= 2:
raise TypeError('Got: ' + str(type(objcts)) + ' instead of '
'a list of TWO Exponenteds AT LEAST')
for i in range(len(objcts)):
if not isinstance(objcts[i], Exponented):
raise TypeError('Got: ' + str(type(objcts[i])) + ' instead of '
'an Exponented')
if 'equal_signs' in options:
if not type(options['equal_signs']) == list:
raise TypeError('Got: ' + str(type(options['equal_signs']))
+ ' instead of a list')
if not len(options['equal_signs']) == len(objcts) - 1:
raise TypeError('Got a list of: '
+ str(len(options['equal_signs']))
+ ' elements instead of '
+ str(len(objcts) - 1))
for i in range(len(options['equal_signs'])):
if not (options['equal_signs'][i] == '='
or options['equal_signs'][i] == 'neq'):
# __
raise TypeError(options['equal_signs'][i]
+ ' should be \'=\' or \'neq\'')
self._elements = []
self._equal_signs = []
for i in range(len(objcts)):
self._elements.append(objcts[i].clone())
if 'equal_signs' in options:
if i < len(options['equal_signs']):
sign_to_add = None
if options['equal_signs'][i] == '=':
sign_to_add = 'equal'
else:
sign_to_add = 'not_equal'
self._equal_signs.append(sign_to_add)
else:
self._equal_signs.append('equal')
Substitutable.__init__(self, subst_dict=subst_dict)
@property
def content(self):
"""The content to be substituted (list containing literal objects)."""
return self._elements
# --------------------------------------------------------------------------
##
# @brief Returns the elements of the Equality
[docs] def get_elements(self):
return self._elements
# --------------------------------------------------------------------------
##
# @brief Returns the equal signs series of the Equality
[docs] def get_equal_signs(self):
return self._equal_signs
elements = property(get_elements, doc="Elements of the object")
equal_signs = property(get_equal_signs, doc="Equal signs of the object")
# --------------------------------------------------------------------------
##
# @brief Creates a string of the given object in the given ML
# @param options Any options
# @return The formated string
[docs] def into_str(self, **options):
global expression_begins
# Equality objects displaying
expression_begins = True
result = ''
if ('force_expression_markers' in options
and options['force_expression_markers']):
# __
result += MARKUP['opening_math_style2']
options.update({'force_expression_begins': True})
result += self.elements[0].into_str(**options)
for i in range(len(self.elements) - 1):
options.update({'force_expression_begins': True})
result += MARKUP[self.equal_signs[i]] \
+ self.elements[i + 1].into_str(**options)
if ('force_expression_markers' in options
and options['force_expression_markers']):
# __
result += MARKUP['closing_math_style2']
return result
def __getitem__(self, i):
"""Make Equality indexable."""
return self._elements[i]
def __setitem__(self, i, data):
"""Make Equality indexable."""
if not isinstance(data, Exponented):
raise TypeError('data should be a Exponented')
self._elements[i] = data.clone()
def __len__(self):
"""Return the number of elements of the Equality."""
return len(self._elements)
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Equation
# @brief One degree one variable. Sum=Sum. Name, number, left/right hand side.
[docs]class Equation(ComposedCalculable):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param arg Equation|(Exponented, Exponented)|(RANDOMLY, ...)|Equality
# @return One instance of Equation
def __init__(self, arg, **options):
self._name = settings.default.EQUATION_NAME
self._number = ''
self._left_hand_side = None
self._right_hand_side = None
self._variable_letter = settings.default.MONOMIAL_LETTER
# First determine name and number of the equation
if 'name' in options and type(options['name']) is str:
self._name = options['name']
if 'number' in options and is_integer(options['number']):
self._number = options['number']
# Then the letter
if 'variable_letter_name' in options \
and type(options['variable_letter_name']) is str:
# __
self._variable_letter = options['variable_letter_name'][0]
# Then determine its left & right hand sides
if isinstance(arg, Equality):
if not len(arg) == 2:
raise RuntimeError('Impossible to turn into an Equation an '
'Equality having not exactly 2 members')
literals_list = list(set(gather_literals(arg[0])
+ gather_literals(arg[1])))
if not len(literals_list) == 1:
raise RuntimeError('create an Equation from an Equality '
'containing more than ONE unknown literal. '
'This one contains '
+ str(len(literals_list))
+ ' literals which are '
+ str([repr(elt) for elt in literals_list]))
if (not isinstance(literals_list[0], Value)
and not literals_list[0].is_literal()):
# __
raise ValueError('Expected a literal Value, instead of '
+ repr(literals_list[0]))
self.__init__((arg[0], arg[1]),
variable_letter_name=literals_list[0]
.get_first_letter(),
**options)
# Different cases of tuples
elif type(arg) == tuple and len(arg) == 2:
# 1st CASE
# Given objects
if (isinstance(arg[0], Exponented)
and isinstance(arg[1], Exponented)):
# __
if isinstance(arg[0], Sum):
# TO FIX ?
# this could be secured by checking the given Sum
# is not containing only one term which would be also
# a Sum
self._left_hand_side = arg[0]
else:
self._left_hand_side = Sum(arg[0])
if isinstance(arg[1], Sum):
# TO FIX ?
# this could be secured by checking the given Sum
# is not containing only one term which would be also
# a Sum
self._right_hand_side = arg[1]
else:
self._right_hand_side = Sum(arg[1])
# 2d CASE
# RANDOMLY !
elif arg[0] == RANDOMLY:
if arg[1] == 'basic_addition':
self._left_hand_side = Polynomial([
Monomial(('+', 1, 1)),
Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))])
self._right_hand_side = Sum(
Item((random.choice(['+', '-']),
random.randint(1, MAX_VALUE))))
self._left_hand_side.term[0]\
.set_letter(self.variable_letter)
elif arg[1] == 'basic_addition_r':
self._right_hand_side = Polynomial([
Monomial(('+', 1, 1)),
Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))])
self._left_hand_side = Sum(
Item((random.choice(['+', '-']),
random.randint(1, MAX_VALUE))))
self._right_hand_side.term[0]\
.set_letter(self.variable_letter)
elif arg[1] == 'any_basic_addition':
m1 = Monomial((random.choices(['+', '-'],
cum_weights=[0.8, 1])[0],
1, 1))
m1.set_letter(self.variable_letter)
m2 = Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))
m3 = Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))
if random.choice([True, False]):
cst_list = [m2, m3]
else:
cst_list = [m3, m2]
drawn_to_be_with_x = cst_list.pop()
if random.choice([True, False]):
polyn_list = [m1, drawn_to_be_with_x]
else:
polyn_list = [drawn_to_be_with_x, m1]
polyn = Polynomial([polyn_list.pop(),
polyn_list.pop()])
if random.choice([True, False]):
sides = [polyn, Sum(cst_list.pop())]
else:
sides = [Sum(cst_list.pop()), polyn]
self._left_hand_side = sides.pop()
self._right_hand_side = sides.pop()
elif arg[1] == 'basic_multiplication':
self._left_hand_side = Sum(
Monomial((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(2, MAX_VALUE),
1)))
self._right_hand_side = Sum(
Item((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(1, MAX_VALUE),
1)))
self._left_hand_side.term[0]\
.set_letter(self.variable_letter)
elif arg[1] == 'basic_multiplication_r':
self._right_hand_side = Sum(
Monomial((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(2, MAX_VALUE),
1)))
self._left_hand_side = Sum(
Item((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(1, MAX_VALUE),
1)))
self._right_hand_side.term[0]\
.set_letter(self.variable_letter)
elif arg[1] == 'any_basic_multiplication':
m1 = Monomial((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(2, MAX_VALUE),
1))
m1.set_letter(self.variable_letter)
m2 = Item((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(1, MAX_VALUE),
1))
if random.choice([True, False]):
items_list = [Sum(m1), Sum(m2)]
else:
items_list = [Sum(m2), Sum(m1)]
self._left_hand_side = items_list.pop()
self._right_hand_side = items_list.pop()
elif arg[1] == 'any_basic': # code duplication... done quickly
if random.choice([True, False]):
m1 = Monomial((random.choices(['+', '-'],
cum_weights=[0.75,
1])[0],
random.randint(2, MAX_VALUE),
1))
m1.set_letter(self.variable_letter)
m2 = Item((random.choices(['+', '-'],
cum_weights=[0.75, 1])[0],
random.randint(1, MAX_VALUE),
1))
if random.choice([True, False]):
items_list = [Sum(m1), Sum(m2)]
else:
items_list = [Sum(m2), Sum(m1)]
self._left_hand_side = items_list.pop()
self._right_hand_side = items_list.pop()
else:
cst_list = list()
m1 = Monomial((random.choices(['+', '-'],
cum_weights=[0.8, 1])[0],
1,
1))
m1.set_letter(self.variable_letter)
m2 = Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))
m3 = Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))
if random.choice([True, False]):
cst_list = [m2, m3]
else:
cst_list = [m3, m2]
drawn_to_be_with_x = cst_list.pop()
if random.choice([True, False]):
polyn_list = [m1, drawn_to_be_with_x]
else:
polyn_list = [drawn_to_be_with_x, m1]
polyn = Polynomial([polyn_list.pop(),
polyn_list.pop()])
if random.choice([True, False]):
sides = [polyn, Sum(cst_list.pop())]
else:
sides = [Sum(cst_list.pop()), polyn]
self._left_hand_side = sides.pop()
self._right_hand_side = sides.pop()
elif (arg[1] in ['classic', 'classic_r', 'classic_x_twice',
'any_classic']):
# __
# Let's build
# classic: ax + b = d | b + ax = d
# classic_r: d = ax + b | d = b + ax
# classic_x_twice: ax + b = cx + d | ax + b = cx |
# cx = ax + b | b + ax = cx + d etc.
ax = Monomial((random.choices(['+', '-'],
cum_weights=[0.65, 1])[0],
random.randint(1, MAX_VALUE),
1))
ax.set_letter(self.variable_letter)
b = Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))
cx = Monomial((random.choices(['+', '-'],
cum_weights=[0.65, 1])[0],
random.randint(1, MAX_VALUE),
1))
cx.set_letter(self.variable_letter)
d = Monomial((random.choice(['+', '-']),
random.randint(1, MAX_VALUE),
0))
if random.choice([True, False]):
polyn1 = Polynomial([ax, b])
else:
polyn1 = Polynomial([b, ax])
if arg[1] == 'classic' or arg[1] == 'classic_r':
polyn2 = Polynomial([d])
elif arg[1] == 'classic_x_twice':
if random.random() > 0.3:
if random.choice([True, False]):
polyn2 = Polynomial([cx, d])
else:
polyn2 = Polynomial([d, cx])
else:
polyn2 = Polynomial([cx])
elif arg[1] == 'any_classic':
random_nb = random.random()
if random_nb < 0.4:
if random.choice([True, False]):
polyn2 = Polynomial([cx, d])
else:
polyn2 = Polynomial([d, cx])
elif random_nb < 0.7:
polyn2 = Polynomial([d])
else:
polyn2 = Polynomial([cx])
if arg[1] == 'classic':
self._left_hand_side = polyn1
self._right_hand_side = polyn2
elif arg[1] == 'classic_r':
self._left_hand_side = polyn2
self._right_hand_side = polyn1
elif arg[1] in ['classic_x_twice', 'any_classic']:
if random.choice([True, False]):
box = [polyn1, polyn2]
else:
box = [polyn2, polyn1]
self._left_hand_side = box.pop()
self._right_hand_side = box.pop()
elif arg[1] == 'classic_with_fractions':
# the following code is copied from Calculation.py
# -> must be factorized
randomly_position = \
random.choices([n for n in range(17)],
weights=FRACTIONS_SUMS_SCALE_TABLE)[0]
chosen_seed_and_generator = FRACTIONS_SUMS_TABLE[
randomly_position]
seed = random.randint(2, chosen_seed_and_generator[1])
# The following test is only intended to avoid having
# "high" results too often. We just check if the common
# denominator will be higher than 75 (arbitrary)
# and if yes, we redetermine
# it once. We don't do it twice since we don't want to
# totally
# forbid high denominators.
if seed * chosen_seed_and_generator[0][0] \
* chosen_seed_and_generator[0][1] >= 75:
# __
seed = random.randint(2, chosen_seed_and_generator[1])
lil_box = [0, 1]
gen1 = chosen_seed_and_generator[0][lil_box.pop()]
gen2 = chosen_seed_and_generator[0][lil_box.pop()]
den1 = Item(gen1 * seed)
den2 = Item(gen2 * seed)
temp1 = random.randint(1, 20)
temp2 = random.randint(1, 20)
num1 = Item(temp1 // gcd(temp1, gen1 * seed))
num2 = Item(temp2 // gcd(temp2, gen2 * seed))
f1 = Fraction((random.choices(['+', '-'],
cum_weights=[0.7, 1])[0],
num1,
den1))
f2 = Fraction((random.choices(['+', '-'],
cum_weights=[0.7, 1])[0],
num2,
den2))
# END OF COPIED CODE --------------------------------------
box = list()
ax = Monomial((Fraction((
random.choices(['+', '-'], cum_weights=[0.7, 1])[0],
Item(random.randint(1, 10)),
Item(random.randint(2, 10))))
.simplified(), 1))
ax.set_letter(self.variable_letter)
b = Monomial((f1.simplified(), 0))
d = Monomial((f2.simplified(), 0))
if random.choice([True, False]):
box = [ax, b]
else:
box = [b, ax]
self._left_hand_side = Polynomial(box)
self._right_hand_side = Sum([d])
elif arg[1] in ['any_simple_expandable',
'any_double_expandable']:
# __
# SIMPLE:
# a monom0_polyn1 or a ±(...) on one side
# + one Monomial with the monom0_polyn1
# and one or two Monomials on the other side
# DOUBLE:
# The same plus another expandable.
# Creation of the expandables, to begin with...
if random.random() <= 0.8:
aux_expd_1 = Expandable((RANDOMLY,
'monom0_polyn1'),
max_coeff=9)
expd_kind = 'monom0_polyn1'
else:
sign = random.choice(['+', '-'])
aux_expd_1 = Expandable((
Monomial((sign, 1, 0)),
Polynomial((RANDOMLY, 9, 1, 2))))
if sign == '+':
expd_kind = '+(...)'
else:
expd_kind = 'monom0_polyn1'
# Now we fill the boxes to draw from
box_1 = []
box_2 = []
additional_Monomial = Monomial((RANDOMLY, 9, 1))
additional_Monomial2 = Monomial((RANDOMLY, 9, 1))
if additional_Monomial.degree == 0:
additional_Monomial2.set_degree(1)
box_1.append(aux_expd_1)
if expd_kind == '+(...)' or random.random() <= 0.5:
box_1.append(additional_Monomial)
box_2.append(Monomial((RANDOMLY, 9, 1)))
if random.random() <= 0.25:
# __
box_2.append(additional_Monomial2)
if arg[1] == 'any_double_expandable':
if random.random() <= 0.8:
aux_expd_2 = Expandable((RANDOMLY,
'monom0_polyn1'),
max_coeff=9)
else:
sign = random.choice(['+', '-'])
aux_expd_2 = Expandable((
Monomial((sign, 1, 0)),
Polynomial((RANDOMLY, 9, 1, 2))))
if random.random() <= 0.5:
box_1.append(aux_expd_2)
else:
box_2.append(aux_expd_2)
boxes = [box_1, box_2]
if random.choice([True, False]):
box_left, box_right = boxes
else:
box_right, box_left = boxes
random.shuffle(box_left)
random.shuffle(box_right)
self._left_hand_side = Sum(box_left)
self._right_hand_side = Sum(box_right)
# All other unforeseen cases: an exception is raised.
else:
raise TypeError('Got: ' + str(type(arg))
+ ' instead of (Exponented, Exponented)|'
'(RANDOMLY, <option>)')
# Another Equation to copy
elif isinstance(arg, Equation):
self._name = arg._name
self._number = arg.number
self._left_hand_side = arg.left_hand_side.clone()
self._right_hand_side = arg.right_hand_side.clone()
self._variable_letter = arg.variable_letter
else:
raise TypeError('Got: ' + str(type(arg))
+ ' instead of Equation|tuple')
# --------------------------------------------------------------------------
##
# @brief Returns the name of the object
@property
def name(self):
if self.number == '':
return MARKUP['opening_bracket'] \
+ MARKUP['open_text_in_maths'] \
+ self._name \
+ MARKUP['close_text_in_maths'] \
+ MARKUP['closing_bracket'] \
+ MARKUP['colon'] \
+ MARKUP['space']
else:
return MARKUP['opening_bracket'] \
+ MARKUP['open_text_in_maths'] \
+ self._name \
+ MARKUP['close_text_in_maths'] \
+ MARKUP['opening_subscript'] \
+ MARKUP['open_text_in_maths'] \
+ str(self.number) \
+ MARKUP['close_text_in_maths'] \
+ MARKUP['closing_subscript'] \
+ MARKUP['closing_bracket'] \
+ MARKUP['colon'] \
+ MARKUP['space']
# --------------------------------------------------------------------------
##
# @brief Returns the number of the Equality
[docs] def get_number(self):
return self._number
# --------------------------------------------------------------------------
##
# @brief Getter for left hand side
[docs] def get_left_hand_side(self):
return self._left_hand_side
# --------------------------------------------------------------------------
##
# @brief Getter for right hand side
[docs] def get_right_hand_side(self):
return self._right_hand_side
# --------------------------------------------------------------------------
##
# @brief Getter for the variable letter
[docs] def get_variable_letter(self):
return self._variable_letter
number = property(get_number, doc="Number of the Equation")
left_hand_side = property(get_left_hand_side,
doc="Left hand side of the Equation")
right_hand_side = property(get_right_hand_side,
doc="Right hand side of the Equation")
variable_letter = property(get_variable_letter,
doc="Variable letter of the Equation")
# --------------------------------------------------------------------------
##
# @brief Sets the number of the Equation
[docs] def set_number(self, arg):
if not type(arg) == int:
raise ValueError('arg should be an int')
self._number = str(arg)
# --------------------------------------------------------------------------
##
# @brief Setter for hand sides
# @return Nothing, just sets the given argument to the left hand side,
# turned into a Sum if necessary
[docs] def set_hand_side(self, left_or_right, arg):
if not (left_or_right == 'left' or left_or_right == 'right'):
raise ValueError('left_or_right should be \'left\' or \'right\'')
if isinstance(arg, Exponented):
if isinstance(arg, CommutativeOperation) and len(arg) == 1:
arg = arg.element[0]
if not isinstance(arg, Sum):
arg = Sum(arg)
if left_or_right == "left":
self._left_hand_side = arg
else:
self._right_hand_side = arg
else:
raise TypeError('arg should have been an Exponented, instead, got '
+ str(type(arg)))
# --------------------------------------------------------------------------
##
# @brief Creates a string of the given object in the given ML
# @param options Any options
# @return The formated string
[docs] def into_str(self, **options):
global expression_begins
# Equation objects displaying
beginning = ''
if 'display_name' in options:
beginning = self.name
left = self.left_hand_side.printed
right = self.right_hand_side.printed
egal_sign = MARKUP['equal']
if self.left_hand_side.contains_a_rounded_number() \
or self.right_hand_side.contains_a_rounded_number():
# __
egal_sign = MARKUP['simeq']
return beginning + left + egal_sign + right
# --------------------------------------------------------------------------
##
# @brief Raw display of the Equation (debugging method)
# @return A string containing "type: sign coeff × X ^ degree"
def __repr__(self):
return "\nEquation: " + str(self.name) \
+ " " + str(self.number) \
+ "\n Left hand side: " + repr(self.left_hand_side) \
+ "\n Right hand side: " + repr(self.right_hand_side) \
+ "\n Variable: " + str(self.variable_letter)
# --------------------------------------------------------------------------
##
# @brief Creates a string of the equation's resolution in the given ML
# @param options Any options
# @return The formated string of the equation's resolution
[docs] def auto_resolution(self, **options):
global expression_begins
# Complete resolution of the equation:o)
result = ""
if 'dont_display_equations_name' not in options:
result = MARKUP['opening_math_style2'] \
+ self.name \
+ MARKUP['closing_math_style2']
# result += MARKUP['newline']
uline1 = ""
uline2 = ""
if 'underline_result' in options and options['underline_result']:
uline1 = MARKUP['open_underline']
uline2 = MARKUP['close_underline']
required.package['ulem'] = True
eq_aux = None
if (isinstance(self, Equation)
and not isinstance(self, CrossProductEquation)):
# __
eq_aux = Equation(self)
elif isinstance(self, CrossProductEquation):
eq_aux = CrossProductEquation(self)
eq_aux1 = None
eq_aux2 = None
equation_did_split_in_two = False
go_on = True
step_nb = 0
while go_on:
step_nb += 1
if not equation_did_split_in_two:
next_eq_aux = eq_aux.solve_next_step(**options)
if not('skip_first_step' in options and step_nb == 1):
if next_eq_aux is None and 'unit' in options:
result += MARKUP['opening_math_style1'] \
+ uline1 \
+ eq_aux.into_str() \
+ MARKUP['open_text_in_maths'] \
+ " " + str(options['unit']) \
+ MARKUP['close_text_in_maths'] \
+ uline2 \
+ MARKUP['closing_math_style1']
else:
result += MARKUP['opening_math_style1'] \
+ eq_aux.into_str() \
+ MARKUP['closing_math_style1']
if (next_eq_aux is None or type(next_eq_aux) == str
or isinstance(next_eq_aux, tuple)):
# __
eq_aux = next_eq_aux
else:
eq_aux = Equation(next_eq_aux)
if isinstance(eq_aux, tuple):
(eq_aux1, eq_aux2) = eq_aux
equation_did_split_in_two = True
elif isinstance(eq_aux, str) or eq_aux is None:
go_on = False
else:
if isinstance(eq_aux1, Equation):
next_eq_aux1 = eq_aux1.solve_next_step(**options)
if isinstance(eq_aux2, Equation):
next_eq_aux2 = eq_aux2.solve_next_step(**options)
if eq_aux1 is not None or eq_aux2 is not None:
result += MARKUP['opening_math_style1']
if isinstance(eq_aux1, Equation):
if next_eq_aux1 is None:
result += uline1
result += eq_aux1.into_str()
if next_eq_aux1 is None and 'unit' in options:
result += MARKUP['open_text_in_maths'] \
+ " " + str(options['unit']) \
+ MARKUP['close_text_in_maths']
if next_eq_aux1 is None:
result += uline2
if isinstance(eq_aux2, Equation):
result += " " + _("or") + " "
elif eq_aux1 is not None:
result += eq_aux1
if isinstance(eq_aux2, Equation):
if next_eq_aux2 is None:
result += uline1
result += eq_aux2.into_str()
if next_eq_aux2 is None and 'unit' in options:
result += MARKUP['open_text_in_maths'] \
+ " " + str(options['unit']) \
+ MARKUP['close_text_in_maths']
if next_eq_aux2 is None:
result += uline2
elif eq_aux2 is not None:
result += eq_aux2
eq_aux2 = None
if not isinstance(eq_aux1, Equation) \
and not isinstance(eq_aux2, Equation):
# __
go_on = False
if eq_aux1 is not None or eq_aux2 is not None:
result += MARKUP['closing_math_style1']
if isinstance(eq_aux1, Equation):
eq_aux1 = eq_aux1.solve_next_step(**options)
if isinstance(eq_aux2, Equation):
eq_aux2 = eq_aux2.solve_next_step(**options)
if (not equation_did_split_in_two):
if eq_aux is None:
pass
else:
result += eq_aux + MARKUP['newline']
else:
if eq_aux1 is None and eq_aux2 is None:
pass
else:
if eq_aux1 is None:
result += eq_aux2 + MARKUP['newline']
else:
result += eq_aux1 + MARKUP['newline']
# if 'get_solution' in options and options['get_solution']:
return result
# --------------------------------------------------------------------------
##
# @brief Creates the next Equation object in the resolution
# @todo Expandables (which have to get checked first, btw) !
# @todo check the case -x = -7 where the - belongs to the Item's value
# @return An Equation
[docs] def solve_next_step(self, **options):
log = settings.dbg_logger.getChild('Equation.solve_next_step')
new_eq = Equation(self)
log.debug("Entering, with Equation: " + repr(new_eq))
# CASE 0: preparing the Equation: getting recursively rid of
# imbricated Sums
if isinstance(new_eq.left_hand_side.term[0], Sum) \
and len(new_eq.left_hand_side) == 1:
# __
log.debug("CASE-0s-left")
new_eq.set_hand_side("left", new_eq.left_hand_side.term[0])
return new_eq.solve_next_step(**options)
elif (isinstance(new_eq.right_hand_side.term[0], Sum)
and len(new_eq.right_hand_side) == 1):
# __
log.debug("CASE-0s-right")
new_eq.set_hand_side("right", new_eq.right_hand_side.term[0])
return new_eq.solve_next_step(**options)
if isinstance(new_eq.left_hand_side.term[0], Product) \
and len(new_eq.left_hand_side) == 1 \
and len(new_eq.left_hand_side.term[0]) == 1:
# __
log.debug("CASE-0p-left")
new_eq.set_hand_side("left",
Sum(new_eq.left_hand_side.term[0].factor[0]))
return new_eq.solve_next_step(**options)
elif (isinstance(new_eq.right_hand_side.term[0], Product)
and len(new_eq.right_hand_side) == 1
and len(new_eq.right_hand_side.term[0]) == 1):
# __
log.debug("CASE-0p-right")
new_eq.set_hand_side("right",
Sum(new_eq.right_hand_side.term[0].factor[0]))
return new_eq.solve_next_step(**options)
reduced_options = {
'details_level': options.get('details_level', 'maximum')
}
next_left_X = new_eq.left_hand_side.expand_and_reduce_next_step(
**reduced_options
)
next_right_X = new_eq.right_hand_side.expand_and_reduce_next_step(
**reduced_options
)
# next_left_C = new_eq.left_hand_side.calculate_next_step()
# next_right_C = new_eq.right_hand_side.calculate_next_step()
if not (isinstance(next_left_X, Calculable)
or isinstance(next_left_X, ComposedCalculable)):
# __
next_left_X_str = str(next_left_X)
else:
next_left_X_str = repr(next_left_X)
log.debug("'decimal_result' is in options? "
+ str('decimal_result' in options) + "; "
"len(new_eq.left_hand_side): "
+ str(len(new_eq.left_hand_side))
+ "\nnext_left_X is: " + next_left_X_str
+ "\nnext_right_X is: " + repr(next_right_X)
+ "\nnew_eq.left_hand_side.term[0].is_literal()? "
+ str(new_eq.left_hand_side.term[0].is_literal()) + "; "
"len(new_eq.right_hand_side): "
+ str(len(new_eq.right_hand_side))
+ "\nisinstance(new_eq.right_hand_side.term[0], "
"Fraction)? "
+ str(isinstance(new_eq.right_hand_side.term[0],
Fraction)))
at_left_one_literal_not_expandable_element = \
(len(new_eq.left_hand_side) == 1
and next_left_X is None
and not new_eq.left_hand_side.term[0].is_numeric()
and len(new_eq.right_hand_side) == 1)
if (at_left_one_literal_not_expandable_element
and isinstance(new_eq.left_hand_side.term[0], Function)
and new_eq.right_hand_side.is_numeric()):
log.debug("A Function to invert")
item_class = Item
if isinstance(new_eq.left_hand_side.term[0].var, AngleItem):
item_class = AngleItem
new_eq.set_hand_side('right',
item_class(new_eq.left_hand_side
.term[0].inv_fct(
new_eq.right_hand_side.term[0]
.evaluate(**options)))
.rounded(options['decimal_result']))
new_eq.set_hand_side('left',
new_eq.left_hand_side.term[0].var)
elif (at_left_one_literal_not_expandable_element
and isinstance(new_eq.right_hand_side.term[0], Fraction)
and new_eq.right_hand_side.term[0].is_reducible()
and 'skip_fraction_simplification' in options):
# __
log.debug('At left, one literal not reducible element; '
'at right, one reducible Fraction, but Fraction '
'simplification must be skipped.')
if 'decimal_result' in options:
log.debug('And decimal result is required')
new_eq.set_hand_side("right",
Item(new_eq
.right_hand_side.term[0]
.evaluate(**options)))
else:
log.debug('But no decimal result is required')
new_eq.set_hand_side("right",
new_eq.right_hand_side.term[0]
.completely_reduced())
elif (at_left_one_literal_not_expandable_element
and 'decimal_result' in options
and (isinstance(new_eq.right_hand_side.term[0], Quotient)
or isinstance(new_eq.right_hand_side.term[0], Function)
or isinstance(new_eq.right_hand_side.term[0], SquareRoot))):
# __
log.debug('At left, one literal not reducible element; '
'at right, one Quotient|Function|SquareRoot and '
'decimal result is required.')
options.update({'force_evaluation': True})
new_eq.set_hand_side("right",
new_eq.right_hand_side.
expand_and_reduce_next_step(**options))
# 1st CASE
# Expand & reduce each side of the Equation, whenever possible
elif (next_left_X is not None) or (next_right_X is not None):
# __
log.debug("1st CASE")
if next_left_X is not None:
# __
new_eq.set_hand_side("left", next_left_X)
if next_right_X is not None:
# __
new_eq.set_hand_side("right", next_right_X)
at_left_one_literal_not_expandable_element = \
(len(new_eq.left_hand_side) == 1
and next_left_X is None
and not new_eq.left_hand_side.term[0].is_numeric()
and len(new_eq.right_hand_side) == 1)
if (at_left_one_literal_not_expandable_element
and 'decimal_result' in options
and isinstance(new_eq.right_hand_side.term[0], Item)
and new_eq.right_hand_side.term[0]
.needs_to_get_rounded(options['decimal_result'])):
# __
new_eq.set_hand_side("right",
new_eq.right_hand_side.term[0]
.rounded(options['decimal_result']))
# 2d CASE
# Irreducible SUMS, like 3 + x = 5 or x - 2 = 3x + 7
# It seems useless to test if one side is reducible, if it was,
# then it would have been treated in the first case.
# After that case is treated, every literal term will be moved
# to the left, and every numeric term moved to the right.
elif (len(new_eq.left_hand_side) >= 2
or len(new_eq.right_hand_side) >= 2
or (len(new_eq.right_hand_side) == 1
and not new_eq.right_hand_side.term[0].is_numeric())):
# __
log.debug("2d CASE")
# All the literal objects will be moved to the left,
# all numeric will be moved to the right
log.debug("Current Equation: " + repr(self))
left_collected_terms = new_eq.left_hand_side.get_numeric_terms()
right_collected_terms = new_eq.right_hand_side.get_literal_terms()
log.debug("left content: " + repr(self.left_hand_side)
+ "\nright content: "
+ repr(self.right_hand_side))
log.debug("\nleft collected terms: "
+ "".join([repr(t) for t in left_collected_terms]))
log.debug("\nright collected terms: "
+ "".join([repr(t) for t in right_collected_terms]))
# Special case of equations like 5 = x - 9
# which should become x = 5 + 9 at the next line, instead of
# -x = -5 - 9 (not suited for Pythagorean Equations)
if new_eq.left_hand_side.is_numeric() \
and not new_eq.right_hand_side.is_numeric() \
and len(new_eq.right_hand_side) == 2 \
and len(right_collected_terms) == 1:
# __
log.debug("Entered in the Special Case [part of 2d Case]")
log.debug("isinstance(right_collected_terms[0], Product)? "
+ str(isinstance(right_collected_terms[0], Product))
+ "\nlen(right_collected_terms[0]) == 2? "
+ str(len(right_collected_terms[0]) == 2)
+ "\nlen(right_collected_terms[0]) == 1? "
+ str(len(right_collected_terms[0]) == 1)
+ "\nisinstance(right_collected_terms[0], Item)? "
+ str(isinstance(right_collected_terms[0], Item)))
if ((isinstance(right_collected_terms[0], Product)
and len(right_collected_terms[0]) == 2
and right_collected_terms[0][0].is_positive())
or (isinstance(right_collected_terms[0], Product)
and len(right_collected_terms[0]) == 1)
or (isinstance(right_collected_terms[0], Item)
and right_collected_terms[0].is_positive())):
# __
log.debug("Special Case [part 2]")
return Equation((new_eq.right_hand_side,
new_eq.left_hand_side))\
.solve_next_step(**options)
# Special Case 2:
# of Equations like 9 = 3x which should become x = 9/3
# and not -3x = -9
if new_eq.left_hand_side.is_numeric() \
and not new_eq.right_hand_side.is_numeric() \
and len(new_eq.right_hand_side) == 1 \
and isinstance(right_collected_terms[0], Product):
# __
log.debug("Special Case [part 3]")
return Equation((new_eq.right_hand_side,
new_eq.left_hand_side)) \
.solve_next_step(**options)
for term in left_collected_terms:
# log.debug("(left)term: " + str(term))
new_eq.left_hand_side.remove(term)
term.set_sign(sign_of_product(['-', term.sign]))
new_eq.set_hand_side("right",
Sum([new_eq.right_hand_side, term]))
log.debug("Now, right_hand_side looks like: "
+ repr(new_eq.right_hand_side))
for term in right_collected_terms:
# log.debug("(right)term: " + str(term))
new_eq.right_hand_side.remove(term)
# term.set_sign(sign_of_product(['-', term.sign]))
term = Product([term, Item(-1)]).reduce_()
new_eq.set_hand_side("left", Sum([new_eq.left_hand_side,
term]))
new_eq.left_hand_side.reduce_()
new_eq.right_hand_side.reduce_()
# log.debug("after reduction, "
# + "right_hand_side looks like: "
# + str(new_eq.right_hand_side))
# 3rd CASE
# Weird cases like 0 = 1 or 2 = 2
elif (new_eq.left_hand_side.term[0].is_numeric()
and (new_eq.right_hand_side.term[0].is_numeric())):
# __
log.debug("3rd CASE")
if new_eq.left_hand_side.term[0].raw_value == \
new_eq.right_hand_side.term[0].raw_value \
and new_eq.left_hand_side.get_sign() == \
new_eq.right_hand_side.get_sign():
# __
return _('Any value of {variable_name} is '
'solution of the equation.')\
.format(variable_name=new_eq.variable_letter)
else:
return _('This equation has no solution.')
# 4th CASE
# Irreducible PRODUCTS ax = b or -x = b or x = b,
# where a and b are Exponenteds.
# The Product in the left side can't be numeric, or it would
# have been already treated before
elif isinstance(new_eq.left_hand_side.term[0], Product):
# __
log.debug("4th CASE")
# Let's replace the possibly remaining Monomial of degree 0
# at the right by an equivalent Item or Fraction
if isinstance(new_eq.right_hand_side.term[0], Monomial):
if isinstance(new_eq.right_hand_side.term[0].factor[0],
Item):
# __
new_eq.right_hand_side\
.set_term(0,
Item(new_eq.right_hand_side.term[0]))
elif isinstance(new_eq.right_hand_side.term[0].factor[0],
Fraction):
# __
new_eq.right_hand_side\
.set_term(0,
Fraction(new_eq.right_hand_side.term[0]))
# Let's get the numeric Exponented to remove from the left:
coefficient = new_eq.left_hand_side.term[0].factor[0]
if coefficient.is_displ_as_a_single_1():
new_eq.set_hand_side("left",
Item(new_eq.left_hand_side.term[0]
.factor[1]))
return new_eq.solve_next_step(**options)
elif coefficient.is_displ_as_a_single_minus_1():
new_eq.left_hand_side.term[0].set_opposite_sign()
new_eq.right_hand_side.term[0].set_opposite_sign()
elif (isinstance(coefficient, Item)
and isinstance(new_eq.right_hand_side.term[0], Item)):
# __
new_eq.left_hand_side.term[0].set_factor(0, Item(1))
new_eq.set_hand_side("right",
Fraction(('+',
new_eq.right_hand_side.term[0],
Item(coefficient))))
new_eq.right_hand_side.term[0]\
.set_down_numerator_s_minus_sign()
else:
new_eq.left_hand_side.term[0].set_factor(0, Item(1))
new_eq.right_hand_side\
.set_term(0,
Quotient(('+',
new_eq
.right_hand_side
.term[0],
coefficient),
use_divide_symbol=True))
# 5th CASE
# Literal Items -x = b (or -x² = b) or x = b, or x² = b
# where a and b are (reduced/simplified) Exponenteds.
# The Item at the left can't be numeric, the case should have
# been treated before
elif (isinstance(new_eq.left_hand_side.term[0], Item)
and new_eq.left_hand_side.term[0].is_literal()):
# __
log.debug("5th CASE")
if new_eq.left_hand_side.term[0].get_sign() == '-':
new_eq.left_hand_side.term[0].set_opposite_sign()
new_eq.right_hand_side.term[0].set_opposite_sign()
log.debug("Swapped signs")
else:
# CASES x² = b
if new_eq.left_hand_side.term[0].exponent == Value(2):
if new_eq.right_hand_side.term[0].is_negative():
return _("This equation has no solution.")
elif new_eq.left_hand_side.is_displ_as_a_single_0():
new_eq.left_hand_side.term[0].set_exponent(Value(1))
else:
temp_sqrt1 = SquareRoot(new_eq.
right_hand_side.term[0])
temp_sqrt2 = SquareRoot(temp_sqrt1)
temp_sqrt2.set_sign('-')
temp_item = Item(new_eq.left_hand_side.term[0])
temp_item.set_exponent(Value(1))
new_eq1 = Equation((temp_item,
temp_sqrt1))
new_eq2 = Equation((temp_item,
temp_sqrt2))
if ('pythagorean_mode' in options
and options['pythagorean_mode']):
# __
new_eq2 = MARKUP['open_text_in_maths'] + ' '\
+ _('because {L} is positive.')\
.format(L=temp_item.printed)\
+ MARKUP['close_text_in_maths']
return (new_eq1, new_eq2)
# Now the exponent must be equivalent to a single 1
# or the algorithm just doesn't know how to solve further.
else:
log.debug("Returning None")
return None
# log.debug(
# "Before having thrown away the neutrals: "
# + str(new_eq))
# throwing away the possibly zeros left..
new_eq.set_hand_side("left",
new_eq.left_hand_side.throw_away_the_neutrals())
new_eq.set_hand_side("right",
new_eq.right_hand_side.throw_away_the_neutrals())
# log.debug(
# "After having thrown away the neutrals,"
# right_hand_side looks like: "
# + str(new_eq.right_hand_side))
log.debug("Leaving, with Equation: " + repr(new_eq))
return new_eq
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class CrossProductEquation
# @brief All objects that are displayable as Cross Product Equations
[docs]class CrossProductEquation(Equation):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param arg CrossProductEquation|(Quotient, Quotient)|
# (num1, num2, deno1, deno2)
# @param arg numx and denox are expected as Calculables
def __init__(self, arg):
if not (type(arg) == tuple or isinstance(arg, CrossProductEquation)):
raise ValueError('Got ' + str(type(arg))
+ 'instead of a tuple|CrossProductEquation')
elif type(arg) == tuple and not (len(arg) == 2 or len(arg) == 4):
raise ValueError('Got a tuple of length ' + str(len(arg))
+ 'instead of a tuple of length 2 or 4')
if isinstance(arg, CrossProductEquation):
self._name = arg.name
self._number = arg.number
self._left_hand_side = arg.left_hand_side.clone()
self._right_hand_side = arg.right_hand_side.clone()
self._variable_letter = arg.variable_letter
self._variable_position = arg.variable_position
self._variable_obj = arg.variable_obj.clone()
else:
self._name = settings.default.EQUATION_NAME
self._number = ''
if len(arg) == 2:
if not(isinstance(arg[0], Quotient)
and isinstance(arg[1], Quotient)):
# __
raise ValueError('Got a a tuple of ' + str(type(arg[0]))
+ ' and of ' + str(type(arg[1])
+ ' instead of a tuple of two Quotients'))
else:
self._left_hand_side = arg[0].clone()
self._right_hand_side = arg[1].clone()
elif len(arg) == 4:
if not(isinstance(arg[0], Calculable)
and isinstance(arg[1], Calculable)
and isinstance(arg[2], Calculable)
and isinstance(arg[3], Calculable)):
# __
raise ValueError('Got a tuple of ' + str(type(arg[0]))
+ ', ' + str(type(arg[1]))
+ ', ' + str(type(arg[2]))
+ 'and ' + str(type(arg[3]))
+ 'instead of a tuple of four '
'Calculables')
else:
self._left_hand_side = Quotient(('+', arg[0], arg[2]),
ignore_1_denominator=True)
self._right_hand_side = Quotient(('+', arg[1], arg[3]),
ignore_1_denominator=True)
# Let's find the variable
# In the same time, we'll determine its position and the var obj
stop = 0
literal_position = 0
literals = 0
variable_letter = ""
variable_obj = None
# Don't change the order of elt below, it is inspired by this...
# 0: x a 1: a x 2: a b 3: a b
# b c b c c x x c
for elt in [self.left_hand_side.numerator,
self.right_hand_side.numerator,
self.right_hand_side.denominator,
self.left_hand_side.denominator]:
if elt.is_literal():
literals += 1
variable_letter += elt.into_str()
variable_obj = elt.clone()
stop = 1
if not stop:
literal_position += 1
if not literals == 1:
raise ValueError('Got ' + str(literals) + 'literal objects'
'instead of exactly one literal object '
'among 4')
self._variable_letter = variable_letter
self._variable_position = literal_position
self._variable_obj = variable_obj
# --------------------------------------------------------------------------
##
# @brief Getter for the variable obj
[docs] def get_variable_obj(self):
return self._variable_obj
# --------------------------------------------------------------------------
##
# @brief Getter for the variable position
[docs] def get_variable_position(self):
return self._variable_position
variable_obj = property(get_variable_obj,
doc="Variable object of the Equation")
variable_position = property(get_variable_position,
doc="Variable position in the Equation")
# --------------------------------------------------------------------------
##
# @brief Creates the next Equation object in the resolution
# @return An Equation
[docs] def solve_next_step(self, **options):
temp_table = Table([[self.left_hand_side.numerator,
self.right_hand_side.numerator],
[self.left_hand_side.denominator,
self.right_hand_side.denominator]])
r1d = options.get('remove_1_deno', True)
new_eq = Equation((self.variable_obj,
temp_table.cross_product((0, 1),
self.variable_position,
remove_1_deno=r1d)))
if (self.left_hand_side.numerator.is_literal()
and self.left_hand_side.denominator.is_displ_as_a_single_1()):
new_eq = new_eq.solve_next_step(**options)
return new_eq
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Table
# @brief All objects that are displayable as Tables
[docs]class Table(Printable, Substitutable):
[docs] class SubstitutableList(list, Substitutable):
"""A list that can call substitute() on its elements."""
def __init__(self, *args, subst_dict=None):
list.__init__(self, *args)
Substitutable.__init__(self, subst_dict=subst_dict)
@property
def content(self):
"""
The content to be substituted (list containing literal objects).
"""
return self
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param arg [[Calculable], [Calculable]] (the Calculables' lists must
# have the same length)
def __init__(self, arg, displ_as_qe=False, ignore_1_denos=None,
subst_dict=None):
if not type(arg) == list:
raise TypeError('arg must be a list (of two lists)')
if not len(arg) == 2:
raise ValueError('Got a list of ' + str(len(arg)) + 'elements'
'instead of a list of 2 elements')
if not type(arg[0]) == list:
raise ValueError('Got ' + str(type(arg[0]))
+ 'instead of a list')
if not type(arg[1]) == list:
raise ValueError('Got ' + str(type(arg[1]))
+ 'instead of a list')
if not len(arg[0]) == len(arg[1]):
raise ValueError('Got two lists of different lengths: '
+ str(len(arg[0])) + ' and ' + str(len(arg[1]))
+ 'instead of two lists of the same length')
for j in range(2):
for i in range(len(arg[j])):
if not isinstance(arg[j][i], Calculable):
raise ValueError('Got: arg[' + str(j) + ']['
+ str(i) + '] being a: '
+ str(type(arg[j][i]))
+ ' instead of a Calculable')
if not isinstance(displ_as_qe, bool):
raise TypeError('displ_as_qe should be a boolean')
if ignore_1_denos is not None and not isinstance(ignore_1_denos, bool):
raise TypeError('if set, ignore_1_denos should be a boolean')
self._nb_of_cols = len(arg[0])
self._data = [Table.SubstitutableList(arg[0], subst_dict=None),
Table.SubstitutableList(arg[1], subst_dict=None)]
self._displ_as_qe = displ_as_qe
self._ignore_1_denos = displ_as_qe
if ignore_1_denos is not None:
self._ignore_1_denos = ignore_1_denos
Substitutable.__init__(self, subst_dict=subst_dict)
@property
def displ_as_qe(self):
return self._displ_as_qe
@property
def ignore_1_denos(self):
return self._ignore_1_denos
@property
def content(self):
return self._data
# --------------------------------------------------------------------------
##
# @brief Returns the Table's content as a list of two lists so it
# can be addressed
[docs] def get_cell(self):
return self._data
# --------------------------------------------------------------------------
cell = property(get_cell,
doc="t.cell is the complete Table t.cell[i][j] is a cell")
# --------------------------------------------------------------------------
##
# @brief Creates a string of the given object in the given ML
# @param options Any options
# @return The formated string
# @todo Separate this from the LaTeX format (seems difficult to do)
[docs] def into_str(self, as_a_quotients_equality=None, ignore_1_denos=None,
**options):
if as_a_quotients_equality is None:
as_a_quotients_equality = self._displ_as_qe
elif not isinstance(as_a_quotients_equality, bool):
raise TypeError('as_a_quotients_equality should be a boolean.')
if ignore_1_denos is None:
ignore_1_denos = self._ignore_1_denos
elif not isinstance(ignore_1_denos, bool):
raise TypeError('ignore_1_denos should be a boolean.')
result = ""
if as_a_quotients_equality:
for i in range(len(self)):
options['ignore_1_denominator'] = ignore_1_denos
result += Quotient(('+',
self.cell[0][i],
self.cell[1][i]
), **options).printed
if i < len(self) - 1:
result += MARKUP['equal']
else: # there, the table will be displayed normally, as a table
content = []
for i in range(2):
for j in range(len(self)):
content += [self.cell[i][j].printed]
result = shared.machine\
.create_table((2, len(self)),
content,
col_fmt=['c' for i in range(len(self))],
borders='all')
return result
# --------------------------------------------------------------------------
##
# @brief Returns the number of columns of the Table
def __len__(self):
return self._nb_of_cols
# --------------------------------------------------------------------------
##
# @brief Produces the cross product of a cell among 4 given
# @param cols: (nb of col 1, nb of col 2)
# @param x_position: position of the unknown variable to compute
# it will be 0, 1, 2 or 3
# 0: x a 1: a x 2: a b 3: a b
# b c b c c x x c
# @param options Any options
# @return A Quotient or possibly a Fraction
[docs] def cross_product(self, col, x_position, remove_1_deno=True, **options):
if col[0] >= len(self) or col[1] >= len(self):
raise ValueError(str(col[0]) + ' or ' + str(col[1])
+ 'should be < ' + str(len(self)))
if x_position not in [0, 1, 2, 3]:
raise ValueError(str(x_position) + 'should be in [0, 1, 2, 3]')
num = None
if x_position == 0 or x_position == 2:
num = Product([self.cell[0][col[1]],
self.cell[1][col[0]]]).throw_away_the_neutrals()
elif x_position == 1 or x_position == 3:
num = Product([self.cell[0][col[0]],
self.cell[1][col[1]]]).throw_away_the_neutrals()
deno = None
if x_position == 0:
deno = self.cell[1][col[1]]
if deno.is_displ_as_a_single_1() and remove_1_deno:
return num
if (self.cell[0][col[1]].is_displ_as_a_single_int()
and self.cell[1][col[0]].is_displ_as_a_single_int()
and self.cell[1][col[1]].is_displ_as_a_single_int()):
# __
return Fraction((num, deno))
else:
return Quotient(('+', num, deno))
elif x_position == 1:
deno = self.cell[1][col[0]]
if deno.is_displ_as_a_single_1() and remove_1_deno:
return num
if (self.cell[0][col[0]].is_displ_as_a_single_int()
and self.cell[1][col[1]].is_displ_as_a_single_int()
and self.cell[1][col[0]].is_displ_as_a_single_int()):
# __
return Fraction((num, deno))
else:
return Quotient(('+', num, deno))
elif x_position == 2:
deno = self.cell[0][col[0]]
if deno.is_displ_as_a_single_1() and remove_1_deno:
return num
if (self.cell[0][col[1]].is_displ_as_a_single_int()
and self.cell[1][col[0]].is_displ_as_a_single_int()
and self.cell[0][col[0]].is_displ_as_a_single_int()):
# __
return Fraction((num, deno))
else:
return Quotient(('+', num, deno))
elif x_position == 3:
deno = self.cell[0][col[1]]
if deno.is_displ_as_a_single_1() and remove_1_deno:
return num
if (self.cell[0][col[0]].is_displ_as_a_single_int()
and self.cell[1][col[1]].is_displ_as_a_single_int()
and self.cell[0][col[1]].is_displ_as_a_single_int()):
# __
return Fraction((num, deno))
else:
return Quotient(('+', num, deno))
# --------------------------------------------------------------------------
##
# @brief Returns True if the Table is entirely numeric
[docs] def is_numeric(self, displ_as=False):
for i in range(2):
for j in range(len(self)):
if not self.cell[i][j].is_numeric(displ_as=displ_as):
return False
return True
[docs] def into_crossproduct_equation(self,
col0=0,
col1=1) -> CrossProductEquation:
"""
Create a CrossProductEquation from two columns.
Ensure there is only one literal among the four cells before using it.
:param col0: the number of the first column to use
:param col1: the number of the second column to use
"""
return CrossProductEquation((self.cell[0][col0],
self.cell[0][col1],
self.cell[1][col0],
self.cell[1][col1]))
[docs] def auto_resolution(self, col0=0, col1=1, subst_dict=None, **options):
result = MARKUP['opening_math_style1'] + self.printed\
+ MARKUP['closing_math_style1']
self.substitute(subst_dict=subst_dict)
return result + self.into_crossproduct_equation(col0=col0, col1=col1)\
.auto_resolution(**options)
# ------------------------------------------------------------------------------
# --------------------------------------------------------------------------
# ------------------------------------------------------------------------------
##
# @class Table_UP
# @brief All objects that are displayable as proportional Tables but uncomplete
[docs]class Table_UP(Table):
# --------------------------------------------------------------------------
##
# @brief Constructor
# @param coeff nb|numericCalculable
# @param first_line [nb|numericCalculable]
# @param info [None|(None|literalCalculable,
# None|literalCalculable)]
# info and first_line should have the same length
# info should contain at least one None|(None, None) element
# (means the column is completely numeric)
def __init__(self, coeff, first_line, info, displ_as_qe=False):
log = settings.dbg_logger.getChild('Table_UP.init')
if (not is_number(coeff)
and not (isinstance(coeff, Calculable) and coeff.is_numeric())):
# __
raise ValueError('Got:' + str(type(coeff))
+ ' instead of a number or a numeric Calculable')
if not type(first_line) == list:
raise ValueError('Got:' + str(type(first_line))
+ ' instead of a list ')
if not type(info) == list:
raise ValueError('Got:' + str(type(info))
+ ' instead of a list ')
if not len(info) == len(first_line):
raise ValueError('Got: two lists of lengths ' + str(len(info))
+ ' and ' + str(len(first_line))
+ ' instead of two lists of the same length.')
for elt in first_line:
if (elt is not None and not is_number(elt)
and not (isinstance(elt, Calculable) and elt.is_numeric())):
# __
raise ValueError('Got:' + str(type(elt)) + ' ' + repr(elt)
+ 'instead of None | nb | numericCalculable')
complete_cols = []
literals_positions = {}
col_nb = 0
for i in range(len(first_line)):
if first_line[i] is None and (info[i] is None
or info[i] == (None, None)):
# __
raise ValueError('Got first_line[i] and info[i] being'
' both equal to None though only one of '
'them can be None in the same time')
elt = info[i]
log.debug('elt: ' + str(elt))
if not (elt is None or type(elt) == tuple):
raise ValueError('Got:' + str(type(elt))
+ ' instead of either None or a tuple ')
if elt is None or elt == (None, None):
complete_cols += [col_nb]
if type(elt) == tuple:
if not len(elt) == 2:
raise ValueError('Got: a tuple of length ' + str(len(elt))
+ 'instead of a tuple of length 2')
if not ((isinstance(elt[0], Calculable)
and elt[0].is_literal())
or elt[0] is None):
# __
raise ValueError('Got:' + str(elt[0])
+ ' instead of None|literalCalculable')
if not ((isinstance(elt[1], Calculable)
and elt[1].is_literal())
or elt[1] is None):
# __
raise ValueError('Got:' + str(type(elt[1]))
+ ' instead of None|literalCalculable')
if elt[0] in literals_positions:
raise ValueError('Got:' + elt[0].into_str()
+ ' being already in the Table,'
+ ' but it should be there only once.')
else:
if (isinstance(elt[0], Calculable)
and not isinstance(elt[1], Calculable)):
# __
literals_positions[elt[0]] = col_nb
if elt[1] in literals_positions:
raise ValueError('Got:' + elt[1].into_str()
+ ' being already in the Table.'
+ ' but it should be there only once')
else:
if (isinstance(elt[1], Calculable)
and not isinstance(elt[0], Calculable)):
# __:
literals_positions[elt[1]] = col_nb
col_nb += 1
if len(complete_cols) == 0:
raise ValueError('Got:' + "no complete column found",
"there should be at least one complete")
# Now everything is clean, let's set the fields
self._coeff = coeff
second_line = []
for i in range(len(first_line)):
if first_line[i] is None:
second_line += [None]
else:
second_line += [Item(Product([coeff,
first_line[i]]).evaluate())]
data = [[], []]
for i in range(len(first_line)):
if info[i] is None:
data[0] += [first_line[i]]
data[1] += [second_line[i]]
elif first_line[i] is None:
data[0] += [info[i][0]]
data[1] += [info[i][1]]
else:
if info[i][0] is None:
data[0] += [first_line[i]]
if info[i][1] is None:
data[1] += [second_line[i]]
else:
data[1] += [info[i][1]]
else:
data[0] += [info[i][0]]
if info[i][1] is None:
data[1] += [second_line[i]]
else:
data[1] += [info[i][1]]
# for i in xrange(len(data[0])):
# if data[0][i] is None:
# d0 = "None"
# else:
# d0 = repr(data[0][i])
# if data[1][i] is None:
# d1 = "None"
# else:
# d1 = repr(data[1][i])
# print "data[0][" + str(i) + "] = " + d0 + "\n"
# print "data[1][" + str(i) + "] = " + d1 + "\n"
Table.__init__(self, data, displ_as_qe=displ_as_qe)
for elt in literals_positions:
col_ref = literals_positions[elt]
distance = len(data[0])
final_col = None
for i in range(len(complete_cols)):
if abs(complete_cols[i] - col_ref) <= distance:
final_col = complete_cols[i]
distance = abs(complete_cols[i] - col_ref)
literals_positions[elt] = (col_ref, final_col)
self._crossproducts_info = literals_positions
# --------------------------------------------------------------------------
##
# @brief Returns the Table's coefficient
[docs] def get_coeff(self):
return self._coeff
# --------------------------------------------------------------------------
##
# @brief Returns the info about Cross Products
[docs] def get_crossproducts_info(self):
return self._crossproducts_info
coeff = property(get_coeff,
doc="the coefficient of the Table_UP")
crossproducts_info = property(get_crossproducts_info,
doc="infos about the cross products")
# for instance, {'EF': (2,0), "GH": (3,0)} means Item 'EF' can
# be calculated by a CrossProduct using columns 2 and 0, etc.
# --------------------------------------------------------------------------
##
# @argument arg is expected to be an object that exists in the cp info
# @brief Returns the CrossProductEquation matching the given arg
[docs] def into_crossproduct_equation(self, arg):
if arg not in self.crossproducts_info:
raise ValueError('Got: ' + str(arg)
+ ' instead of an object expected to exist'
'in self.crossproducts_info')
col0 = self.crossproducts_info[arg][0]
col1 = self.crossproducts_info[arg][1]
col_temp = col1
if col0 > col1:
col1 = col0
col0 = col_temp
return CrossProductEquation((self.cell[0][col0],
self.cell[0][col1],
self.cell[1][col0],
self.cell[1][col1]))
[docs]class QuotientsEquality(Table):
"""A shortcut to create Tables as quotients equalities."""
def __init__(self, arg, displ_as_qe=True, ignore_1_denos=True,
subst_dict=None):
"""Initialization of the Table as a Quotients equality."""
Table.__init__(self, arg, displ_as_qe=displ_as_qe,
ignore_1_denos=ignore_1_denos,
subst_dict=subst_dict)