123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879 |
- """Representations and Inference for Logic (Chapters 7-9, 12)
- Covers both Propositional and First-Order Logic. First we have four
- important data types:
- KB Abstract class holds a knowledge base of logical expressions
- Expr A logical expression, imported from utils.py
- substitution Implemented as a dictionary of var:value pairs, {x:1, y:x}
- Be careful: some functions take an Expr as argument, and some take a KB.
- Logical expressions can be created with Expr or expr, imported from utils, TODO
- or with expr, which adds the capability to write a string that uses
- the connectives ==>, <==, <=>, or <=/=>. But be careful: these have the
- opertor precedence of commas; you may need to add parens to make precendence work.
- See logic.ipynb for examples.
- Then we implement various functions for doing logical inference:
- pl_true Evaluate a propositional logical sentence in a model
- tt_entails Say if a statement is entailed by a KB
- pl_resolution Do resolution on propositional sentences
- dpll_satisfiable See if a propositional sentence is satisfiable
- WalkSAT Try to find a solution for a set of clauses
- And a few other functions:
- to_cnf Convert to conjunctive normal form
- unify Do unification of two FOL sentences
- diff, simp Symbolic differentiation and simplification
- """
- from .utils import (
- removeall, unique, first, isnumber, issequence, Expr, expr, subexpressions
- )
- import itertools
- from collections import defaultdict
- # ______________________________________________________________________________
- class KB:
- """A knowledge base to which you can tell and ask sentences.
- To create a KB, first subclass this class and implement
- tell, ask_generator, and retract. Why ask_generator instead of ask?
- The book is a bit vague on what ask means --
- For a Propositional Logic KB, ask(P & Q) returns True or False, but for an
- FOL KB, something like ask(Brother(x, y)) might return many substitutions
- such as {x: Cain, y: Abel}, {x: Abel, y: Cain}, {x: George, y: Jeb}, etc.
- So ask_generator generates these one at a time, and ask either returns the
- first one or returns False."""
- def __init__(self, sentence=None):
- raise NotImplementedError
- def tell(self, sentence):
- "Add the sentence to the KB."
- raise NotImplementedError
- def ask(self, query):
- """Return a substitution that makes the query true, or, failing that, return False."""
- return first(self.ask_generator(query), default=False)
- def ask_generator(self, query):
- "Yield all the substitutions that make query true."
- raise NotImplementedError
- def retract(self, sentence):
- "Remove sentence from the KB."
- raise NotImplementedError
- class PropKB(KB):
- """A KB for propositional logic. Inefficient, with no indexing. """
- def __init__(self, sentence=None):
- self.clauses = []
- if sentence:
- self.tell(sentence)
- def tell(self, sentence):
- "Add the sentence's clauses to the KB."
- self.clauses.extend(conjuncts(to_cnf(sentence)))
- def ask_generator(self, query):
- "Yield the empty substitution {} if KB entails query; else no results."
- if tt_entails(Expr('&', *self.clauses), query):
- yield {}
- def ask_if_true(self, query):
- "Return True if the KB entails query, else return False."
- for _ in self.ask_generator(query):
- return True
- return False
- def retract(self, sentence):
- "Remove the sentence's clauses from the KB."
- for c in conjuncts(to_cnf(sentence)):
- if c in self.clauses:
- self.clauses.remove(c)
- # ______________________________________________________________________________
- def is_symbol(s):
- "A string s is a symbol if it starts with an alphabetic char."
- return isinstance(s, str) and s[:1].isalpha()
- def is_var_symbol(s):
- "A logic variable symbol is an initial-lowercase string."
- return is_symbol(s) and s[0].islower()
- def is_prop_symbol(s):
- """A proposition logic symbol is an initial-uppercase string."""
- return is_symbol(s) and s[0].isupper()
- def variables(s):
- """Return a set of the variables in expression s.
- >>> variables(expr('F(x, x) & G(x, y) & H(y, z) & R(A, z, 2)')) == {x, y, z}
- True
- """
- return {x for x in subexpressions(s) if is_variable(x)}
- def is_definite_clause(s):
- """returns True for exprs s of the form A & B & ... & C ==> D,
- where all literals are positive. In clause form, this is
- ~A | ~B | ... | ~C | D, where exactly one clause is positive.
- >>> is_definite_clause(expr('Farmer(Mac)'))
- True
- """
- if is_symbol(s.op):
- return True
- elif s.op == '==>':
- antecedent, consequent = s.args
- return (is_symbol(consequent.op) and
- all(is_symbol(arg.op) for arg in conjuncts(antecedent)))
- else:
- return False
- def parse_definite_clause(s):
- "Return the antecedents and the consequent of a definite clause."
- assert is_definite_clause(s)
- if is_symbol(s.op):
- return [], s
- else:
- antecedent, consequent = s.args
- return conjuncts(antecedent), consequent
- # Useful constant Exprs used in examples and code:
- A, B, C, D, E, F, G, P, Q, x, y, z = map(Expr, 'ABCDEFGPQxyz')
- # ______________________________________________________________________________
- def tt_entails(kb, alpha):
- """Does kb entail the sentence alpha? Use truth tables. For propositional
- kb's and sentences. [Figure 7.10]. Note that the 'kb' should be an
- Expr which is a conjunction of clauses.
- >>> tt_entails(expr('P & Q'), expr('Q'))
- True
- """
- assert not variables(alpha)
- return tt_check_all(kb, alpha, prop_symbols(kb & alpha), {})
- def tt_check_all(kb, alpha, symbols, model):
- "Auxiliary routine to implement tt_entails."
- if not symbols:
- if pl_true(kb, model):
- result = pl_true(alpha, model)
- assert result in (True, False)
- return result
- else:
- return True
- else:
- P, rest = symbols[0], symbols[1:]
- return (tt_check_all(kb, alpha, rest, extend(model, P, True)) and
- tt_check_all(kb, alpha, rest, extend(model, P, False)))
- def prop_symbols(x):
- "Return a list of all propositional symbols in x."
- if not isinstance(x, Expr):
- return []
- elif is_prop_symbol(x.op):
- return [x]
- else:
- return list(set(symbol for arg in x.args for symbol in prop_symbols(arg)))
- def tt_true(s):
- """Is a propositional sentence a tautology?
- >>> tt_true('P | ~P')
- True
- """
- s = expr(s)
- return tt_entails(True, s)
- def pl_true(exp, model={}):
- """Return True if the propositional logic expression is true in the model,
- and False if it is false. If the model does not specify the value for
- every proposition, this may return None to indicate 'not obvious';
- this may happen even when the expression is tautological."""
- if exp in (True, False):
- return exp
- op, args = exp.op, exp.args
- if is_prop_symbol(op):
- return model.get(exp)
- elif op == '~':
- p = pl_true(args[0], model)
- if p is None:
- return None
- else:
- return not p
- elif op == '|':
- result = False
- for arg in args:
- p = pl_true(arg, model)
- if p is True:
- return True
- if p is None:
- result = None
- return result
- elif op == '&':
- result = True
- for arg in args:
- p = pl_true(arg, model)
- if p is False:
- return False
- if p is None:
- result = None
- return result
- p, q = args
- if op == '==>':
- return pl_true(~p | q, model)
- elif op == '<==':
- return pl_true(p | ~q, model)
- pt = pl_true(p, model)
- if pt is None:
- return None
- qt = pl_true(q, model)
- if qt is None:
- return None
- if op == '<=>':
- return pt == qt
- elif op == '^': # xor or 'not equivalent'
- return pt != qt
- else:
- raise ValueError("illegal operator in logic expression" + str(exp))
- # ______________________________________________________________________________
- # Convert to Conjunctive Normal Form (CNF)
- def to_cnf(s):
- """Convert a propositional logical sentence to conjunctive normal form.
- That is, to the form ((A | ~B | ...) & (B | C | ...) & ...) [p. 253]
- >>> to_cnf('~(B | C)')
- (~B & ~C)
- """
- s = expr(s)
- if isinstance(s, str):
- s = expr(s)
- s = eliminate_implications(s) # Steps 1, 2 from p. 253
- s = move_not_inwards(s) # Step 3
- return distribute_and_over_or(s) # Step 4
- def eliminate_implications(s):
- "Change implications into equivalent form with only &, |, and ~ as logical operators."
- if s is False:
- s = expr("F")
- if s is True:
- s = expr("T")
- s = expr(s)
- if not s.args or is_symbol(s.op):
- return s # Atoms are unchanged.
- args = list(map(eliminate_implications, s.args))
- a, b = args[0], args[-1]
- if s.op == '==>':
- return b | ~a
- elif s.op == '<==':
- return a | ~b
- elif s.op == '<=>':
- return (a | ~b) & (b | ~a)
- elif s.op == '^':
- assert len(args) == 2 # TODO: relax this restriction
- return (a & ~b) | (~a & b)
- else:
- assert s.op in ('&', '|', '~')
- return Expr(s.op, *args)
- def move_not_inwards(s):
- """Rewrite sentence s by moving negation sign inward.
- >>> move_not_inwards(~(A | B))
- (~A & ~B)"""
- s = expr(s)
- if s.op == '~':
- def NOT(b):
- return move_not_inwards(~b)
- a = s.args[0]
- if a.op == '~':
- return move_not_inwards(a.args[0]) # ~~A ==> A
- if a.op == '&':
- return associate('|', list(map(NOT, a.args)))
- if a.op == '|':
- return associate('&', list(map(NOT, a.args)))
- return s
- elif is_symbol(s.op) or not s.args:
- return s
- else:
- return Expr(s.op, *list(map(move_not_inwards, s.args)))
- def distribute_and_over_or(s):
- """Given a sentence s consisting of conjunctions and disjunctions
- of literals, return an equivalent sentence in CNF.
- >>> distribute_and_over_or((A & B) | C)
- ((A | C) & (B | C))
- """
- s = expr(s)
- if s.op == '|':
- s = associate('|', s.args)
- if s.op != '|':
- return distribute_and_over_or(s)
- if len(s.args) == 0:
- return False
- if len(s.args) == 1:
- return distribute_and_over_or(s.args[0])
- conj = first(arg for arg in s.args if arg.op == '&')
- if not conj:
- return s
- others = [a for a in s.args if a is not conj]
- rest = associate('|', others)
- return associate('&', [distribute_and_over_or(c | rest)
- for c in conj.args])
- elif s.op == '&':
- return associate('&', list(map(distribute_and_over_or, s.args)))
- else:
- return s
- def associate(op, args):
- """Given an associative op, return an expression with the same
- meaning as Expr(op, *args), but flattened -- that is, with nested
- instances of the same op promoted to the top level.
- >>> associate('&', [(A&B),(B|C),(B&C)])
- (A & B & (B | C) & B & C)
- >>> associate('|', [A|(B|(C|(A&B)))])
- (A | B | C | (A & B))
- """
- args = dissociate(op, args)
- if len(args) == 0:
- return _op_identity[op]
- elif len(args) == 1:
- return args[0]
- else:
- return Expr(op, *args)
- _op_identity = {'&': True, '|': False, '+': 0, '*': 1}
- def dissociate(op, args):
- """Given an associative op, return a flattened list result such
- that Expr(op, *result) means the same as Expr(op, *args)."""
- result = []
- def collect(subargs):
- for arg in subargs:
- if arg.op == op:
- collect(arg.args)
- else:
- result.append(arg)
- collect(args)
- return result
- def conjuncts(s):
- """Return a list of the conjuncts in the sentence s.
- >>> conjuncts(A & B)
- [A, B]
- >>> conjuncts(A | B)
- [(A | B)]
- """
- return dissociate('&', [s])
- def disjuncts(s):
- """Return a list of the disjuncts in the sentence s.
- >>> disjuncts(A | B)
- [A, B]
- >>> disjuncts(A & B)
- [(A & B)]
- """
- return dissociate('|', [s])
- # ______________________________________________________________________________
- def pl_resolution(KB, alpha):
- "Propositional-logic resolution: say if alpha follows from KB. [Figure 7.12]"
- clauses = KB.clauses + conjuncts(to_cnf(~alpha))
- new = set()
- while True:
- n = len(clauses)
- pairs = [(clauses[i], clauses[j])
- for i in range(n) for j in range(i+1, n)]
- for (ci, cj) in pairs:
- resolvents = pl_resolve(ci, cj)
- if False in resolvents:
- return True
- new = new.union(set(resolvents))
- if new.issubset(set(clauses)):
- return False
- for c in new:
- if c not in clauses:
- clauses.append(c)
- def pl_resolve(ci, cj):
- """Return all clauses that can be obtained by resolving clauses ci and cj."""
- clauses = []
- for di in disjuncts(ci):
- for dj in disjuncts(cj):
- if di == ~dj or ~di == dj:
- dnew = unique(removeall(di, disjuncts(ci)) +
- removeall(dj, disjuncts(cj)))
- clauses.append(associate('|', dnew))
- return clauses
- # ______________________________________________________________________________
- class PropDefiniteKB(PropKB):
- "A KB of propositional definite clauses."
- def tell(self, sentence):
- "Add a definite clause to this KB."
- assert is_definite_clause(sentence), "Must be definite clause"
- self.clauses.append(sentence)
- def ask_generator(self, query):
- "Yield the empty substitution if KB implies query; else nothing."
- if pl_fc_entails(self.clauses, query):
- yield {}
- def retract(self, sentence):
- self.clauses.remove(sentence)
- def clauses_with_premise(self, p):
- """Return a list of the clauses in KB that have p in their premise.
- This could be cached away for O(1) speed, but we'll recompute it."""
- return [c for c in self.clauses
- if c.op == '==>' and p in conjuncts(c.args[0])]
- def pl_fc_entails(KB, q):
- """Use forward chaining to see if a PropDefiniteKB entails symbol q.
- [Figure 7.15]
- >>> pl_fc_entails(horn_clauses_KB, expr('Q'))
- True
- """
- count = {c: len(conjuncts(c.args[0]))
- for c in KB.clauses
- if c.op == '==>'}
- inferred = defaultdict(bool)
- agenda = [s for s in KB.clauses if is_prop_symbol(s.op)]
- while agenda:
- p = agenda.pop()
- if p == q:
- return True
- if not inferred[p]:
- inferred[p] = True
- for c in KB.clauses_with_premise(p):
- count[c] -= 1
- if count[c] == 0:
- agenda.append(c.args[1])
- return False
- """ [Figure 7.13]
- Simple inference in a wumpus world example
- """
- wumpus_world_inference = expr("(B11 <=> (P12 | P21)) & ~B11")
- """ [Figure 7.16]
- Propositional Logic Forward Chaining example
- """
- horn_clauses_KB = PropDefiniteKB()
- for s in "P==>Q; (L&M)==>P; (B&L)==>M; (A&P)==>L; (A&B)==>L; A;B".split(';'):
- horn_clauses_KB.tell(expr(s))
- # ______________________________________________________________________________
- # DPLL-Satisfiable [Figure 7.17]
- def dpll_satisfiable(s):
- """Check satisfiability of a propositional sentence.
- This differs from the book code in two ways: (1) it returns a model
- rather than True when it succeeds; this is more useful. (2) The
- function find_pure_symbol is passed a list of unknown clauses, rather
- than a list of all clauses and the model; this is more efficient."""
- clauses = conjuncts(to_cnf(s))
- symbols = prop_symbols(s)
- return dpll(clauses, symbols, {})
- def dpll(clauses, symbols, model):
- "See if the clauses are true in a partial model."
- unknown_clauses = [] # clauses with an unknown truth value
- for c in clauses:
- val = pl_true(c, model)
- if val is False:
- return False
- if val is not True:
- unknown_clauses.append(c)
- if not unknown_clauses:
- return model
- P, value = find_pure_symbol(symbols, unknown_clauses)
- if P:
- return dpll(clauses, removeall(P, symbols), extend(model, P, value))
- P, value = find_unit_clause(clauses, model)
- if P:
- return dpll(clauses, removeall(P, symbols), extend(model, P, value))
- if not symbols:
- raise TypeError("Argument should be of the type Expr.")
- P, symbols = symbols[0], symbols[1:]
- return (dpll(clauses, symbols, extend(model, P, True)) or
- dpll(clauses, symbols, extend(model, P, False)))
- def find_pure_symbol(symbols, clauses):
- """Find a symbol and its value if it appears only as a positive literal
- (or only as a negative) in clauses.
- >>> find_pure_symbol([A, B, C], [A|~B,~B|~C,C|A])
- (A, True)
- """
- for s in symbols:
- found_pos, found_neg = False, False
- for c in clauses:
- if not found_pos and s in disjuncts(c):
- found_pos = True
- if not found_neg and ~s in disjuncts(c):
- found_neg = True
- if found_pos != found_neg:
- return s, found_pos
- return None, None
- def find_unit_clause(clauses, model):
- """Find a forced assignment if possible from a clause with only 1
- variable not bound in the model.
- >>> find_unit_clause([A|B|C, B|~C, ~A|~B], {A:True})
- (B, False)
- """
- for clause in clauses:
- P, value = unit_clause_assign(clause, model)
- if P:
- return P, value
- return None, None
- def unit_clause_assign(clause, model):
- """Return a single variable/value pair that makes clause true in
- the model, if possible.
- >>> unit_clause_assign(A|B|C, {A:True})
- (None, None)
- >>> unit_clause_assign(B|~C, {A:True})
- (None, None)
- >>> unit_clause_assign(~A|~B, {A:True})
- (B, False)
- """
- P, value = None, None
- for literal in disjuncts(clause):
- sym, positive = inspect_literal(literal)
- if sym in model:
- if model[sym] == positive:
- return None, None # clause already True
- elif P:
- return None, None # more than 1 unbound variable
- else:
- P, value = sym, positive
- return P, value
- def inspect_literal(literal):
- """The symbol in this literal, and the value it should take to
- make the literal true.
- >>> inspect_literal(P)
- (P, True)
- >>> inspect_literal(~P)
- (P, False)
- """
- if literal.op == '~':
- return literal.args[0], False
- else:
- return literal, True
- def unify(x, y, s):
- """Unify expressions x,y with substitution s; return a substitution that
- would make x,y equal, or None if x,y can not unify. x and y can be
- variables (e.g. Expr('x')), constants, lists, or Exprs. [Figure 9.1]"""
- if s is None:
- return None
- elif x == y:
- return s
- elif is_variable(x):
- return unify_var(x, y, s)
- elif is_variable(y):
- return unify_var(y, x, s)
- elif isinstance(x, Expr) and isinstance(y, Expr):
- return unify(x.args, y.args, unify(x.op, y.op, s))
- elif isinstance(x, str) or isinstance(y, str):
- return None
- elif issequence(x) and issequence(y) and len(x) == len(y):
- if not x:
- return s
- return unify(x[1:], y[1:], unify(x[0], y[0], s))
- else:
- return None
- def is_variable(x):
- "A variable is an Expr with no args and a lowercase symbol as the op."
- return isinstance(x, Expr) and not x.args and x.op[0].islower()
- def unify_var(var, x, s):
- if var in s:
- return unify(s[var], x, s)
- elif occur_check(var, x, s):
- return None
- else:
- return extend(s, var, x)
- def occur_check(var, x, s):
- """Return true if variable var occurs anywhere in x
- (or in subst(s, x), if s has a binding for x)."""
- if var == x:
- return True
- elif is_variable(x) and x in s:
- return occur_check(var, s[x], s)
- elif isinstance(x, Expr):
- return (occur_check(var, x.op, s) or
- occur_check(var, x.args, s))
- elif isinstance(x, (list, tuple)):
- return first(e for e in x if occur_check(var, e, s))
- else:
- return False
- def extend(s, var, val):
- "Copy the substitution s and extend it by setting var to val; return copy."
- s2 = s.copy()
- s2[var] = val
- return s2
- def subst(s, x):
- """Substitute the substitution s into the expression x.
- >>> subst({x: 42, y:0}, F(x) + y)
- (F(42) + 0)
- """
- if isinstance(x, list):
- return [subst(s, xi) for xi in x]
- elif isinstance(x, tuple):
- return tuple([subst(s, xi) for xi in x])
- elif not isinstance(x, Expr):
- return x
- elif is_var_symbol(x.op):
- return s.get(x, x)
- else:
- return Expr(x.op, *[subst(s, arg) for arg in x.args])
- def fol_fc_ask(KB, alpha):
- raise NotImplementedError
- def standardize_variables(sentence, dic=None):
- """Replace all the variables in sentence with new variables."""
- if dic is None:
- dic = {}
- if not isinstance(sentence, Expr):
- return sentence
- elif is_var_symbol(sentence.op):
- if sentence in dic:
- return dic[sentence]
- else:
- v = Expr('v_{}'.format(next(standardize_variables.counter)))
- dic[sentence] = v
- return v
- else:
- return Expr(sentence.op,
- *[standardize_variables(a, dic) for a in sentence.args])
- standardize_variables.counter = itertools.count()
- # ______________________________________________________________________________
- class FolKB(KB):
- """A knowledge base consisting of first-order definite clauses.
- >>> kb0 = FolKB([expr('Farmer(Mac)'), expr('Rabbit(Pete)'),
- ... expr('(Rabbit(r) & Farmer(f)) ==> Hates(f, r)')])
- >>> kb0.tell(expr('Rabbit(Flopsie)'))
- >>> kb0.retract(expr('Rabbit(Pete)'))
- >>> kb0.ask(expr('Hates(Mac, x)'))[x]
- Flopsie
- >>> kb0.ask(expr('Wife(Pete, x)'))
- False
- """
- def __init__(self, initial_clauses=[]):
- self.clauses = [] # inefficient: no indexing
- for clause in initial_clauses:
- self.tell(clause)
- def tell(self, sentence):
- if is_definite_clause(sentence):
- self.clauses.append(sentence)
- else:
- raise Exception("Not a definite clause: {}".format(sentence))
- def ask_generator(self, query):
- return fol_bc_ask(self, query)
- def retract(self, sentence):
- self.clauses.remove(sentence)
- def fetch_rules_for_goal(self, goal):
- return self.clauses
- def fol_bc_ask(KB, query):
- """A simple backward-chaining algorithm for first-order logic. [Figure 9.6]
- KB should be an instance of FolKB, and query an atomic sentence. """
- return fol_bc_or(KB, query, {})
- def fol_bc_or(KB, goal, theta):
- for rule in KB.fetch_rules_for_goal(goal):
- lhs, rhs = parse_definite_clause(standardize_variables(rule))
- for theta1 in fol_bc_and(KB, lhs, unify(rhs, goal, theta)):
- yield theta1
- def fol_bc_and(KB, goals, theta):
- if theta is None:
- pass
- elif not goals:
- yield theta
- else:
- first, rest = goals[0], goals[1:]
- for theta1 in fol_bc_or(KB, subst(theta, first), theta):
- for theta2 in fol_bc_and(KB, rest, theta1):
- yield theta2
- # ______________________________________________________________________________
- # Example application (not in the book).
- # You can use the Expr class to do symbolic differentiation. This used to be
- # a part of AI; now it is considered a separate field, Symbolic Algebra.
- def diff(y, x):
- """Return the symbolic derivative, dy/dx, as an Expr.
- However, you probably want to simplify the results with simp.
- >>> diff(x * x, x)
- ((x * 1) + (x * 1))
- """
- if y == x:
- return 1
- elif not y.args:
- return 0
- else:
- u, op, v = y.args[0], y.op, y.args[-1]
- if op == '+':
- return diff(u, x) + diff(v, x)
- elif op == '-' and len(y.args) == 1:
- return -diff(u, x)
- elif op == '-':
- return diff(u, x) - diff(v, x)
- elif op == '*':
- return u * diff(v, x) + v * diff(u, x)
- elif op == '/':
- return (v * diff(u, x) - u * diff(v, x)) / (v * v)
- elif op == '**' and isnumber(x.op):
- return (v * u ** (v - 1) * diff(u, x))
- elif op == '**':
- return (v * u ** (v - 1) * diff(u, x) +
- u ** v * Expr('log')(u) * diff(v, x))
- elif op == 'log':
- return diff(u, x) / u
- else:
- raise ValueError("Unknown op: {} in diff({}, {})".format(op, y, x))
- def simp(x):
- "Simplify the expression x."
- if isnumber(x) or not x.args:
- return x
- args = list(map(simp, x.args))
- u, op, v = args[0], x.op, args[-1]
- if op == '+':
- if v == 0:
- return u
- if u == 0:
- return v
- if u == v:
- return 2 * u
- if u == -v or v == -u:
- return 0
- elif op == '-' and len(args) == 1:
- if u.op == '-' and len(u.args) == 1:
- return u.args[0] # --y ==> y
- elif op == '-':
- if v == 0:
- return u
- if u == 0:
- return -v
- if u == v:
- return 0
- if u == -v or v == -u:
- return 0
- elif op == '*':
- if u == 0 or v == 0:
- return 0
- if u == 1:
- return v
- if v == 1:
- return u
- if u == v:
- return u ** 2
- elif op == '/':
- if u == 0:
- return 0
- if v == 0:
- return Expr('Undefined')
- if u == v:
- return 1
- if u == -v or v == -u:
- return 0
- elif op == '**':
- if u == 0:
- return 0
- if v == 0:
- return 1
- if u == 1:
- return 1
- if v == 1:
- return u
- elif op == 'log':
- if u == 1:
- return 0
- else:
- raise ValueError("Unknown op: " + op)
- # If we fall through to here, we can not simplify further
- return Expr(op, *args)
- def d(y, x):
- "Differentiate and then simplify."
- return simp(diff(y, x))
|