diff --git a/README.md b/README.md index bd4738c..379b651 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,10 @@ Now that the kernel understands the product rule, when it later encounters a pat The term rewriting system and pattern matching engine is fairly advanced. The computer algebra system at this stage is extremely limited, but simple calculus and algebraic manipulation is certainly supported (see examples below). If you are looking for a more mature computer algebra system, please consider using Mathematica (proprietary) or Mathics (open source, Sympy-backed). +![Jupyter screenshot](/images/jupyter_screenshot.png) + +(This screenshot demonstrates the Jupyter notebook interface for Expreduce. This Jupyter extension can be found [here](https://github.com/mmatera/iwolfram).) + # Install and run [DOWNLOAD HERE](https://github.com/corywalker/expreduce/releases/latest) diff --git a/expreduce.go b/expreduce.go index d42ae33..b7a0606 100644 --- a/expreduce.go +++ b/expreduce.go @@ -24,7 +24,7 @@ var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") var netprofile = flag.Bool("netprofile", false, "Enable live profiling at http://localhost:8080/debug/pprof/") var scriptfile = flag.String("script", "", "script `file` to read from") var initfile = flag.String("initfile", "", "A script to run on initialization.") -var loadRubi = flag.Bool("loadrubi", true, "Load the Rubi definitions for integral support on startup.") +var preloadRubi = flag.Bool("preloadrubi", false, "Preload the Rubi definitions for integral support on startup.") func main() { flag.Parse() @@ -44,8 +44,8 @@ func main() { fmt.Printf("Welcome to Expreduce!\n\n") es := expreduce.NewEvalState() - if *loadRubi { - fmt.Println("Loading Rubi snapshot for integral support. Disable with -loadrubi=false.") + if *preloadRubi { + fmt.Println("Pre-loading Rubi snapshot for integral support. Disable with -preloadrubi=false.") es.Eval(atoms.E(atoms.S("LoadRubiBundledSnapshot"))) fmt.Println("Done loading Rubi snapshot.") fmt.Print("\n") diff --git a/expreduce/atoms/ex_complex.go b/expreduce/atoms/ex_complex.go index 3b3ff3a..ef66507 100644 --- a/expreduce/atoms/ex_complex.go +++ b/expreduce/atoms/ex_complex.go @@ -14,28 +14,27 @@ type Complex struct { needsEval bool } -func (cmplx *Complex) StringForm(p expreduceapi.ToStringParams) string { - if p.Form == "FullForm" { - return fmt.Sprintf("Complex[%v, %v]", cmplx.Re, cmplx.Im) - } - iString := "I" - if p.Form == "TeXForm" { - iString = "i" - } +func (cmplx *Complex) AsExpr() expreduceapi.Ex { + iSym := S("I") reInt, reIsInt := cmplx.Re.(*Integer) imInt, imIsInt := cmplx.Im.(*Integer) if reIsInt && reInt.Val.Sign() == 0 { if imIsInt && imInt.Val.Int64() == 1 { - return iString - } - if imIsInt && imInt.Val.Int64() == -1 { - return fmt.Sprintf("(-%v)", iString) + return iSym } - p.PreviousHead = "System`Times" - return fmt.Sprintf("(%v*%v)", cmplx.Im.StringForm(p), iString) + return E(S("Times"), cmplx.Im, iSym) + } + if imIsInt && imInt.Val.Int64() == 1 { + return E(S("Plus"), cmplx.Re, iSym) + } + return E(S("Plus"), cmplx.Re, E(S("Times"), cmplx.Im, iSym)) +} + +func (cmplx *Complex) StringForm(p expreduceapi.ToStringParams) string { + if p.Form == "FullForm" { + return fmt.Sprintf("Complex[%v, %v]", cmplx.Re, cmplx.Im) } - p.PreviousHead = "System`Plus" - return fmt.Sprintf("(%v + %v*%v)", cmplx.Re.StringForm(p), cmplx.Im.StringForm(p), iString) + return cmplx.AsExpr().StringForm(p) } func (cmplx *Complex) IsEqual(other expreduceapi.Ex) string { diff --git a/expreduce/atoms/ex_integer.go b/expreduce/atoms/ex_integer.go index dcf0323..dbf0196 100644 --- a/expreduce/atoms/ex_integer.go +++ b/expreduce/atoms/ex_integer.go @@ -5,7 +5,6 @@ import ( "hash/fnv" "math/big" - "github.com/corywalker/expreduce/expreduce/parser/parens" "github.com/corywalker/expreduce/pkg/expreduceapi" ) @@ -20,14 +19,6 @@ type Integer struct { }*/ func (thisInt *Integer) StringForm(params expreduceapi.ToStringParams) string { - if thisInt.Val.Cmp(big.NewInt(0)) < 0 { - if parens.NeedsParens("System`Times", params.PreviousHead) { - if params.Form == "TeXForm" { - return fmt.Sprintf("{(%d)}", thisInt.Val) - } - return fmt.Sprintf("(%d)", thisInt.Val) - } - } return fmt.Sprintf("%d", thisInt.Val) } diff --git a/expreduce/atoms/ex_symbol.go b/expreduce/atoms/ex_symbol.go index fea50d4..36775b2 100644 --- a/expreduce/atoms/ex_symbol.go +++ b/expreduce/atoms/ex_symbol.go @@ -21,6 +21,9 @@ func formatSymName(name string, params expreduceapi.ToStringParams) string { if name == "E" { return "e" } + if name == "I" { + return "i" + } if name == "Pi" { return "\\pi" } diff --git a/expreduce/builtin_arithmetic.go b/expreduce/builtin_arithmetic.go index 3394398..493ee80 100644 --- a/expreduce/builtin_arithmetic.go +++ b/expreduce/builtin_arithmetic.go @@ -1,11 +1,14 @@ package expreduce import ( + "bytes" + "fmt" "math/big" "strings" "github.com/corywalker/expreduce/expreduce/atoms" "github.com/corywalker/expreduce/expreduce/iterspec" + "github.com/corywalker/expreduce/expreduce/parser/parens" "github.com/corywalker/expreduce/pkg/expreduceapi" ) @@ -113,12 +116,74 @@ func collectTerms(e expreduceapi.ExpressionInterface) expreduceapi.ExpressionInt return collected } +func splitFrac(ex expreduceapi.Ex) (num expreduceapi.Ex, den expreduceapi.Ex) { + asPow, isPow := atoms.HeadAssertion(ex, "System`Power") + if isPow { + if asPow.Len() != 2 { + return ex, nil + } + powInt, powIsInt := asPow.GetPart(2).(*atoms.Integer) + if !powIsInt { + return ex, nil + } + if powInt.Val.Int64() == -1 { + return nil, asPow.GetPart(1) + } + } + asRat, isRat := ex.(*atoms.Rational) + if isRat { + if asRat.Num.Int64() == 1 { + return nil, atoms.NewInteger(asRat.Den) + } + return atoms.NewInteger(asRat.Num), atoms.NewInteger(asRat.Den) + } + return ex, nil +} + func getArithmeticDefinitions() (defs []Definition) { defs = append(defs, Definition{ Name: "Plus", Default: "0", - toString: func(this expreduceapi.ExpressionInterface, params expreduceapi.ToStringParams) (bool, string) { - return toStringInfix(this.GetParts()[1:], " + ", "System`Plus", params) + toString: func(this expreduceapi.ExpressionInterface, p expreduceapi.ToStringParams) (bool, string) { + thisHead := "System`Plus" + parts := this.GetParts()[1:] + if p.Form != "InputForm" && p.Form != "OutputForm" && p.Form != "TeXForm" { + return false, "" + } + if len(parts) < 2 { + return false, "" + } + addParens := parens.NeedsParens(thisHead, p.PreviousHead) + var buffer bytes.Buffer + if addParens { + if p.Form == "TeXForm" { + buffer.WriteString("{\\left(") + } else { + buffer.WriteString("(") + } + } + nextParams := p + nextParams.PreviousHead = thisHead + for i := 0; i < len(parts); i++ { + toWrite := parts[i].StringForm(nextParams) + if i != 0 { + if toWrite[0] == '-' { + buffer.WriteString(" - ") + toWrite = toWrite[1:] + } else { + buffer.WriteString(" + ") + } + } + buffer.WriteString(toWrite) + } + if addParens { + if p.Form == "TeXForm" { + buffer.WriteString("\\right)}") + } else { + buffer.WriteString(")") + } + } + return true, buffer.String() }, legacyEvalFn: func(this expreduceapi.ExpressionInterface, es expreduceapi.EvalStateInterface) expreduceapi.Ex { // Calls without argument receive identity values @@ -174,11 +239,64 @@ func getArithmeticDefinitions() (defs []Definition) { if params.Form == "TeXForm" { delim = " " } - ok, res := toStringInfix(this.GetParts()[1:], delim, "System`Times", params) - if ok && strings.HasPrefix(res, "(-1)"+delim) { - return ok, "-" + res[5:] + + timesParts := atoms.E(atoms.S("Times")) + for _, part := range this.GetParts()[1:] { + partAsComplex, partIsComplex := part.(*atoms.Complex) + if partIsComplex { + asExpr := partAsComplex.AsExpr() + cmplxTimes, cmplxIsTimes := atoms.HeadAssertion(asExpr, "System`Times") + if cmplxIsTimes { + for _, cmplxPart := range cmplxTimes.GetParts()[1:] { + timesParts.AppendEx(cmplxPart) + } + continue + } + } + timesParts.AppendEx(part) + } + + num := atoms.E(atoms.S("Times")) + den := atoms.E(atoms.S("Times")) + for _, part := range timesParts.GetParts()[1:] { + numPart, denPart := splitFrac(part) + if numPart != nil { + num.AppendEx(numPart) + } + if denPart != nil { + den.AppendEx(denPart) + } + } + if den.Len() > 0 { + numOk, numStr := toStringInfix(num.GetParts()[1:], delim, "System`Times", params) + if num.Len() == 1 { + numOk, numStr = true, num.GetPart(1).StringForm(params) + } + denOk, denStr := toStringInfix(den.GetParts()[1:], delim, "System`Times", params) + if den.Len() == 1 { + denOk, denStr = true, den.GetPart(1).StringForm(params) + } + if !numOk || !denOk { + return false, "" + } + prefix := "" + if strings.HasPrefix(numStr, "-1"+delim) { + prefix = "-" + numStr = numStr[3:] + } + if params.Form == "TeXForm" { + return true, fmt.Sprintf("%v\\frac{%v}{%v}", prefix, numStr, denStr) + } + return true, fmt.Sprintf("%v(%v)/(%v)", prefix, numStr, denStr) + } + ok, res := toStringInfix(num.GetParts()[1:], delim, "System`Times", params) + if !ok { + return false, "" + } + if strings.HasPrefix(res, "-1"+delim) { + return true, "-" + res[3:] } - return ok, res + return true, res }, legacyEvalFn: func(this expreduceapi.ExpressionInterface, es expreduceapi.EvalStateInterface) expreduceapi.Ex { // Calls without argument receive identity values diff --git a/expreduce/builtin_calculus.go b/expreduce/builtin_calculus.go index 3cb2263..51c260f 100644 --- a/expreduce/builtin_calculus.go +++ b/expreduce/builtin_calculus.go @@ -2,6 +2,7 @@ package expreduce func getCalculusDefinitions() (defs []Definition) { defs = append(defs, Definition{Name: "D"}) + defs = append(defs, Definition{Name: "Grad"}) defs = append(defs, Definition{Name: "Integrate"}) return } diff --git a/expreduce/builtin_list.go b/expreduce/builtin_list.go index 90a0b5c..ad4901f 100644 --- a/expreduce/builtin_list.go +++ b/expreduce/builtin_list.go @@ -43,6 +43,28 @@ func toStringList(this expreduceapi.ExpressionInterface, params expreduceapi.ToS return true, buffer.String() } +func toStringPart(this expreduceapi.ExpressionInterface, params expreduceapi.ToStringParams) (bool, string) { + if params.Form == "FullForm" { + return false, "" + } + subParams := params + subParams.PreviousHead = "" + var buffer bytes.Buffer + for i, e := range this.GetParts()[1:] { + if i == 0 { + buffer.WriteString(e.StringForm(params)) + buffer.WriteString("[[") + } else { + buffer.WriteString(e.StringForm(subParams)) + if i != len(this.GetParts()[1:])-1 { + buffer.WriteString(",") + } + } + } + buffer.WriteString("]]") + return true, buffer.String() +} + func memberQ(components []expreduceapi.Ex, item expreduceapi.Ex, es expreduceapi.EvalStateInterface) bool { for _, part := range components { if matchq, _ := matcher.IsMatchQ(part, item, matcher.EmptyPD(), es); matchq { @@ -496,7 +518,8 @@ func getListDefinitions() (defs []Definition) { }, }) defs = append(defs, Definition{ - Name: "Part", + Name: "Part", + toString: toStringPart, legacyEvalFn: func(this expreduceapi.ExpressionInterface, es expreduceapi.EvalStateInterface) expreduceapi.Ex { if len(this.GetParts()) == 1 { return this diff --git a/expreduce/builtin_trig.go b/expreduce/builtin_trig.go index 56ea3a5..57d2c09 100644 --- a/expreduce/builtin_trig.go +++ b/expreduce/builtin_trig.go @@ -63,5 +63,7 @@ func getTrigDefinitions() (defs []Definition) { Name: "TrigToExp", OmitDocumentation: true, }) + defs = append(defs, Definition{Name: "Degree"}) + defs = append(defs, Definition{Name: "RotationMatrix"}) return } diff --git a/expreduce/evalstate.go b/expreduce/evalstate.go index 74d8455..e64b7d9 100644 --- a/expreduce/evalstate.go +++ b/expreduce/evalstate.go @@ -183,6 +183,7 @@ func (es *EvalState) Init(loadAllDefs bool) { es.MarkSeen("System`EllipticF") es.MarkSeen("System`ProductLog") es.MarkSeen("System`FresnelS") + es.MarkSeen("System`Gamma") es.MarkSeen("System`Cosh") es.MarkSeen("System`Sinh") diff --git a/expreduce/parser/interp.go b/expreduce/parser/interp.go index c7cedae..377dc75 100644 --- a/expreduce/parser/interp.go +++ b/expreduce/parser/interp.go @@ -480,6 +480,7 @@ func ReplaceSyms(in string) string { in = strings.Replace(in, "\\[Omega]", "ω", -1) in = strings.Replace(in, "\\[CapitalOmega]", "Ω", -1) in = strings.Replace(in, "\\[Alpha]", "α", -1) + in = strings.Replace(in, "\\[Theta]", "θ", -1) return in } diff --git a/expreduce/resources.go b/expreduce/resources.go index b08cd39..f7b1dda 100644 --- a/expreduce/resources.go +++ b/expreduce/resources.go @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3916d384fed3f084e23c0984d17c4cc22547dc7a10b4ca6f70a3866fcf5beca7 -size 1339970 +oid sha256:42b3910e4654d0755c195643af1eafef9312800c9ddd5ad75b0bfe74905b8c9f +size 1342570 diff --git a/expreduce/resources/arithmetic.m b/expreduce/resources/arithmetic.m index f5f68ed..006ba88 100644 --- a/expreduce/resources/arithmetic.m +++ b/expreduce/resources/arithmetic.m @@ -21,10 +21,10 @@ EStringTest["(1.*(a + b))", "(a + b)/1."], EStringTest["(2*(a + b))", "2*(a + b)"], EStringTest["(a*(b + c))", "a*(b + c)"], - EStringTest["(-a + -b)", "-1*(a + b)"], - EStringTest["(-a + -b)", "-(a + b)"], + EStringTest["(-a - b)", "-1*(a + b)"], + EStringTest["(-a - b)", "-(a + b)"], EStringTest["((-1.)*(a + b))", "-1.*(a + b)"], - EStringTest["(-a + -b)", "(a + b)/-1"], + EStringTest["(-a - b)", "(a + b)/-1"], EStringTest["((-1.)*(a + b))", "(a + b)/-1."], (*Test that we do not delete all the addends*) @@ -213,6 +213,9 @@ ESameTest[I/(2 Sqrt[3] a^2), (0+1/6*I)*3^(1/2)*a^(-2)], (* Test wouldntBeLessThanNegOne. *) ESameTest[(1/3)*3^(-1/2), (1/3)*3^(-1/2)], + + ESameTest[Sqrt[2/\[Pi]], Sqrt[2]*Sqrt[1/Pi]], + ESameTest[Sqrt[3/(2 \[Pi])], Sqrt[3/2]*Sqrt[1/Pi]], ], EKnownFailures[ ESameTest[-2^(1/3), (-2)*2^(-2/3)], ESameTest[-2^(1+a), (-2)*2^(a)], diff --git a/expreduce/resources/calculus.m b/expreduce/resources/calculus.m index 696e6ef..3bb5c8e 100644 --- a/expreduce/resources/calculus.m +++ b/expreduce/resources/calculus.m @@ -14,6 +14,7 @@ D[Sin[a_], x_] := D[a,x] Cos[a]; D[Cos[a_], x_] := -D[a,x] Sin[a]; D[Exp[x_Symbol], x_Symbol] := Exp[x]; +D[l_List, x_] := Table[D[l[[idx]], x], {idx, Length[l]}]; Attributes[D] = {ReadProtected, Protected}; Tests`D = { ESimpleExamples[ @@ -39,7 +40,17 @@ ESameTest[-Cos[Cos[x]] Sin[x], D[Sin[Cos[x]],x]], ESameTest[Cos[Log[x]]/x, D[Sin[Log[x]],x]], ESameTest[-(Sin[Log[x]]/x), D[Cos[Log[x]],x]], - ESameTest[1-(1+Cot[x]) Sin[x+Log[Sin[x]]], D[Cos[Log[Sin[x]]+x]+x,x]] + ESameTest[1-(1+Cot[x]) Sin[x+Log[Sin[x]]], D[Cos[Log[Sin[x]]+x]+x,x]], + ESameTest[{a,b}, D[{a*x, b*x}, x]], + ] +}; + +Grad::usage = "`Grad[e, {var1, var2, ...}]` finds the gradient of `e` with respect to the named variables."; +Grad[e_,vars_List]:=Table[D[e,vars[[idx]]],{idx,Length[vars]}]; +Attributes[Grad] = {ReadProtected, Protected}; +Tests`Grad = { + ESimpleExamples[ + ESameTest[{-Sin[x+2 y],-2 Sin[x+2 y]}, Grad[Cos[x+2y],{x,y}]], ] }; @@ -67,6 +78,10 @@ Integrate[a_,{x_Symbol,start_,end_}] := (ReplaceAll[Integrate[a, x],x->end] - ReplaceAll[Integrate[a, x],x->start]) // Simplify; Integrate[a_,x_Symbol] := Module[{cleanedA, replaceRules}, + If[!MemberQ[$ContextPath, "Rubi`"], + Print["Loading Rubi rules for integration. This happens once. Preload on startup with -preloadrubi."]; + LoadRubiBundledSnapshot[] + ]; replaceRules = genSubscriptReplacements[a]; cleanedA = a /. replaceRules[[1]]; (Rubi`Int[cleanedA, x] /. replaceRules[[2]]) // Simplify diff --git a/expreduce/resources/matrix.m b/expreduce/resources/matrix.m index 7e9ae56..8a76d30 100644 --- a/expreduce/resources/matrix.m +++ b/expreduce/resources/matrix.m @@ -74,6 +74,7 @@ }; Dot::usage = "`a.b` computes the product of `a` and `b` for vectors and matrices."; +Dot[m_?MatrixQ,v_?VectorQ]:=Table[m[[idx]].v,{idx,Length[v]}]; Attributes[Dot] = {Flat, OneIdentity, Protected}; Tests`Dot = { ESimpleExamples[ diff --git a/expreduce/resources/trig.m b/expreduce/resources/trig.m index ec8955a..93fa0fc 100644 --- a/expreduce/resources/trig.m +++ b/expreduce/resources/trig.m @@ -116,3 +116,23 @@ ESameTest[(I (E^(-I x)-E^(I x)))/(E^(-I x)+E^(I x)), TrigToExp[Tan[x]]], ] }; + +Degree::usage = "`Degree` stands for Pi/180." +Degree = Pi/180; +Attributes[Degree] = {Constant,Protected,ReadProtected}; +Tests`Degree = { + ESimpleExamples[ + ESameTest[1, Sin[90 Degree]], + ] +}; + +RotationMatrix::usage = "`RotationMatrix[θ]` yields a rotation matrix for the angle `θ`."; +RotationMatrix[θ_] := {{Cos[θ],-Sin[θ]},{Sin[θ],Cos[θ]}}; +RotationMatrix[θ_, {x_, 0, 0}] := {{(x^3 Conjugate[x]^3)/Abs[x]^6,0,0},{0,Cos[\[Theta]],-((x^2 Conjugate[x] Sin[\[Theta]])/Abs[x]^3)},{0,(x Conjugate[x]^2 Sin[\[Theta]])/Abs[x]^3,(x^3 Conjugate[x]^3 Cos[\[Theta]])/Abs[x]^6}}; +RotationMatrix[θ_, {0, y_, 0}] := {{Cos[\[Theta]],0,(y^2 Conjugate[y] Sin[\[Theta]])/Abs[y]^3},{0,(y^3 Conjugate[y]^3)/Abs[y]^6,0},{-((y Conjugate[y]^2 Sin[\[Theta]])/Abs[y]^3),0,(y^3 Conjugate[y]^3 Cos[\[Theta]])/Abs[y]^6}}; +RotationMatrix[θ_, {0, 0, z_}] := {{Cos[\[Theta]],-((z Sin[\[Theta]])/Abs[z]),0},{(Conjugate[z] Sin[\[Theta]])/Abs[z],(z Conjugate[z] Cos[\[Theta]])/Abs[z]^2,0},{0,0,(z Conjugate[z])/Abs[z]^2}}; +Tests`RotationMatrix = { + ESimpleExamples[ + ESameTest[{{0, -1}, {1, 0}}, RotationMatrix[90 Degree]], + ] +}; diff --git a/expreduce/string.go b/expreduce/string.go index 59dbcf9..f6709f1 100644 --- a/expreduce/string.go +++ b/expreduce/string.go @@ -75,8 +75,17 @@ func toStringInfixAdvanced(parts []expreduceapi.Ex, delim string, thisHead strin buffer.WriteString(start) } nextParams := params - nextParams.PreviousHead = thisHead for i := 0; i < len(parts); i++ { + useTeXGrouping := false + if thisHead == "System`Power" && params.Form == "TeXForm" && i == 1 { + nextParams.PreviousHead = "" + useTeXGrouping = true + } else { + nextParams.PreviousHead = thisHead + } + if useTeXGrouping { + buffer.WriteString("{") + } if surroundEachArg { buffer.WriteString("(") buffer.WriteString(parts[i].StringForm(nextParams)) @@ -84,6 +93,9 @@ func toStringInfixAdvanced(parts []expreduceapi.Ex, delim string, thisHead strin } else { buffer.WriteString(parts[i].StringForm(nextParams)) } + if useTeXGrouping { + buffer.WriteString("}") + } if i != len(parts)-1 { buffer.WriteString(delim) } diff --git a/images/jupyter_screenshot.png b/images/jupyter_screenshot.png new file mode 100644 index 0000000..7ce0099 Binary files /dev/null and b/images/jupyter_screenshot.png differ