====== Wyrażenia matematyczne ======
Podczas laboratorium zaimplementujemy kilka klas pozwalających na reprezentację prostych wyrażeń matematycznych (funkcji jednej lub kilku zmiennych) jako grafów.
Podobna reprezentacja jest rdzeniem funkcji oferowanych na przykład przez [[https://live.sympy.org/]] lub [[https://www.derivative-calculator.net/]]. Nie będziemy jednak "parsowali" wyrażeń, czyli budowali grafowej reprezentacji na podstawie specyfikacji tekstowej. Zamiast tego, wyrażenia będą budowane za pomocą odpowiednio skonstruowanego kodu.
Założona funkcjonalność:
*budowa wyrażeń
*obliczanie wartości wyrażeń/funkcji dla różnych wartości zmiennych
*wydruk wyrażeń
*symboliczne obliczanie pochodnych [opcjonalnie]
===== Jakie klasy będą potrzebne =====
Przeanalizujmy wyrażenie $ 2x^3 + x^2 -2x + 7 $
Jego elementami są:
*stałe (np 2 i 7)
*zmienne (x)
*funkcja potęgowa ($x^3$, $x^2$) -- ''Power(arg,p)''
*operacja mnożenia ''Prod(args...)''
*operacja dodawania ''Sum(args...)''
Wyrażenie $ 2x^3 + x^2 -2x + 7 $ może być zapisane jako ''Sum(Prod(2,Power(x,3)),Power(x,2),Prod(-2,x),7)''. Zakładając, że każdy z tych elementów będzie obiektem, otrzymamy reprezentację w postaci acyklicznego grafu, jak na poniższym rysunku. Węzłami grafu są stałe, zmienne, operacje i funkcje, natomiast krawędzie odpowiadają argumentom poszczególnych operacji.
W zasadzie jest to drzewo, którego liśćmi są zmienne i stałe. Ponieważ zmienne mogą powtarzać się, liście będące zmiennymi zostały złączone
{{ :po:expressions-example.png?600 |Przykładowa reprezentacja wyrażenia 2x^3 + x^2-2x+7}}
Zaimplementujemy więc klasy, jak na poniższym diagramie i dodamy wspólnego przodka, abstrakcyjną klasę ''Node''. Niebieskie linie to asocjacje/agregacje łączące węzły nadrzędne (opreacje, funkcje) i ich argumenty.
*Argumentem ''Power'' jest jakieś wyrażenie, czyli ''Node''.
*''Sum'' może mieć więcej argumentów, stąd agregacja elementów ''Node''
*''Prod'' (//product//) to iloczyn większej liczby czynników ''Node''
{{ :po:expressions-classes.png?600 |Klasy}}
==== Implementujemy klasy ====
=== Node ===
abstract public class Node {
int sign=1;
Node minus(){
sign = -1;
return this;
}
Node plus(){
sign = 1;
return this;
}
int getSign(){return sign;}
/**
* Oblicza wartość wyrażenia dla danych wartości zmiennych
* występujących w wyrażeniu
*/
abstract double evaluate();
/**
*
* zwraca tekstową reprezentację wyrażenia
*/
public String toString(){return "";}
/**
*
* Zwraca liczbę argumentów węzła
*/
int getArgumentsCount(){return 0;}
}
W klasie ''Node'' wprowadzono atrybut ''sign'' znak, dziedziczony przez klasy potomne. Potraktujmy to następująco. Załóżmy, że reprezentacją tekstową węzła jest ''abcd''
*jeśli ''sign<0'' reprezentacją tekstową będzie ''-(abcd)'' [w nawiasach]
*jeśli ''sign>=0'' będzie to po prostu ''abcd'' [bez nawiasów]
=== Constant===
public class Constant extends Node {
double value;
Constant(double value){
this.sign = value<0?-1:1;
this.value = value<0?-value:value;
}
@Override
double evaluate() {
return sign*value;
}
@Override
public String toString() {
String sgn=sign<0?"-":"";
// return sgn+Double.toString(value);
DecimalFormat format = new DecimalFormat("0.#####",new DecimalFormatSymbols(Locale.US));
return sgn+format.format(value);
}
}
Stosując ''DecimalFormat'' pozbędziemy się niepotrzebnych zer na końcu wartości ''double''.
=== Variable ===
Klasa ''Variable'' reprezentuje zmienną, która może być użyta w wyrażeniach.
public class Variable extends Node {
String name;
Double value;
Variable(String name){
this.name = name;
}
void setValue(double d){
value = d;
}
@Override
double evaluate() {
return sign*value;
}
@Override
public String toString() {
String sgn=sign<0?"-":"";
return sgn+name;
}
}
=== Power ===
''Power'' jest jedyną zaimplementowaną funkcją. Zauważmy, że $Power(x,-1)=\frac{1}{x}$. Argumentem ''Power'' nie musi być pojedyncza zmienna. Może to być np.:
''Power(Sum(x,2),-2)'', czyli $\frac{1}{(x+2)^2}$
W kodzie pokazano przykładową implementację ''toString()'' z próbą wstawienia nawiasów w takich przypadkach, jak $-(-x^5)$ lub $(x+1)^2$
public class Power extends Node {
double p;
Node arg;
Power(Node n,double p){
arg = n;
this.p = p;
}
@Override
double evaluate() {
double argVal = arg.evaluate();
return Math.pow(argVal,p);
}
int getArgumentsCount(){return 1;}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
if(sign<0)b.append("-");
int argSign = arg.getSign();
int cnt = arg.getArgumentsCount();
boolean useBracket = false;
if(argSign<0 ||cnt>1)useBracket = true;
String argString = arg.toString();
if(useBracket)b.append("(");
b.append(argString);
if(useBracket)b.append(")");
b.append("^");
b.append(p);
return b.toString();
}
}
=== Sum ===
Klasa ''Sum'' jest wyrażeniem reprezentującą sumę argumentów. Zawiera listę węzłów ''List args = new ArrayList<>();''
Teoretycznie, lista argumentów może liczyć 0,1,2,... elementów. Metoda ''add()'' dodaje składniki do listy
public class Sum extends Node {
List args = new ArrayList<>();
Sum(){}
Sum(Node n1, Node n2){
args.add(n1);
args.add(n2);
}
Sum add(Node n){
args.add(n);
return this;
}
Sum add(double c){
args.add(new Constant(c));
return this;
}
Sum add(double c, Node n) {
Node mul = new Prod(c,n);
args.add(mul);
return this;
}
@Override
double evaluate() {
double result =0;
oblicz sumę wartości zwróconych przez wywołanie evaluate skłądników sumy
return sign*result;
}
int getArgumentsCount(){return args.size();}
public String toString(){
StringBuilder b = new StringBuilder();
if(sign<0)b.append("-(");
//zaimplementuj
if(sign<0)b.append(")");
return b.toString();
}
}
=== Prod ===
''Prod'' to iloczyn (ang. //product//). Reprezentuje wyrażenie będące iloczynem czynników. Metoda ''mul()'' dodaje czynnik do listy.
public class Prod extends Node {
List args = new ArrayList<>();
Prod(){}
Prod(Node n1){
args.add(n1);
}
Prod(double c){
wywołaj konstruktor jednoargumentowy przekazując new Constant(c)
}
Prod(Node n1, Node n2){
args.add(n1);
args.add(n2);
}
Prod(double c, Node n){
wywołaj konstruktor dwuargumentowy
}
Prod mul(Node n){
args.add(n);
return this;
}
Prod mul(double c){
???
}
@Override
double evaluate() {
double result =1;
// oblicz iloczyn czynników wołąjąc ich metodę evaluate
return sign*result;
}
int getArgumentsCount(){return args.size();}
public String toString(){
StringBuilder b = new StringBuilder();
if(sign<0)b.append("-");
// ...
zaimplementuj
return b.toString();
}
}
==== Testujemy implementację ====
=== Test 1 ===
static void buildAndPrint(){
Variable x = new Variable("x");
Node exp = new Sum()
.add(2.1,new Power(x,3))
.add(new Power(x,2))
.add(-2,x)
.add(7);
System.out.println(exp.toString());
}
Oczekiwany wynik: ''2.1*x^3 + x^2 + (-2)*x + 7''
=== Test 2 ===
static void buildAndEvaluate(){
Variable x = new Variable("x");
Node exp = new Sum()
.add(new Power(x,3))
.add(-2,new Power(x,2))
.add(-1,x)
.add(2);
for(double v=-5;v<5;v+=0.1){
x.setValue(v);
System.out.printf(Locale.US,"f(%f)=%f\n",v,exp.evaluate());
}
Sprawdź, jakie są pierwiastki?
=== Test 3 ===
static void defineCircle(){
Variable x = new Variable("x");
Variable y = new Variable("y");
Node circle = new Sum()
.add(new Power(x,2))
.add(new Power(y,2))
.add(8,x)
.add(4,y)
.add(16);
System.out.println(circle.toString());
double xv = 100*(Math.random()-.5);
double yv = 100*(Math.random()-.5);
x.setValue(xv);
y.setValue(yv);
double fv = circle.evaluate();
System.out.print(String.format("Punkt (%f,%f) leży %s koła %s",xv,yv,(fv<0?"wewnątrz":"na zewnątrz"),circle.toString()));
}
Znajdź i wypisz 100 punktów leżących wewnątrz okręgu.... Oczywiście napisz kod, który je znajduje ;-)
===== Symboliczne obliczanie pochodnych =====
**Zadanie do realizacji w domu.**
Pochodna wyrażenia jest również wyrażeniem... Dodajmy metodę ''diff()'' w klasie ''Node''
abstract Node diff(Variable var);
Dodajmy jeszcze metodę
abstract boolean isZero();
która będzie służyła do optymalizacji wywołań (pomijamy wyrażenia o wartości 0).
==== Pochodne ====
=== Constant ===
Zawsze pochodną stałej jest zero.
@Override
Node diff(Variable var) {
return new Constant(0);
}
=== Variable ===
Pochodna $\frac{d}{dx}(x)=1$, natomiast $\frac{d}{dx}(y)=0$
Node diff(Variable var) {
if(var.name.equals(name))return new Constant(sign);
else return new Constant(0);
}
=== Power ===
Z definicji $\frac{d}{dx}(x^n)=nx^{n-1}$, ale tak naprawdę powinniśmy potraktować to jako złożenie funkcji $Power(arg(x)),p$, stąd
$$\frac{d}{dx}(arg(x)^n) = n\cdot arg(x)^{n-1}\cdot \frac{d}{dx}arg(x)$$
Node diff(Variable var) {
Prod r = new Prod(sign*p,new Power(arg,p-1));
r.mul(arg.diff(var));
return r;
}
=== Sum ===
Pochodna sumy jest sumą pochodnych. Najprostsza implementacja jest podana poniżej.
Node diffVanilla(Variable var) {
Sum r = new Sum();
for(Node n:args){
r.add(n.diff(var));
}
return r;
}
Niestety, dużo pochodnych składowych będzie zerowych. Stąd propozycja usprawnienia przez sprawdzenie przed dodaniem, czy wyrażenie nie jest zerem.
=== Prod ===
Ogólny algorytm iloczynu jest podany np. tu [[https://en.wikipedia.org/wiki/Product_rule]].
Zauważmy, że stosuje się to także do funkcji stałych (w projekcie ''Constant''):
$$\frac{d}{dx}(2\cdot f(x))=\frac{d}{dx}(2)\cdot f(x) + 2\cdot \frac{d}{dx}f(x)$$
$$=0\cdot f(x) + 2\cdot \frac{d}{dx} f(x)$$
Stąd najprostsza implementacja ma postać:
Node diffVanilla(Variable var) {
Sum r = new Sum();
for(int i=0;i
Niestety, w jej wyniku może powstać sporo wyrażeń zerowych...
==== Testy ====
=== Test 1 ===
static void diffPoly() {
Variable x = new Variable("x");
Node exp = new Sum()
.add(2,new Power(x,3))
.add(new Power(x,2))
.add(-2,x)
.add(7);
System.out.print("exp=");
System.out.println(exp.toString());
Node d = exp.diff(x);
System.out.print("d(exp)/dx=");
System.out.println(d.toString());
}
Wynik
exp=2*x^3 + x^2 + (-2)*x + 7
d(exp)/dx=0*x^3 + 2*3*x^2*1 + 2*x^1*1 + 0*x + (-2)*1 + 0
=== Test 2 ===
static void diffCircle() {
Variable x = new Variable("x");
Variable y = new Variable("y");
Node circle = new Sum()
.add(new Power(x,2))
.add(new Power(y,2))
.add(8,x)
.add(4,y)
.add(16);
System.out.print("f(x,y)=");
System.out.println(circle.toString());
Node dx = circle.diff(x);
System.out.print("d f(x,y)/dx=");
System.out.println(dx.toString());
System.out.print("d f(x,y)/dy=");
Node dy = circle.diff(y);
System.out.println(dy.toString());
}
Wynik
f(x,y)=x^2 + y^2 + 8*x + 4*y + 16
d f(x,y)/dx=2*x^1*1 + 2*y^1*0 + 0*x + 8*1 + 0*y + 4*0 + 0
d f(x,y)/dy=2*x^1*0 + 2*y^1*1 + 0*x + 8*0 + 0*y + 4*1 + 0
==== Dodaj rozszerzenia i optymalizacje ====
**Pomijaj przy sumowaniu wyrażenia zerowe** Przed utworzeniem węzłów ''Sum'' i ''Prod'' umieść wyrażenia na listach.
*Jeżeli lista jest pusta, zwróć ''new Constant(0)''
*Jeżeli lista zawiera jeden element, po prostu zwróć go
Wynik dla circle:
f(x,y)=x^2 + y^2 + 8*x + 4*y + 16
d f(x,y)/dx=2*x^1*1 + 8*1
d f(x,y)/dy=2*y^1*1 + 4*1
**Dodaj metodę ''simplify()'' **, która zastąpi iloczyn stałych pojedynczą stałą. Spłaszcz strukturę ''Constant()*Product(args)'' do pojedynczego iloczynu.
exp=2*x^3 + x^2 + (-2)*x + 7
d(exp)/dx=6*x^2 + 2*x^1 + (-2)
f(x,y)=x^2 + y^2 + 8*x + 4*y + 16
d f(x,y)/dx=2*x^1 + 8
d f(x,y)/dy=2*y^1 + 4
**Dodaj klasy odpowiadające innym funkcjom**, np. exp(), log(), sin(), cos(), itd... A w przyszłości może uda się rozwinąć program do postaci podobnej do [[https://www.derivative-calculator.net/]]