def parse_ode(lines, varnames): """ Input: Output: list of SparsePolynomial representing this ODE system ordered as variables in the ring """ eqs_raw = dict() plhs = re.compile("d\((\w+)\)") for l in lines: if plhs.search(l): lhs, rhs = l.split("=") lhs = plhs.search(lhs).groups(1)[0] rhs = rhs.strip() eqs_raw[lhs] = rhs var_to_ind = {v: i for i, v in enumerate(varnames)} eqs = dict() for lhs, rhs in eqs_raw.items(): if lhs not in varnames: raise ValueError(f"Variable {lhs} is not in the list of variables") try: eqs[lhs] = SparsePolynomial.from_string(rhs, varnames, var_to_ind) except TypeError as e: print(rhs) print(e) for v in varnames: if v not in eqs: eqs[v] = SparsePolynomial(varnames, QQ) return [eqs[v] for v in varnames]
def parse_reactions(lines, varnames): """ Input: lines with reactions, each reaction of the form "reactants -> products, rate" and varnames Output: the list of corresponding equations """ raw_reactions = [] var_to_ind = {v: i for i, v in enumerate(varnames)} for l in lines: if "," not in l: continue reaction, rate = separate_reation_rate(l) lhs, rhs = reaction.split("->") raw_reactions.append((lhs.strip(), rhs.strip(), rate.strip())) eqs = {v: SparsePolynomial(varnames, QQ) for v in varnames} for lhs, rhs, rate in raw_reactions: rate_poly = SparsePolynomial.from_string(rate, varnames, var_to_ind) ldict = species_to_multiset(lhs) rdict = species_to_multiset(rhs) monomial = tuple((var_to_ind[v], mult) for v, mult in ldict.items()) reaction_poly = rate_poly * SparsePolynomial(varnames, QQ, {monomial: QQ(1)}) for v, mult in rdict.items(): eqs[v] += reaction_poly * mult for v, mult in ldict.items(): eqs[v] += reaction_poly * (-mult) return [eqs[v] for v in varnames]
def do_lumping(polys, observable, new_vars_name='y', print_system=False, print_reduction=True, out_format="sympy", loglevel="INFO", initial_conditions=None): """ Main function, performs a lumping of a polynomial ODE system Input - polys - the right-hand side of the system - observable - a nonempty list of linear forms in state variables that must be kept nonlumped - new_vars_name (optional) - the name for variables in the lumped polynomials - print_system and print_reduction (optional) - whether to print the original system and the result, respectively on the screen - out_format - "sympy" or "internal", the way the output polynomials should be represeted the options are sympy polynomials and SparsePolynomial - loglevel - INFO (only essential information) or DEBUG (a lot of infromation about the computation process) Output a tuple (the right-hand side of an aggregated system, new_variables) """ logging.basicConfig( format='%(asctime)s %(levelname)-8s %(message)s', level=logging.INFO if loglevel == "INFO" else logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S', filename="lumper_debug.log") logging.debug("Starting aggregation") if isinstance(polys[0], SparsePolynomial): logging.debug("Input is in the SparsePolynomial format") else: logging.debug("Input is expected to be in SymPy format") polys = [SparsePolynomial.from_sympy(p) for p in polys] observable = [SparsePolynomial.from_sympy(ob) for ob in observable] result = do_lumping_internal(polys, observable, new_vars_name, print_system, print_reduction, initial_conditions) if initial_conditions is not None: eval_point = [initial_conditions.get(v, 0) for v in polys[0].gens] result["new_ic"] = [] for vect in result["subspace"]: result["new_ic"].append( sum([p[0] * p[1] for p in zip(eval_point, vect)])) if out_format == "sympy": out_ring = result["polynomials"][0].get_sympy_ring() result["polynomials"] = [ out_ring(p.get_sympy_dict()) for p in result["polynomials"] ] elif out_format == "internal": pass else: raise ValueError(f"Unknown output format {out_format}") return result
def lcm_rec(arr, l, u): if u - l == 1: return arr[l] mid = (u + l) // 2 res = SparsePolynomial.lcm( [lcm_rec(arr, l, mid), lcm_rec(arr, mid, u)]) return res
def perform_change_of_variables(self, polys, new_vars_name='y'): """ Restrict a polynomial system of ODEs with the rhs given by polys (SparsePolynomial) to the subspace new_vars_name (optional) - the name for variables in the lumped polynomials """ old_vars = polys[0].gens domain = polys[0].domain new_vars = [new_vars_name + str(i) for i in range(self.dim())] pivots = set(self.parametrizing_coordinates()) lpivots = sorted(pivots) basis = self.basis() # plugging all nonpivot variables with zeroes logging.debug("Plugging zero to nonpivot coordinates") shrinked_polys = [] for p in polys: filtered_dict = dict() for monom, coef in p.dataiter(): new_monom = [] skip = False for var, exp in monom: if var not in pivots: skip = True break else: new_monom.append((lpivots.index(var), exp)) if not skip: new_monom = tuple(new_monom) filtered_dict[new_monom] = coef shrinked_polys.append( SparsePolynomial(new_vars, domain, filtered_dict)) logging.debug("Constructing new polys") new_polys = [ SparsePolynomial(new_vars, domain) for _ in range(self.dim()) ] for i, vec in enumerate(basis): logging.debug(f" Polynomial number {i}") for j in vec.nonzero_iter(): # ordering is important due to the implementation of # multiplication for SparsePolynomial new_polys[i] += shrinked_polys[j] * vec._data[j] return new_polys
def construct_matrices_from_rational_functions(rational_functions): """ Computes Jacobian, pulls out common denominator, and constructs matrices J_1^T, ..., J_N^T from the remaining polynomial matrix Input - rational_functions - the right-hand side of the system of ODEs (f_1, ..., f_n) represented by RationalFunction Output a list of matrices (SparseMatrix) J_1^T, ..., J_N^T """ logging.debug("Starting constructing matrices (RationalFunction)") variables = rational_functions[0].gens field = rational_functions[0].domain # Compute Jacobian J = [[rf.derivative(v) for rf in rational_functions] for v in variables] def lcm_rec(arr, l, u): if u - l == 1: return arr[l] mid = (u + l) // 2 res = SparsePolynomial.lcm( [lcm_rec(arr, l, mid), lcm_rec(arr, mid, u)]) return res denoms = [rf.denom for rf in rational_functions] d = list( filter((lambda x: x != SparsePolynomial.from_string("1", [])), denoms)) lcm = lcm_rec(d, 0, len(d)) lcm = lcm * lcm p = [lcm // (denom * denom) for denom in denoms] # Pull out the common denominator poly_J = [] for i in range(len(J)): poly_J_row = [] for j in range(len(J[i])): poly_J_row.append(J[i][j].num * p[j]) poly_J.append(poly_J_row) # Work with remaining polynomial matrix as in construct_matrices_from_polys jacobians = dict() for row_ind, poly_row in enumerate(poly_J): for col_ind, poly in enumerate(poly_row): p_ind = row_ind * len(poly_row) + col_ind logging.debug("Processing numerator polynomial number %d", p_ind) for m, coef in poly.dataiter(): if m not in jacobians: jacobians[m] = SparseRowMatrix(len(variables), field) jacobians[m].increment(row_ind, col_ind, coef) return jacobians.values()
def from_string(s, varnames, var_to_ind = None): """ Parsing a string to a rational function, sting is allowed to include floating-point numbers in the standard and scientific notation, they will be converted to rationals IMPORTANT: String must contain only one "/"! """ if "/" not in s: num_str = s denom_str = "1" else: split = s.split("/") if len(split) == 2: num_str = split[0] denom_str = split[1] else: raise NotImplementedError num = SparsePolynomial.from_string(num_str, varnames, var_to_ind) denom = SparsePolynomial.from_string(denom_str, varnames, var_to_ind) return RationalFunction(num, denom)
def extract_observables(lines, varnames): """ Input: lines of the partitions section Output: list of SparsePolynomial representing the observables """ var_to_ind = {v : i for i, v in enumerate(varnames)} sets = [m.groups(1)[0] for m in re.finditer("\{([^\{\}]*)\}", " ".join(lines))] observables = [] for s in sets: obs_as_str = "+".join(re.split("\s*,\s*", s)) obs_poly = SparsePolynomial.from_string(obs_as_str, varnames, var_to_ind) observables.append(obs_poly) return observables
lumped_polys_values = [evalp(p, specialization_lumped) for p in lumped_system] assert(polys_values_lumped == lumped_polys_values) print(test_name + ": lumping is correct") ############################################### if __name__ == "__main__": # Example 1 R = sympy.polys.rings.vring(["x0", "x1", "x2"], QQ) polys = [x0**2 + x1 + x2, x2, x1] lumping = do_lumping(polys, [x0], print_reduction=False, initial_conditions={"x0" : 1, "x1" : 2, "x2" : 5}) check_lumping("Example 1", polys, lumping, 2) assert lumping["new_ic"] == [QQ(1), QQ(7)] # Example 2 polys = [x1**2 + 4 * x1 * x2 + 4 * x2**2, x1 + 2 * x0**2, x2 - x0**2] lumping = do_lumping(polys, [x0], print_reduction=False) check_lumping("Example 2", polys, lumping, 2) # PP for n = 2 system = read_system("e2.ode") lumping = do_lumping( system["equations"], [SparsePolynomial.from_string("S0", system["variables"])], print_reduction=False ) check_lumping("PP for n = 2", system["equations"], lumping, 12) ############################################
def do_lumping_internal(polys, observable, new_vars_name='y', print_system=True, print_reduction=False, ic=None): """ Performs a lumping of a polynomial ODE system represented by SparsePolynomial Input - polys - the right-hand side of the system - observable - a nonempty list of linear forms in state variables that must be kept nonlumped - new_vars_name (optional) - the name for variables in the lumped polynomials - verbose (optional) - whether to report the result on the screen or not Output a tuple (the right-hand side of an aggregated system, new_variables) """ logging.basicConfig( format='%(asctime)s %(levelname)-8s %(message)s', level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S', filename="lumper_debug.log" ) logging.debug("Starting aggregation") # Reduce the problem to the common invariant subspace problem vars_old = polys[0].gens field = polys[0].domain matrices = construct_matrices(polys) # Find a lumping vectors_to_include = [] for linear_form in observable: vec = SparseVector.from_list(linear_form.linear_part_as_vec(), field) vectors_to_include.append(vec) lumping_subspace = find_smallest_common_subspace(matrices, vectors_to_include) lumped_polys = lumping_subspace.perform_change_of_variables(polys, new_vars_name) new_ic = None if ic is not None: eval_point = [ic.get(v, 0) for v in polys[0].gens] new_ic = [] for vect in lumping_subspace.basis(): new_ic.append(sum([p[0] * p[1] for p in zip(eval_point, vect.to_list())])) # Nice printing vars_new = lumped_polys[0].gens if print_system: print("Original system:") for i in range(len(polys)): print(f"{vars_old[i]}' = {polys[i]}") print("Outputs to fix:") print(", ".join(map(str, observable))) if print_reduction: print("New variables:") for i in range(lumping_subspace.dim()): new_var = SparsePolynomial(vars_old, field) for j in range(len(vars_old)): if lumping_subspace.basis()[i][j] != 0: new_var += SparsePolynomial(vars_old, field, {((j, 1),) : lumping_subspace.basis()[i][j]}) print(f"{vars_new[i]} = {new_var}") if new_ic is not None: print("New initial conditions:") for v, val in zip(vars_new, new_ic): print(f"{v}(0) = {float(val)}") print("Lumped system:") for i in range(lumping_subspace.dim()): print(f"{vars_new[i]}' = {lumped_polys[i]}") return {"polynomials" : lumped_polys, "subspace" : [v.to_list() for v in lumping_subspace.basis()], "new_ic" : new_ic}
def simplify(self): gcd = SparsePolynomial.gcd([self.num, self.denom]) self.num = self.num // gcd self.denom = self.denom // gcd
rf2dx_expected = RationalFunction.from_string("(2*y**2*x)/(z**2)", varnames) rf2dx_test = rf2.derivative('x') print("Expected: \t", rf2dx_expected) print("Actual: \t", rf2dx_test) rf2dz_expected = RationalFunction.from_string("(-(2*x**2*y**2))/(z**3)", varnames) rf2dz_test = rf2.derivative('z') print("Expected: \t", rf2dz_expected) print("Actual: \t", rf2dz_test) rf = RationalFunction.from_string("(x)/(2 * y**2)", varnames) rf_dz = rf.derivative('z') print(rf_dz) sp1 = SparsePolynomial.from_string("2*x**23 + 4", ['x']) sp2 = SparsePolynomial.from_string("2*x**23 + 4", ['x']) assert sp1 == sp2 rf1 = RationalFunction.from_string("x/y",['x','y']) rf2 = RationalFunction.from_string("x/y",['x','y']) assert rf1 == rf2 print("--- LCM Test --------------------------------------------------------") sp1 = SparsePolynomial.from_string("x*y**2 + x**2*y", ['x','y']) sp2 = SparsePolynomial.from_string("x**2*y**2", ['x','y']) lcm = SparsePolynomial.lcm([sp1,sp2]) print("Expected: \t", "x**2*y**3 + x**3*y**2") print("Actual: \t", lcm) print("--- Division Test ---------------------------------------------------") sp1 = SparsePolynomial.from_string("x**2 - 1", ['x'])
import sys import time from sympy import QQ sys.path.insert(0, "../") sys.path.insert(0, "./../../") import parser import clue from sparse_polynomial import SparsePolynomial system = parser.read_system("BIOMD0000000033.ode") obs = SparsePolynomial.from_string("AktInactive", system['variables']) start = time.time() lumped = clue.do_lumping(system['equations'], [obs], print_system=True) end = time.time() print(f"The size of the original model is {len(system['equations'])}") print(f"The size of the reduced model is {len(lumped['polynomials'])}") print(f"Computation took {end - start} seconds")
# Model generated from: # Borisov, N. M., Chistopolsky, A. S., Faeder, J. R., & Kholodenko, B. N. # Domain-oriented reduction of rule-based network models. IET systems biology, 2(5), 342-351, 2008. # Source: https://www.ncbi.nlm.nih.gov/pmc/articles/PMC2628550/bin/NIHMS80246-supplement-Supplement_4.doc ## import sys import time from sympy import QQ sys.path.insert(0, "../") sys.path.insert(0, "./../../") import parser import clue from sparse_polynomial import SparsePolynomial system = parser.read_system("OrderedPhosphorylation.ode") obs = SparsePolynomial.from_string("s0", system['variables']) start = time.time() lumped = clue.do_lumping(system['equations'], [obs]) end = time.time() print(f"The size of the original model is {len(system['equations'])}") print(f"The size of the reduced model is {len(lumped['polynomials'])}") print(f"Computation took {end - start} seconds")
############################################### obss = { "BIOMD0000000504.ode": [["cFos_P", "cJun_P"], ["MMP1_mRNA", "MMP13_mRNA", "TIMP1_mRNA"], ["MMP1", "MMP13", "ColFrag"], ["JAK1_P", "JNK_P", "cJun_P", "cJun_dimer", "STAT3_P_nuc", "STAT3_P_cyt"]], "fceri_ji.ode": [["S0"], ["S2", "S178", "S267", "S77"], ["S2 + S178 + S267 + S77"], ["S7"], ["S1"]], "e2.ode": [["S0"], ["S1"]], "Barua.ode": [["aS000"], ["aS027"]] } if __name__ == "__main__": path = sys.argv[1] name = path[path.rindex('/') + 1:] system = read_system("{0}".format(path)) obs_sets = obss[name] for obs_set in obs_sets: obs_polys = [ SparsePolynomial.from_string(s, system['variables']) for s in obs_set ] do_lumping(system["equations"], obs_polys)
def do_lumping_internal(rhs, observable, new_vars_name='y', print_system=True, print_reduction=False, ic=None, discard_useless_matrices=True): """ Performs a lumping of a polynomial ODE system represented by SparsePolynomial Input - polys - the right-hand side of the system - observable - a nonempty list of linear forms in state variables that must be kept nonlumped - new_vars_name (optional) - the name for variables in the lumped polynomials - verbose (optional) - whether to report the result on the screen or not Output a tuple (the right-hand side of an aggregated system, new_variables) """ logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', level=logging.DEBUG, datefmt='%Y-%m-%d %H:%M:%S', filename="lumper_debug.log") logging.debug("Starting aggregation") # Reduce the problem to the common invariant subspace problem vars_old = rhs[0].gens field = rhs[0].domain deleted = 0 matrices = construct_matrices(rhs) print(f"-> There are {len(matrices)} matrices in total.") start = timeit.default_timer() # Proceed only with matrices that are linearly independent if discard_useless_matrices: matrices = sorted(matrices, key=lambda m: m.nonzero_count()) vectors_of_matrices = [m.to_vector() for m in matrices] subspace = Subspace(field) for i in range(len(vectors_of_matrices)): pivot_index = subspace.absorb_new_vector(vectors_of_matrices[i]) if pivot_index < 0: del matrices[i - deleted] deleted += 1 logging.debug(f"Discarded {deleted} linearly dependant matrices") print( f"-> I discarded {deleted} linearly dependant matrices in {timeit.default_timer()-start}s" ) # Find a lumping vectors_to_include = [] for linear_form in observable: vec = SparseVector.from_list(linear_form.linear_part_as_vec(), field) vectors_to_include.append(vec) start = timeit.default_timer() lumping_subspace = find_smallest_common_subspace(matrices, vectors_to_include) print( f"-> I found the lumping subspace in {timeit.default_timer()-start}s") lumped_rhs = lumping_subspace.perform_change_of_variables( rhs, new_vars_name) new_ic = None if ic is not None: eval_point = [ic.get(v, 0) for v in rhs[0].gens] new_ic = [] for vect in lumping_subspace.basis(): new_ic.append( sum([p[0] * p[1] for p in zip(eval_point, vect.to_list())])) # Nice printing vars_new = lumped_rhs[0].gens if print_system: print("Original system:") for i in range(len(rhs)): print(f"{vars_old[i]}' = {rhs[i]}") print("Outputs to fix:") print(", ".join(map(str, observable))) if print_reduction: print("New variables:") for i in range(lumping_subspace.dim()): new_var = SparsePolynomial(vars_old, field) for j in range(len(vars_old)): if lumping_subspace.basis()[i][j] != 0: new_var += SparsePolynomial( vars_old, field, {((j, 1), ): lumping_subspace.basis()[i][j]}) print(f"{vars_new[i]} = {new_var}") if new_ic is not None: print("New initial conditions:") for v, val in zip(vars_new, new_ic): print(f"{v}(0) = {float(val)}") print("Lumped system:") for i in range(lumping_subspace.dim()): print(f"{vars_new[i]}' = {lumped_rhs[i]}") return { "rhs": lumped_rhs, "subspace": [v.to_list() for v in lumping_subspace.basis()], "new_ic": new_ic }
def perform_change_of_variables(self, rhs, new_vars_name='y'): """ Restrict a system of ODEs with the rhs given by rhs (SparsePolynomial or RationalFunction) to the subspace new_vars_name (optional) - the name for variables in the lumped polynomials """ old_vars = rhs[0].gens domain = rhs[0].domain new_vars = [new_vars_name + str(i) for i in range(self.dim())] pivots = set(self.parametrizing_coordinates()) lpivots = sorted(pivots) basis = self.basis() logging.debug("Constructing new rhs") if isinstance(rhs[0], SparsePolynomial): new_rhs = [ SparsePolynomial(old_vars, domain) for _ in range(self.dim()) ] elif isinstance(rhs[0], RationalFunction): new_rhs = [ RationalFunction( SparsePolynomial(old_vars, domain), SparsePolynomial.from_string("1", old_vars, domain)) for _ in range(self.dim()) ] for i, vec in enumerate(basis): logging.debug(f" Equation number {i}") for j in vec.nonzero_iter(): # ordering is important due to the implementation of # multiplication for SparsePolynomial new_rhs[i] += rhs[j] * vec._data[j] logging.debug("Plugging zero to nonpivot coordinates") if isinstance(rhs[0], SparsePolynomial): shrinked_polys = [] for p in new_rhs: filtered_dict = dict() for monom, coef in p.dataiter(): new_monom = [] skip = False for var, exp in monom: if var not in pivots: skip = True break else: new_monom.append((lpivots.index(var), exp)) if not skip: new_monom = tuple(new_monom) filtered_dict[new_monom] = coef shrinked_polys.append( SparsePolynomial(new_vars, domain, filtered_dict)) return shrinked_polys elif isinstance(rhs[0], RationalFunction): # plugging all nonpivot variables with zeros shrinked_rfs = [] for rf in new_rhs: num_filtered_dict = dict() for monom, coef in rf.num.dataiter(): new_monom = [] skip = False for var, exp in monom: if var not in pivots: skip = True break else: new_monom.append((lpivots.index(var), exp)) if not skip: new_monom = tuple(new_monom) num_filtered_dict[new_monom] = coef new_num = SparsePolynomial(new_vars, domain, num_filtered_dict) denom_filtered_dict = dict() for monom, coef in rf.denom.dataiter(): new_monom = [] skip = False for var, exp in monom: if var not in pivots: skip = True break else: new_monom.append((lpivots.index(var), exp)) if not skip: new_monom = tuple(new_monom) denom_filtered_dict[new_monom] = coef new_denom = SparsePolynomial(new_vars, domain, denom_filtered_dict) if new_denom.is_zero() and False: print() print("Before plugging all nonpivot variables with zeros:") print('\t', rf) print() print("After plugging all nonpivot variables with zeros:") print('\t', f"({new_num})/({new_denom})") print() raise ZeroDivisionError shrinked_rfs.append(RationalFunction(new_num, new_denom)) return shrinked_rfs
from sympy import QQ sys.path.insert(0, "../") sys.path.insert(0, "./../../") import parser import clue from sparse_polynomial import SparsePolynomial system = parser.read_system("BIOMD0000000504.ode") obs_sets = [["cFos_P", "cJun_P"], ["MMP1_mRNA", "MMP13_mRNA", "TIMP1_mRNA"], ["MMP1", "MMP13", "ColFrag"], [ "JAK1_P", "JNK_P", "cJun_P", "cJun_dimer", "STAT3_P_nuc", "STAT3_P_cyt" ]] for obs_set in obs_sets: print("===============================================") obs_polys = [ SparsePolynomial.from_string(s, system['variables']) for s in obs_set ] start = time.time() lumped = clue.do_lumping(system['equations'], obs_polys) end = time.time() print(f"The size of the original model is {len(system['equations'])}") print(f"The size of the reduced model is {len(lumped['polynomials'])}") print(f"Computation took {end - start} seconds")
# A stochastic model of Escherichia coli AI‐2 quorum signal circuit reveals alternative synthesis pathways. # Molecular systems biology, 2(1), 2006 # Source: https://www.ebi.ac.uk/biomodels/MODEL8262229752 ## import sys import time from sympy import QQ sys.path.insert(0, "../") sys.path.insert(0, "./../../") import parser import clue from sparse_polynomial import SparsePolynomial system = parser.read_system("MODEL8262229752.ode") obs = [ SparsePolynomial.from_string("Pfs_mRNA", system['variables']), SparsePolynomial.from_string("LuxS_mRNA", system['variables']), SparsePolynomial.from_string("AI2_intra", system['variables']) ] start = time.time() lumped = clue.do_lumping(system['equations'], obs) end = time.time() print(f"The size of the original model is {len(system['equations'])}") print(f"The size of the reduced model is {len(lumped['polynomials'])}") print(f"Computation took {end - start} seconds")
# lumping = do_lumping( # system["equations"], # [SparsePolynomial.from_string("C3GActive", system["variables"])], # print_reduction=False, # discard_useless_matrices=True, # ) # time += timeit.default_timer() - start # print("Average Time: ", time/N) # check_lumping("BIOMD0000000033", system["equations"], lumping) # MODEL1502270000 system = read_system("../examples/RationalFunctions/MODEL1502270000.ode") lumping = do_lumping( system["equations"], [SparsePolynomial.from_string("gmax*Kp+a", system["variables"])], print_reduction=False, discard_useless_matrices=True, ) print("Lumping Size: ", len(lumping['rhs'])) check_lumping("MODEL1502270000", system["equations"], lumping) lumping = do_lumping( system["equations"], [SparsePolynomial.from_string("si", system["variables"])], print_reduction=False, discard_useless_matrices=False, ) print("Lumping Size: ", len(lumping['rhs'])) check_lumping("MODEL1502270000", system["equations"], lumping) # print(lumping)
import sys import sympy from sympy import QQ sys.path.insert(0, "../") sys.path.insert(0, "./") import clue from sparse_polynomial import SparsePolynomial exprs = [ "a * (3 * a + b) - 8.5 * (a + b)**5 - 3 * c * b * (c - a)", "(a + b + c**2)**5 - 3 * a + b * 17 * 19 * 0.5" ] for e in exprs: parsed = sympy.parse_expr(e) sp = SparsePolynomial.from_string(e, ["a", "b", "c"]) assert (sympy.simplify(parsed - sympy.parse_expr(str(sp))) == 0)