first commit cont.
This commit is contained in:
459
chapters/pcomplex.qmd
Normal file
459
chapters/pcomplex.qmd
Normal file
@@ -0,0 +1,459 @@
|
||||
|
||||
# Ein Fallbeispiel: Der parametrisierte Datentyp PComplex
|
||||
|
||||
Wir wollen als neuen numerischen Typen **komplexe Zahlen in Polardarstellung $z=r e^{i\phi}=(r,ϕ)$** einführen.
|
||||
|
||||
- Der Typ soll sich in die Typhierarchie einfügen als Subtyp von 'Number'.
|
||||
- $r$ und $\phi$ sollen Gleitkommazahlen sein. (Im Unterschied zu komplexen Zahlen in 'kartesischen' Koordinaten hat eine Einschränkung auf ganzzahlige Werte von r oder ϕ mathematisch wenig Sinn.)
|
||||
|
||||
## Die Definition von `PComplex`
|
||||
|
||||
Ein erster Versuch könnte so aussehen:
|
||||
|
||||
```{julia}
|
||||
struct PComplex1{T <: AbstractFloat} <: Number
|
||||
r :: T
|
||||
ϕ :: T
|
||||
end
|
||||
|
||||
z1 = PComplex1(-32.0, 33.0)
|
||||
z2 = PComplex1{Float32}(12, 13)
|
||||
@show z1 z2;
|
||||
```
|
||||
|
||||
Julia stellt automatisch *default constructors* zur Verfügung:
|
||||
|
||||
- den Konstruktor `PComplex1`, bei dem der Typ `T` von den übergebenen Argumenten abgeleitet wird und
|
||||
- Konstruktoren `PComplex{Float64},...` mit expliziter Typangabe. Hier wird versucht, die Argumente in den angeforderten Typ zu konvertieren.
|
||||
|
||||
------
|
||||
|
||||
Wir wollen nun, dass der Konstruktor noch mehr tut.
|
||||
In der Polardarstellung soll $0\le r$ und $0\le \phi<2\pi$ gelten.
|
||||
|
||||
Wenn die übergebenen Argumente das nicht erfüllen, sollten sie entsprechend umgerechnet werden.
|
||||
|
||||
Dazu definieren wir einen _inner constructor_, der den _default constructor_ ersetzt.
|
||||
|
||||
- Ein _inner constructor_ ist eine Funktion innerhalb der `struct`-Definition.
|
||||
- In einem _inner constructor_ kann man die spezielle Funktion `new` verwenden, die wie der _default constructor_ wirkt.
|
||||
|
||||
|
||||
```{julia}
|
||||
struct PComplex{T <: AbstractFloat} <: Number
|
||||
r :: T
|
||||
ϕ :: T
|
||||
|
||||
function PComplex{T}(r::T, ϕ::T) where T<:AbstractFloat
|
||||
if r<0 # flip the sign of r and correct phi
|
||||
r = -r
|
||||
ϕ += π
|
||||
end
|
||||
if r==0 ϕ=0 end # normalize r=0 case to phi=0
|
||||
ϕ = mod(ϕ, 2π) # map phi into interval [0,2pi)
|
||||
new(r, ϕ) # new() ist special function,
|
||||
end # available only inside inner constructors
|
||||
|
||||
end
|
||||
```
|
||||
|
||||
```{julia}
|
||||
#| echo: false
|
||||
#| output: false
|
||||
|
||||
#=
|
||||
in den ganzen quarto-runs wollen bir hier noch das default-show benutzen
|
||||
=#
|
||||
zz = @which Base.show(stdout, PComplex{Float64}(2.,3.))
|
||||
if zz.module != Base
|
||||
Base.delete_method(zz)
|
||||
end
|
||||
```
|
||||
```{julia}
|
||||
z1 = PComplex{Float64}(-3.3, 7π+1)
|
||||
```
|
||||
Für die explizite Angabe eines *inner constructors* müssen wir allerdings einen Preis zahlen: Die sonst von Julia bereitgestellten *default constructors* fehlen.
|
||||
|
||||
Den Konstruktor, der ohne explizite Typangabe in geschweiften Klammern auskommt und den Typ der Argumente übernimmt, wollen wir gerne auch haben:
|
||||
|
||||
```{julia}
|
||||
PComplex(r::T, ϕ::T) where {T<:AbstractFloat} = PComplex{T}(r,ϕ)
|
||||
|
||||
z2 = PComplex(2.0, 0.3)
|
||||
```
|
||||
|
||||
|
||||
## Eine neue Schreibweise
|
||||
|
||||
Julia verwendet `//` als Infix-Konstruktor für den Typ `Rational`. Sowas Schickes wollen wir auch.
|
||||
|
||||
In der Elektronik/Elektrotechnik werden [Wechselstromgrößen durch komplexe Zahlen beschrieben.](https://de.wikipedia.org/wiki/Komplexe_Wechselstromrechnung). Dabei ist eine Darstellung komplexer Zahlen durch "Betrag" und "Phase" üblich und sie wird gerne in der sogenannten [Versor-Form](https://de.wikipedia.org/wiki/Versor) (engl. *phasor*) dargestellt:
|
||||
$$
|
||||
z= r\enclose{phasorangle}{\phi} = 3.4\;\enclose{phasorangle}{45^\circ}
|
||||
$$
|
||||
wobei man in der Regel den Winkel in Grad notiert.
|
||||
|
||||
:::{.callout-note .titlenormal collapse="true"}
|
||||
|
||||
## Mögliche Infix-Operatoren in Julia
|
||||
|
||||
In Julia ist eine große Anzahl von Unicode-Zeichen reserviert für die Verwendung als Operatoren. Die definitive Liste ist im [Quellcode des Parsers.](https://github.com/JuliaLang/julia/blob/eaa2c58aeb12f27c1d8c116ab111773a4fc4495f/src/julia-parser.scm#L13-L31)
|
||||
|
||||
Auf Details werden wir in einem späteren Kapitel noch eingehen.
|
||||
|
||||
Und ja, der Julia-Parser ist in einem Lisp(genauer: Scheme)-Dialekt geschrieben. In Julia ist ein kleiner Scheme-Interpreter namens [femtolisp](https://github.com/JeffBezanson/femtolisp) integriert. Geschrieben hat ihn einer der "Väter" von Julia bevor er mit der Arbeit an Julia begann.
|
||||
|
||||
:::
|
||||
|
||||
Das Winkel-Zeichen `∠` steht leider nicht als Operatorsymbol zur Verfügung. Wir weichen aus auf `⋖`. Das kann in Julia als als `\lessdot<tab>` eingegeben werden.
|
||||
|
||||
```{julia}
|
||||
⋖(r::Real, ϕ::Real) = PComplex(r, π*ϕ/180)
|
||||
|
||||
z3 = 2. ⋖ 90.
|
||||
```
|
||||
(Die Typ-Annotation -- `Real` statt `AbstractFloat` -- ist ein Vorgriff auf kommende weitere Konstruktoren. Im Moment funktioniert der Operator `⋖` erstmal nur mit `Float`s.)
|
||||
|
||||
|
||||
Natürlich wollen wir auch die Ausgabe so schön haben. Details dazu findet man in der [Dokumentation](https://docs.julialang.org/en/v1/manual/types/#man-custom-pretty-printing).
|
||||
|
||||
```{julia}
|
||||
using Printf
|
||||
|
||||
function Base.show(io::IO, z::PComplex)
|
||||
# wir drucken die Phase in Grad, auf Zehntelgrad gerundet,
|
||||
p = z.ϕ * 180/π
|
||||
sp = @sprintf "%.1f" p
|
||||
print(io, z.r, "⋖", sp, '°')
|
||||
end
|
||||
|
||||
@show z3;
|
||||
```
|
||||
|
||||
|
||||
## Methoden für `PComplex`
|
||||
|
||||
Damit unser Typ ein anständiges Mitglied der von `Number` abstammenden Typfamilie wird, brauchen wir allerdings noch eine ganze Menge mehr. Es müssen Arithmetik, Vergleichsoperatoren, Konvertierungen usw. definiert werden.
|
||||
|
||||
|
||||
Wir beschränken uns auf Multiplikation und Quadratwurzeln.
|
||||
|
||||
|
||||
:::{.callout-note collapse="true"}
|
||||
## Module
|
||||
|
||||
- Um die `methods` der existierenden Funktionen und Operationen zu ergänzen, muss man diese mit ihrem 'vollen Namen' ansprechen.
|
||||
- Alle Objekte gehören zu einem Namensraum oder `module`.
|
||||
- Die meisten Basisfunktionen gehören zum Modul `Base`, welches standardmäßig immer ohne explizites `using ...` geladen wird.
|
||||
- Solange man keine eigenen Module definiert, sind die eigenen Definitionen im Modul `Main`.
|
||||
- Das Macro `@which`, angewendet auf einen Namen, zeigt an, in welchem Modul der Name definiert wurde.
|
||||
|
||||
```{julia}
|
||||
f(x) = 3x^3
|
||||
@which f
|
||||
```
|
||||
|
||||
```{julia}
|
||||
wp = @which +
|
||||
ws = @which(sqrt)
|
||||
println("Modul für Addition: $wp, Modul für sqrt: $ws")
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
```{julia}
|
||||
qwurzel(z::PComplex) = PComplex(sqrt(z.r), z.ϕ / 2)
|
||||
```
|
||||
|
||||
```{julia}
|
||||
#| echo: false
|
||||
#| output: false
|
||||
|
||||
#=
|
||||
damit das length(methods(sqrt)) klappt
|
||||
=#
|
||||
if hasmethod(sqrt, (PComplex,))
|
||||
zz = @which Base.sqrt(PComplex{Float64}(1.,1.))
|
||||
Base.delete_method(zz)
|
||||
end
|
||||
```
|
||||
|
||||
Die Funktion `sqrt()` hat schon einige Methoden:
|
||||
```{julia}
|
||||
length(methods(sqrt))
|
||||
```
|
||||
|
||||
Jetzt wird es eine Methode mehr:
|
||||
```{julia}
|
||||
Base.sqrt(z::PComplex) = qwurzel(z)
|
||||
|
||||
length(methods(sqrt))
|
||||
```
|
||||
|
||||
```{julia}
|
||||
sqrt(z2)
|
||||
```
|
||||
|
||||
und nun zur Multiplikation:
|
||||
|
||||
```{julia}
|
||||
Base.:*(x::PComplex, y::PComplex) = PComplex(x.r * y.r, x.ϕ + y.ϕ)
|
||||
|
||||
@show z1 * z2;
|
||||
```
|
||||
(Da das Operatorsymbol kein normaler Name ist, muss der Doppelpunkt bei der Zusammensetzung mit `Base.` sein.)
|
||||
|
||||
Wir können allerdings noch nicht mit anderen numerischen Typen multiplizieren. Dazu könnte man nun eine Vielzahl von entsprechenden Methoden definieren. Julia stellt *für numerische Typen* noch einen weiteren Mechanismus zur Verfügung, der das etwas vereinfacht.
|
||||
|
||||
|
||||
## Typ-Promotion und Konversion
|
||||
|
||||
In Julia kann man bekanntlich die verschiedensten numerischen Typen nebeneinander verwenden.
|
||||
|
||||
```{julia}
|
||||
1//3 + 5 + 5.2 + 0xff
|
||||
```
|
||||
Wenn man in die zahlreichen Methoden schaut, die z.B. für `+` und `*` definiert sind, findet man u.a. eine Art 'catch-all-Definition'
|
||||
|
||||
```julia
|
||||
+(x::Number, y::Number) = +(promote(x,y)...)
|
||||
*(x::Number, y::Number) = *(promote(x,y)...)
|
||||
```
|
||||
|
||||
|
||||
|
||||
(Die 3 Punkte sind der splat-Operator, der das von promote() zurückgegebene Tupel wieder in seine Bestandteile zerlegt.)
|
||||
|
||||
Da die Methode mit den Typen `(Number, Number)` sehr allgemein ist, wird sie erst verwendet, wenn spezifischere Methoden nicht greifen.
|
||||
|
||||
Was passiert hier?
|
||||
|
||||
### Die Funktion `promote(x,y,...)`
|
||||
|
||||
Diese Funktion versucht, alle Argumente in einen gemeinsamen Typen umzuwandeln, der alle Werte (möglichst) exakt darstellen kann.
|
||||
|
||||
```{julia}
|
||||
promote(12, 34.555, 77/99, 0xff)
|
||||
```
|
||||
|
||||
```{julia}
|
||||
z = promote(BigInt(33), 27)
|
||||
@show z typeof(z);
|
||||
```
|
||||
|
||||
Die Funktion `promote()` verwendet dazu zwei Helfer, die Funktionen
|
||||
`promote_type(T1, T2)` und `convert(T, x)`
|
||||
|
||||
Wie üblich in Julia, kann man diesen Mechanismus durch [eigene *promotion rules* und `convert(T,x)`-Methoden erweitern.](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/)
|
||||
|
||||
|
||||
### Die Funktion `promote_type(T1, T2,...)`
|
||||
|
||||
Sie ermittelt, zu welchem Typ umgewandelt werden soll. Argumente sind Typen, nicht Werte.
|
||||
|
||||
```{julia}
|
||||
@show promote_type(Rational{Int64}, ComplexF64, Float32);
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Die Funktion `convert(T,x)`
|
||||
|
||||
Die Methoden von
|
||||
`convert(T, x)` wandeln `x` in ein Objekt vom Typ `T` um. Dabei sollte eine solche Umwandlung verlustfrei möglich sein.
|
||||
|
||||
```{julia}
|
||||
z = convert(Float64, 3)
|
||||
```
|
||||
|
||||
```{julia}
|
||||
z = convert(Int64, 23.00)
|
||||
```
|
||||
|
||||
```{julia}
|
||||
z = convert(Int64, 2.3)
|
||||
```
|
||||
|
||||
Die spezielle Rolle von `convert()` liegt darin, dass es an verschiedenen Stellen _implizit_ und automatisch eingesetzt wird:
|
||||
|
||||
> [The following language constructs call convert](https://docs.julialang.org/en/v1/manual/conversion-and-promotion/#When-is-convert-called?):
|
||||
>
|
||||
- Assigning to an array converts to the array's element type.
|
||||
- Assigning to a field of an object converts to the declared type of the field.
|
||||
- Constructing an object with new converts to the object's declared field types.
|
||||
- Assigning to a variable with a declared type (e.g. local x::T) converts to that type.
|
||||
- A function with a declared return type converts its return value to that type.
|
||||
|
||||
-- und natürlich in `promote()`
|
||||
|
||||
Für selbstdefinierte Datentypen kann man convert() um weitere Methoden ergänzen.
|
||||
|
||||
Für Datentypen innerhalb der Number-Hierarchie gibt es wieder eine 'catch-all-Definition'
|
||||
```julia
|
||||
convert(::Type{T}, x::Number) where {T<:Number} = T(x)
|
||||
```
|
||||
|
||||
Also: Wenn für einen Typen `T` aus der Hierarchie `T<:Number` ein Konstruktor `T(x)` mit einem numerischen Argument `x` existiert, dann wird dieser Konstruktor `T(x)` automatisch für Konvertierungen benutzt. (Natürlich können auch speziellere Methoden für `convert()` definiert werden, die dann Vorrang haben.)
|
||||
|
||||
|
||||
### Weitere Konstruktoren für `PComplex`
|
||||
|
||||
|
||||
```{julia}
|
||||
|
||||
## (a) r, ϕ beliebige Reals, z.B. Integers, Rationals
|
||||
|
||||
PComplex{T}(r::T1, ϕ::T2) where {T<:AbstractFloat, T1<:Real, T2<: Real} =
|
||||
PComplex{T}(convert(T, r), convert(T, ϕ))
|
||||
|
||||
PComplex(r::T1, ϕ::T2) where {T1<:Real, T2<: Real} =
|
||||
PComplex{promote_type(Float64, T1, T2)}(r, ϕ)
|
||||
|
||||
## (b) Zur Umwandlung von Reals: Konstruktor mit
|
||||
## nur einem Argument r
|
||||
|
||||
PComplex{T}(r::S) where {T<:AbstractFloat, S<:Real} =
|
||||
PComplex{T}(convert(T, r), convert(T, 0))
|
||||
|
||||
PComplex(r::S) where {S<:Real} =
|
||||
PComplex{promote_type(Float64, S)}(r, 0.0)
|
||||
|
||||
## (c) Umwandlung Complex -> PComplex
|
||||
|
||||
PComplex{T}(z::Complex{S}) where {T<:AbstractFloat, S<:Real} =
|
||||
PComplex{T}(abs(z), angle(z))
|
||||
|
||||
PComplex(z::Complex{S}) where {S<:Real} =
|
||||
PComplex{promote_type(Float64, S)}(abs(z), angle(z))
|
||||
|
||||
|
||||
```
|
||||
|
||||
|
||||
Ein Test der neuen Konstruktoren:
|
||||
|
||||
```{julia}
|
||||
|
||||
3//5 ⋖ 45, PComplex(Complex(1,1)), PComplex(-13)
|
||||
|
||||
```
|
||||
|
||||
|
||||
Wir brauchen nun noch *promotion rules*, die festlegen, welcher Typ bei `promote(x::T1, y::T2)` herauskommen soll. Damit wird `promote_type()` intern um die nötigen weiteren Methoden erweitert.
|
||||
|
||||
### *Promotion rules* für `PComplex`
|
||||
|
||||
|
||||
```{julia}
|
||||
Base.promote_rule(::Type{PComplex{T}}, ::Type{S}) where {T<:AbstractFloat,S<:Real} =
|
||||
PComplex{promote_type(T,S)}
|
||||
|
||||
Base.promote_rule(::Type{PComplex{T}}, ::Type{Complex{S}}) where
|
||||
{T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
|
||||
```
|
||||
1. Regel:
|
||||
: Wenn ein `PComplex{T}` und ein `S<:Real` zusammentreffen, dann sollen beide zu `PComplex{U}` umgewandelt werden, wobei `U` der Typ ist, zu dem `S` und `T` beide umgewandelt (_promoted_) werden können.
|
||||
|
||||
2. Regel
|
||||
: Wenn ein `PComplex{T}` und ein `Complex{S}` zusammentreffen, dann sollen beide zu `PComplex{U}` umgewandelt werden, wobei `U` der Typ ist, zu dem `S` und `T` beide umgewandelt werden können.
|
||||
|
||||
|
||||
|
||||
Damit klappt nun die Multiplikation mit beliebigen numerischen Typen.
|
||||
|
||||
```{julia}
|
||||
z3, 3z3
|
||||
```
|
||||
|
||||
```{julia}
|
||||
(3.0+2im) * (12⋖30.3), 12sqrt(z2)
|
||||
```
|
||||
|
||||
|
||||
|
||||
:::{.callout-caution icon="false" collapse="true" .titlenormal}
|
||||
|
||||
## Zusammenfassung: unser Typ `PComplex`
|
||||
|
||||
```julia
|
||||
struct PComplex{T <: AbstractFloat} <: Number
|
||||
r :: T
|
||||
ϕ :: T
|
||||
|
||||
function PComplex{T}(r::T, ϕ::T) where T<:AbstractFloat
|
||||
if r<0 # flip the sign of r and correct phi
|
||||
r = -r
|
||||
ϕ += π
|
||||
end
|
||||
if r==0 ϕ=0 end # normalize r=0 case to phi=0
|
||||
ϕ = mod(ϕ, 2π) # map phi into interval [0,2pi)
|
||||
new(r, ϕ) # new() ist special function,
|
||||
end # available only inside inner constructors
|
||||
|
||||
end
|
||||
|
||||
# additional constructors
|
||||
PComplex(r::T, ϕ::T) where {T<:AbstractFloat} = PComplex{T}(r,ϕ)
|
||||
|
||||
|
||||
PComplex{T}(r::T1, ϕ::T2) where {T<:AbstractFloat, T1<:Real, T2<: Real} =
|
||||
PComplex{T}(convert(T, r), convert(T, ϕ))
|
||||
|
||||
PComplex(r::T1, ϕ::T2) where {T1<:Real, T2<: Real} =
|
||||
PComplex{promote_type(Float64, T1, T2)}(r, ϕ)
|
||||
|
||||
|
||||
PComplex{T}(r::S) where {T<:AbstractFloat, S<:Real} =
|
||||
PComplex{T}(convert(T, r), convert(T, 0))
|
||||
|
||||
PComplex(r::S) where {S<:Real} =
|
||||
PComplex{promote_type(Float64, S)}(r, 0.0)
|
||||
|
||||
|
||||
PComplex{T}(z::Complex{S}) where {T<:AbstractFloat, S<:Real} =
|
||||
PComplex{T}(abs(z), angle(z))
|
||||
|
||||
PComplex(z::Complex{S}) where {S<:Real} =
|
||||
PComplex{promote_type(Float64, S)}(abs(z), angle(z))
|
||||
|
||||
# nice input
|
||||
⋖(r::Real, ϕ::Real) = PComplex(r, π*ϕ/180)
|
||||
|
||||
# nice output
|
||||
using Printf
|
||||
|
||||
function Base.show(io::IO, z::PComplex)
|
||||
# wir drucken die Phase in Grad, auf Zehntelgrad gerundet,
|
||||
p = z.ϕ * 180/π
|
||||
sp = @sprintf "%.1f" p
|
||||
print(io, z.r, "⋖", sp, '°')
|
||||
end
|
||||
|
||||
# arithmetic
|
||||
Base.sqrt(z::PComplex) = PComplex(sqrt(z.r), z.ϕ / 2)
|
||||
|
||||
Base.:*(x::PComplex, y::PComplex) = PComplex(x.r * y.r, x.ϕ + y.ϕ)
|
||||
|
||||
# promotion rules
|
||||
Base.promote_rule(::Type{PComplex{T}}, ::Type{S}) where
|
||||
{T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
|
||||
|
||||
Base.promote_rule(::Type{PComplex{T}}, ::Type{Complex{S}}) where
|
||||
{T<:AbstractFloat,S<:Real} = PComplex{promote_type(T,S)}
|
||||
```
|
||||
:::
|
||||
|
||||
:::{.content-hidden unless-format="xxx"}
|
||||
|
||||
Jetzt geht sowas wie `PComplex(1, 0)` noch nicht. Wir wollen auch andere reelle Typen für `r` und `ϕ` zulassen. Der Einfachheit halber wandeln wir hier alles nach `Float64` um. Analog verfahren wir auch, wenn nur ein reelles oder komplexes Argument verwendet wird.
|
||||
|
||||
```julia
|
||||
PComplex(r::Real, ϕ::Real) = PComplex(Float64(r), Float64(ϕ))
|
||||
PComplex(r::Real) = PComplex(Float64(r), 0.0)
|
||||
PComplex(z::Complex) = PComplex(abs(z), angle(z))
|
||||
|
||||
z3 = PComplex(-2); z4 = PComplex(3im)
|
||||
@show z3 z4;
|
||||
```
|
||||
|
||||
:::
|
||||
Reference in New Issue
Block a user