Doç. Dr. Özgür Baştürk
Ankara Üniversitesi, Astronomi ve Uzay Bilimleri Bölümü
obasturk at ankara.edu.tr
http://ozgur.astrotux.org
Her ne kadar nesne yönelimli programlama (object oriented programming) kavramını farklı programcı ve bilgisayar bilimcileri farklı şekillerde tanımlıyor olsalar da Nesne Yönelimli Programlama'yı sınıf hiyerarşilerine dayalı programcılık paradigması olarak tanımlamak daha doğrudur. Nesnelerle programlama' yı da nesne yönelimli programlama kavramı altında ele alanlar olsa dahi, örneğin Python'da her “şey” bir nesne olduğu için zaten her zaman yapılan nesnelerle programlamadır. Aradaki fark Python'un temel veri türlerinin (int, float, str, list, tuple, dict) dışında, kullanıcı tarafından tanımlanmış türler söz konusu olduğunda nesne yönelimli programlamadan bahsetmekle açıklığa kavuşturulabilir.
Kalıt (Inheritance) ve Sıradüzen (Hierarchy) Kavramları: Nesne yönelimli programcılıkta temel yaklaşım, birbirleriyle bağlantılı sınıfları bir araya getirerek tek bir birim (aile) şeklinde programlamaktır. Bu yaklaşım, programın detaylarının bir kenara bırakılmasına, daha kolay modifiye edilmesine ve geliştirilmesine yardımcı olur.
Sınıflardan oluşan bir aile tıpkı biyolojik bir aile gibi, “ebeveyn” sınıflar (parent) ve “çocuk” sınıflar (child) içerir. Böyle bir aileye sınıf sıradüzeni (class hierarchy) adı verilir. Çocuk sınıflar, ebeveyn sınıflarını veri ve metotlarını miras (ya da kalıt) olarak devralır (inheritance), bu veri ve metotları değiştirebilir ve kendi veri ve metotlarını bunlara ekleyebilir. Yani, bir işlevi olan bir sınıfınız var ve yeni bir işleve ihtiyacınız varsa, bu sınıfın bir “çocuğunu” yaratıp, bu işlevi onun yapmasını sağlayabilirsiniz. Böylece hem “ebeveyn” sınıf hala elinizdedir, hem de çocuk sınıf ebeveyn sınıftaki kodlar tekrar etmediğinden daha kısadır ve kolay manipüle edilir.
Bir program herhangi bir sınıfın çocuk ya da ebeveyn olduğuna bakmaksızın, tüm bir “aile ağacını” tek bir birim olarak değerlendirir ve bu ailedeki tüm üyelerle aynı şekilde çalışır.
“Ebeveyn” sınıf, ana sınıf (base class) ya da üst sınıf (superclass), “çocuk” sınıflar alt sınıf (subclass) ya da türetilmiş sınıf (derived class) olarak adlandırılırlar.
$y = c_0 + c_1 x$ şeklinde tanımlanan doğrular için tanımladığımız bir doğru sınıfını aşağıdaki şekilde kodladığımızı varsayalım.
class Dogru:
def __init__(self, c0, c1):
self.c0 = c0
self.c1 = c1
def __call__(self, x):
return self.c0 + self.c1*x
def tablo(self, L, R, n):
from numpy import linspace
"""L <= x <= R arasinda dogru uzerinde bulunan n noktayi bir
tablo seklinde listeler"""
s = ''
for x in linspace(L, R, n):
y = self(x)
s += '%12g %12g\n' % (x, y)
return s
Gördüğünüz gibi __init metodu $c_0$ ve $c_1$ öz niteliklerini (attribute) başlatıyor, \call__ metodu verilen bir $x$ değerinin doğruda karşılık geldiği y değerini hesaplıyor, $tablo$ metodu ise belirli bir aralık dahlinde, doğru üzerindeki $n$ adet noktayı ekrana bir tablo şeklinde listeliyor.
Python ve Nesne Yönelimli Programcılık yaklaşımını destekleyen diğer programlama dillerinde $Dogru$ sınıfı için yazdığımız kodları tekrar yazmamızı önleyen özel bir kod kurgusu mutlaka bulunur. Örneğin bir $Parabol$ sınıfını Dogru sınıfının kodlarını kalıt (miras, inheritance) alacak şekilde şekilde kodlamak mümkündür.
class Parabol(Dogru):
Böylece $Parabol$ sınıfı $Dogru$ sınıfının tüm kodlarını kalıt alır, bu nedenle $Parabol$ sınıfı, üst sınıfı olan $Dogru$ sınıfından türetilmiş (derived) bir alt sınıftır (subclass).
$Parabol$ sınıfı $Dogru$ sınıfıyla birebir aynı değildir, ona $c_2$ verisini (attribute) ekler ve __call__ metodu da $Dogru$ sınıfınkininden farklıdır. Ancak, $tablo$ metodu olduğu gibi miras alınabilir.
Parabol sınıfını __init başlatıcı metodu ve \call__ metodunu kullanarak bir alt sınıf olarak kurgulamak üzere aşağıdaki kod parçası kullanılabilir.
class Parabol(Dogru):
def __init__(self, c0, c1, c2):
Dogru.__init__(self, c0, c1)
self.c2 = c2
def __call__(self, x):
return Dogru.__call__(self, x) + self.c2*x**2
Daha sonra $Parabol$ sınıfının bir olgusu ($p$) başlatılır ve __call__ metoduyla doğrudan hesap yapmak üzere $x$ değeri bu olguya geçirilir ve bu sınıfın $tablo$ metodu aşağıdaki şekilde kullanılır.
Python'da bir $i$ olgusunun (instance), $t$ türüne (type) ait olup olmadığını kontrol etmek için $isinstance(i,t)$ şeklinde bir fonksiyon bulunmaktadır.
d = Dogru(-1,1)
isinstance(d,Dogru)
isinstance(d,Parabol)
Görüldüğü gibi bir doğru bir parabol değildir (yani $Dogru$ sınıfının bir olgusu olan $d$ aynı zamanda $Parabol$ sınıfının da bir olgusu değildir.) Tersi doğru mu görelim.
p = Parabol(-1,0,10)
isinstance(p,Parabol)
isinstance(p,Dogru)
Görüldüğü gibi bir parabol olgusu aynı zamanda bir doğru olgusudur. Zira onun bütün niteliklerini taşır. Her olgunun __class__ adı verilen özel bir öz niteliği (attribute) daha vardır ve olgunun türünü tutar.
p.__class__
Bu aşağıdaki şekilde kontrol edilebilir.
p.__class__ == Parabol
ya da ismine __name__ özniteliği ile ulaşılabilir.
p.__class__.__name__
Görüldüğü gibi p.__class\ bir sınıf nesnesi (ya da sınıf nesnesi tanımı, class definition, class object) iken p.\class.\name__ bir metindir. Bu karşılaştırmaya dayanan bir şartlı ifade için her iki şekli de aşağıdaki gibi kullanmak mümkündür. Ancak tavsiye edilen $isinstance(p, Parabol)$ fonksiyonunun kullanımıdır.
if p.__class__.__name__ == "Parabol":
print("bu calisir")
# ya da
if p.__class__ == Parabol:
print("bu da cailisir")
Diğer taraftan $issubclass(c1, c2)$ ifadesi $c_1$'in $c_2$'nin bir alt sınıfı olup olmadığını kontrol eder.
issubclass(Parabol, Dogru)
>>> issubclass(Dogru, Parabol)
Bir alt sınıfın üst sınıfları __class nesnesinin \bases öz niteliğinde demet (tuple) türünde tutulur. Bu öz niteliğin tüm elemanları birer sınıf nesnesi olacağı için bu sınıfların adını öğrenmek için de sınıf nesnesinin \name__ öz niteliğinin kullanılması gerekir.
p.__class__.__bases__
p.__class__.__bases__[0].__name__
$Parabol$ sınıfının $Dogru$ sınıfının tüm öz nitelik ve metotlarını miras olarak devralması yerine $Dogru$ sınıfının bir olgusunu öz nitelik olarak kullanmak da alternatif bir çözümdür.
class Parabol:
def __init__(self, c0, c1, c2):
self.dogru = Dogru(c0, c1) # c0 ve c2 Dogru sinifindan gelsin
self.c2 = c2
def __call__(self, x):
return self.c2*x**2 + self.dogru(x)
Hangi çözümün tercih edileceği probleme göre değişir. Eğer ”Parabol sınıfı Dogru sınıfının bir nesnesidir” ifadesi problem açısından da doğal geliyorsa $Parabol$ sınıfının $Dogru$ sınıfı ile ilişki “is-a-relationship” olarak tarif edilir. Eğer ”Parabol sınıfının bir Dogru nesnesine sahip olması gerektiği” düşünülürse o vakit bu ilişki “has-a-relationship” olarak tarif edilir. Verilen örnekte “is-a-relationship” daha doğal görünmektedir; çünkü, doğru parabolün özel bir şeklidir. Ancak programcılıkta bir problemin çok sayıda çözümü olduğunu ve programcının bu çözümler arasında çalışma hızı, kod uzunluğu ve basitliği gibi kriterler bakımından en optimumunu seçmesi gerektiğini de hatırlatmak gerekir.
Daha önce nümerik türev almak için basit bir yöntem verilmiş ve bu yöntemin formülüne dayanan $Turev$ isminde bir sınıf da oluşturulmuştu. Kullanılan formül (1. basamaktan geri türev) her fonksiyonun türevini almak için kullanılabilir ancak bunun için tek yöntem değildir. $f^{\prime}(x)$'i hesaplamak için başka formüller de mevcuttur.
Tüm bu formüller Python'a adapte edilmek istendiğinde her biri bir sınıf olarak tasarlanabilir ve bu sınıfların hepsi aynı öz niteliklerle ($f$ ve $h$) başlatılabilir. Dolayısıyla bir sınıf hiyerarşisi dahilinde nesne yönelimli programcılıkla soruyu çözmek doğal bir yönelimdir. Başlatıcı için tek bir kod parçası yazıp onu tüm sınıfların kullanması sağlanılabilir. $Turev$ adı verilen bir üst sınıf (superclass) yaratılıp, bu sınıfın __init fonksiyonuyla tüm alt sınıfların ihtiyaç duyacağı iki öz niteliği ($f$ ve $h$) başlatılabilir. Bu durumda diğer tüm sınıflar türevi ne şeklide hesaplıyorlarsa ona uygun bir \call__ metodu oluşturmak yeterli olacaktır.
class Turev:
def __init__(self, f, h=1E-9):
self.f = f
self.h = float(h)
class Ileri1(Turev):
def __call__(self, x):
f, h = self.f, self.h
return (f(x+h) - f(x))/h
class Geri1(Turev):
def __call__(self, x):
f, h = self.f, self.h
return (f(x) - f(x-h))/h
class Merkezi2(Turev):
def __call__(self, x):
f, h = self.f, self.h
return (f(x+h) - f(x-h))/(2*h)
Bu basit uyarlama sınıf hiyerarşisi içerisinde nesne yönelimli programlamanın basitliğini ve yeteneğini ortaya koymaktadır. Genel strateji, ortak kodu üst sınıf içerisinde kodlayıp, üst sınıftan farklılaşan tüm bileşenleri ise alt sınıflara taşımaktır. Bu durumda geriye kalan üç formül de koda kolaylıkla eklenebilir.
class Merkezi4(Turev):
def __call__(self, x):
f, h = self.f, self.h
return (4./3)*(f(x+h) - f(x-h)) /(2*h) - \
(1./3)*(f(x+2*h) - f(x-2*h))/(4*h)
class Merkezi6(Turev):
def __call__(self, x):
f, h = self.f, self.h
return (3./2) *(f(x+h) - f(x-h)) /(2*h) - \
(3./5) *(f(x+2*h) - f(x-2*h))/(4*h) + \
(1./10)*(f(x+3*h) - f(x-3*h))/(6*h)
class Ileri3(Turev):
def __call__(self, x):
f, h = self.f, self.h
return (-(1./6)*f(x+2*h) + f(x+h) - 0.5*f(x) - \
(1./3)*f(x-h))/h
Bu kodu örnek bir test durumu için çalıştırmak üzere aşağıdaki komutlar verilebiir.
from math import pi,sin
mycos = Merkezi4(sin)
print(mycos(pi))
$Merkezi4(sin)$ ifadesi öncelikle $f^{\prime}$ ve $h$'ı başlatmak üzere $Merkezi4$ sınıfı içinde başlatıcı metodu (__init) arar. Ancak bu metod bu sınıfın içinde olmadığı ve bu sınıf $Merkezi4(Turev)$ şeklinde tanımlandığı için bu kez üst sınıf olan $Turev$ sınıfının başlatıcı fonksiyonuna yönelir (çünkü $Turev$, $Merkezi4$ sınıfının üst sınıflarını veren $Merkezi4$.\bases listesinde yer alır). Bu şekilde $f$ ve $h$ başlatılır ($f$, $sin$; $h$ gönderilmediği için varsayılan olarak $10^{-9}$ değerlerini alır). Böylece $mycos$ olgusu oluşmuş olur. $mycos(pi)$ ifadesi ile bu olguya doğrudan bir değer gönderildiği için $Merkezi4$ sınıfının \call__ metodu aranır ve bulunduğu için çalıştırılır ve sonuç bu metot tarafından programa döndürülür ve $print$ ifadesi de onu ekrana yazdırır. Bu şekilde üst sınıflar içinde de metotların aranması işlemine bilgisyar biliminde dinamik bağlantı (dynamic binding) adı verilir.
Bu örnekte standart bir Python fonksiyonu yerine __call__ metodu olan bir nesne de kullanılabilir. Kodun çalışma şekli aynıdır!
class Polinom2:
def __init__(self, a, b, c):
self.a, self.b, self.c = a, b, c
def __call__(self, t):
return self.a*t**2 + self.b*t + self.c
f = Polinom2(1, 0, 1)
dfdt = Merkezi4(f)
t = 2
print("f’({:g}) = {:g}".format(t, dfdt(t)))
Görüldüğü gibi $Polinom2$ sınıfının bir olgusu olan $f$, $Polinom2$ sınıfının __call__ metodunun varlığı sayesinde bir fonksiyon gibi $Merkezi4$ sınıfına argüman olarak geçirilebilmiştir. Normalde fonksiyonlar sabit isimlere sahip birer yapıyken burada istenildiği şekilde ismi değiştirilip yine de aynı fonksiyona erişilebildi. Dinamik bağlantı kavramıyla kastedilen işlev budur ve bu, Python programlama dilinde son derece doğal bir şekilde, çoğu zaman farkına bile varmadan kullanılan bir işlevdir. Aşağıdaki örnek bu kavramı çok iyi anlatmaktadır. Zira $f$ aslında fonksiyonun adı değilken fonksiyon da bir nesne olduğu için ve $f$ herhangi bir nesnenin adı olabildiği için $fonk1$ (ya da $fonk2$) fonksiyonunun adının yerine kolaylıkla geçebilmektedir. $f$ denince artık bir fonksiyondan bahsedilmektedir!
fonk1 = Polinom2(1, -1, 1)
fonk2 = Polinom2(1, 0, -1)
girdi = fonk1
if girdi == fonk1:
f = fonk1
elif girdi == fonk2:
f = fonk2
Sınıflara Giriş dersinde verilen bir örnekte $Dogru$ sınıfının bir alt sınıfı olarak kodlanan $Parabol$ sınıfı, $Dogru$ üst sınıfının (superclass) işlevselliğini genişletmektedir. Miras alma (inheritance) sadece bir sınıfın işlevselliğini genişletmek için değil, kısıtlamak için de kullanılabilir.
class Parabol:
def __init__(self, c0, c1, c2):
self.c0 = c0
self.c1 = c1
Self.c2 = c2
def __call__(self, x):
return self.c0 + self.c1*x + self.c2*x**2
def tablo(self, L, R, n):
print(L, R, n)
şeklinde tanımlanmış bir sınıf olsun. $Dogru$ sınıfı, $Parabol$ sınıfının bir alt sınıfı olarak tanımlanabilir ve bu şekilde onun işlevselliği de kısıtlanabilir.
class Dogru(Parabol):
def __init__(self, c0, c1):
Parabol.__init__(self, c0, c1, 0)
__call__ ve $tablo$ metotları $Parabol$'den miras alınarak aynı şekilde kullanılabilir. Görüldüğü üzere sınıfları hiyerarşik bir şekilde ilişkilendirmenin birden fazla yolu vardır. Bir $Dogru$ sınıfı ile başlayıp $Parabol$, Kübik polinom ve giderek daha genel bir polinom şeklinde $Dogru$ sınıfının işlevselliğini genişletmek yerine, genel bir Polinom sınıfı ile başlanıp, tersi yönde Prabol sınıfı onun ilk üç katsayısı hariç tüm katsayılarının sıfır olduğu bir alt sınıfı, Dogru da Parabol sınıfının bir katsayı daha sıfır yapılarak bir alt sınıfı olarak tanımlanabilir ve hiyerarşi bu şekilde de kurulabilir.
Sınıf hiyerarşisine dayanan bir kodlama uygulamasının ne kadar güçlü olabileceğni göstermek üzere nümerik türev hesabı yapan programı, hesapladoğı nümerik türevi gerçek değeri ile (hesaplanabildiğinde) karşılaştıran ve aradaki farkı hata değeri olarak ekrana getiren bir şekilde geliştirmeye çalışalım. Yapılması gereken başlatıcı (__init__) metoduna bir öz nitelik ve üst sınıfa hata hesabı yapan bir metod daha eklemek. Bu kod Turev sınıfına eklenebileceği gibi onun bir alt sınıfı olan $Turev2$ sınıfına da eklenebilir, farklı nümerik türev formüllerinin bu sınıftan kod miras almasını sağlanabilir.
class Turev2(Turev):
def __init__(self, f, h=1E-9, dfdx_tam=None):
Turev.__init__(self, f, h)
self.tam = dfdx_tam
def hata(self, x):
if self.tam is not None:
df_numerik = self(x)
df_tam = self.tam(x)
return df_tam - df_numerik
class Ileri1(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (f(x+h) - f(x))/h
Türevi farklı formüllerle alan diğer tüm formüllerin tanımlandığı sınıflar da tıpkı yukarıdaki örnek kod parçasında $Ileri1$ için olduğu gibi $Turev2$ sınıfından türetilmelidirler ki türevin tam değeri ile formüllere dayalı olarak hesaplanan nümerik değerleri arasındaki fark üzerinden hepsi için birer hata hesabı mümkün olsun.
from math import *
mycos = Ileri1(sin, dfdx_tam=cos)
print('Numerik turevin hatasi ', mycos.hata(x=pi))
Bu kodun daha özenli bir testi için farklı türev formüllerinin hangi duyarlılıkta türevi hesapladığını (ne kadar hata verdiğini) ortaya koyan bir test bloğu yazalım. Bu test bloğunun çıktısı farklı $h$ değerleri için farklı türev yöntemlerinin herhangi bir fonksiyon için hesapladığı nümerik türev değerlerini ve hatalarını içeren bir tablo şeklinde olsun.
Sonuç olarak tüm yöntemler ve bu testleri içeren bir kod örneği aşağıdaki şekilde olmalıdır.
class Turev:
def __init__(self, f, h=1E-9):
self.f = f
self.h = float(h)
class Turev2(Turev):
def __init__(self, f, h=1E-9, dfdx_tam=None):
Turev.__init__(self, f, h)
self.tam = dfdx_tam
def hata(self, x):
if self.tam is not None:
df_numerik = self(x)
df_tam = self.tam(x)
return df_tam - df_numerik
class Ileri1(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (f(x+h) - f(x))/h
class Geri1(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (f(x) - f(x-h))/h
class Merkezi2(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (f(x+h) - f(x-h))/(2*h)
class Merkezi4(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (4./3)*(f(x+h) - f(x-h)) /(2*h) - \
(1./3)*(f(x+2*h) - f(x-2*h))/(4*h)
class Merkezi6(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (3./2) *(f(x+h) - f(x-h)) /(2*h) - \
(3./5) *(f(x+2*h) - f(x-2*h))/(4*h) + \
(1./10)*(f(x+3*h) - f(x-3*h))/(6*h)
class Ileri3(Turev2):
def __call__(self, x):
f, h = self.f, self.h
return (-(1./6)*f(x+2*h) + f(x+h) - 0.5*f(x) - \
(1./3)*f(x-h))/h
def _test1():
from math import cos,pi,sin
mycos = Ileri1(sin, dfdx_tam=cos)
print('Numerik turevin hatasi', mycos.hata(x=pi))
def tablo(f, x, h_degerleri, yontemler, dfdx=None):
# Tablo basligi yaz (h ve her metod icin sinif adi
print(' h ', end='')
for yontem in yontemler:
print('{:>15s}'.format(yontem.__name__), end='')
print("\n") # yeni satira gec
for h in h_degerleri:
print('{:10.2E}'.format(h), end='')
for yontem in yontemler:
if dfdx is not None: # eger turevin tam degeri varsa hata hesapla
d = yontem(f, h, dfdx)
cikti = d.hata(x)
else: # degeri yaz
d = yontem(f, h)
cikti = d(x)
print('{:15.8E}'.format(cikti), end='')
print("\n") # yeni satir
def _test2():
from math import exp
def f1(x):
return exp(-10*x)
def df1dx(x):
return -10*exp(-10*x)
tablo(f1, 0, [2**(-k) for k in range(10)], \
[Ileri1, Merkezi2, Merkezi4], df1dx)
if __name__ == '__main__':
_test2()
Akla şöyle bir soru gelebilir: "Sınıf hiyerarşisiden faydalanmaksızın aynı problemi sadece fonksiyonlardan yararlanarak da çözmek mümkün müdür?" Cevap: “neredeyse evet!” 'tir. Sınıf yapısından olduğu gibi $f$ ve $h$'ı bir başltaıcı ile başlatıp, $x$'i türev hesabında çağırmak yerine bu kez yazılacak her fonksiyona $f$, $x$ ve $h$ birer argüman yapılmalıdır.
def tureval(f, yontem, h=1.0E-9):
h = float(h) # tam sayi bolmesinden kacmak icin float donusumu
if yontem == 'Ileri1':
def Ileri1(x):
return (f(x+h) - f(x)) / h
return Ileri1
elif yontem == 'Geri1':
def Geri1(x):
return (f(x) - f(x-h)) / h
return Geri1
Bu durumda program aşağıdaki gibi çalışır:
from math import cos,pi,sin
mycos = tureval(sin, 'Ileri1')
mysin = tureval(mycos, 'Ileri1')
x = pi
print(mycos(x), cos(x), mysin(x), -sin(x))
Nümreik türev almanın pek çok yöntemi (ve dolayısı ile pek çok formülü) olduğu gibi nümerik integrasyonun da pek çok yöntemi bulunmaktadır. Sınıf hiyerarşisine bir dayalı nesne yönelimli programlama çözümü bu yöntemlere de tıpkı nümerik türev problemine uygulandığı gibi uygulanabilir.
Tüm nümerik integrasyon yöntemleri $\omega_i$ ağırlıkları; $x_i$ integralin alındığı aralıktaki noktaları göstermek üzere yandaki formülle uygulanabilir.
$$ \int_{a}^{b} f(x) dx \approx \sum_{i = 0}^{n-1} \omega_i f(x_i) $$Tüm bu formülleri Python'a adapte edilmek istendiğinde $x_i$, $w_i$ değerlerini birer NumPy
dizisine toplayıp, $f(x)$'i de vektörleştirilmiş (NumPy dizileri üzerinde işlem yapabilen) bir fonksiyon olarak $f(xi)$ şeklinde kodlamanın iyi bir çözüm olduğu düşünülmelidir. Yine her bir formülü aşağıdaki yapıda bir sınıfın içerisine adapte etmek de mümkündür.
class BirIntegralYontemi:
def __init__(self, a, b, n):
self.a, self.b, self.n = a, b, n
# [a,b] araliginda n nokta ve agirliklari hesapla
def integral(self, f):
s = 0
for i in range(len(self.agirliklar)):
s += self.agirliklar[i]*f(self.noktalar[i])
return s
Sınıf yapısı bu şekilde tasarlandığında integral metodunun her bir integral yöntemi için ortak olacağını görmek son derece kolaydır. Dolayısıyla bu metod, üst sınıfın bir kodu olmalı ve her bir yöntem için yazılacak alt sınıflar tarafından kalıt alınabilmelidir. O nedenle bu metodu herhangi bir integral yöntemi için yazılacak üstteki gibi bir sınıftan çıkarıp bir üst sınıfa (superclass) almak iyi bir çözümdür.
class Integral:
def __init__(self, a, b, n):
self.a, self.b, self.n = a, b, n
self.noktalar, self.agirliklar = self.metot_olustur()
def metot_olustur(self):
raise NotImplementedError('%s sinifinda boyle bir kural yok' % \
self.__class__.__name__)
def integralal(self, f):
s = 0
for i in range(len(self.agirliklar)):
s += float(self.agirliklar[i])*float(f(self.noktalar[i]))
return s
Görüldüğü gibi __init__ başlatıcı metodu (constructor) $a$, $b$ ve $n$ özniteliklerini başlatıyor. Şimdi tüm bu alt sınıflar bu kodu kalıt (inheritance) alabilir. $x_i$ noktaları ve $\omega_i$ ağırlıklarını birer dizi (ya da liste) olarak hesap etme işi metot_olustur metodunda gerçekleştiriliyor. Ancak bu liste ve dizinin içeriğini her bir integrasyon yöntemi ayrı bir formülle, ilgili alt sınıfta dolduracağı için bu işin gerçekleşmediği durumda $NotImplementedError$ (bu metot adapte edilmedi hatası) üretiliyor. Bu metot_olustur metodu ne zamanki herhangi bir integrasyon yöntemi bir alt sınıfta bir nümerik integral alma yönteminin formülünü adapte ediliyor, onun tarafından devre dışı bırakılmış oluyor. Bu şekilde metot_olustur metodunu herhangi bir nümerik integral formülü ile integrali hesaplayan herhangi bir alt sınıfta yazma işlemi unutulursa bu yöntemin adapte edilmediğine dair hata mesajı başlatıcı metot içerisinde üretilmiş olunur. Hata aynı zamanda yöntemin tanımlandığı alt sınıfın adı da verildiği için hangi yöntemin adapte edilemediğini göstermesi bakımından da akıllı bir şekilde yaratılmaktadır.
Bir metodun bu şekilde kod içerisinde tekrar oluşturulması ya da duruma göre daha önce aynı isimle oluşturulmuş bir metodun yerini alması bilgisayar biliminde polimorfizm olarak adlandırılır. Bu şekilde kodlanan metotlara da polimorfik metotlar denir. Genel pratik, tıpkı yukarıdaki örnekte olduğu gibi, polimorfik bir metodu bir üst sınıf içerisinde oluşturup, duruma göre bir alt sınıfta yeniden tanımlamaktır ("overloading").
$integralal$ metodundaki kod, herhangi bir yöntemle integral alacak tüm alt sınıflar tarafından kalıt alınabilir. Bu kod NumPy
dizilerinin yanı sıra herhangi bir $x - \omega$ türü (liste, demet) için de çalışabilir. Ama bu kodun üst sınıfa NumPy
'ın $dot$ fonksiyonundan yararlanmak üzere bir de NumPy dizileri ile çalışan versiyonunu yazmak gerekecektir.
def vektor_ntegralal(self, f):
return dot(self.agirliklar,f(self.noktalar[i]))
Şimdi bir de herhangi bir integrasyon yöntemi (orta nokta yöntemi) ile nümerik integral hesaplayan bir alt sınıfı kodlanabilir. Yapılması gereken yöntemin formülünü metot_olustur() metoduna adapte etmek.
class OrtaNokta(Integral):
def metot_olustur(self):
from numpy import linspace, zeros
a, b, n = self.a, self.b, self.n # kisa yazim icin tekrar donusum
h = (b-a)/float(n)
x = linspace(a + 0.5*h, b - 0.5*h, n)
w = zeros(len(x)) + h
return x, w
Görüldüğü gibi bu metot işlemlerini vektörize bir şekilde NumPy
dizleri üzerinden gerçekleştiriyor ve sonuçta da bir NumPy
dizisi döndürüyor. Diğer yöntemleri de adapte etmeden önce kodun nasıl çalışacağını görmek için bir _test() bloğu yazmak iyi bir fikirdir.
def _test():
def f(x): return x*x
on = OrtaNokta(0, 2, 101)
print(on.integralal(f))
Kod gördüğünüz gibi önce $OrtaNokta$ sınıfından $on$ adında bir olgu oluşturuyor. $OrtaNokta$ sınıfıının başlatıcı (__init__) metodu olmadığı için bu metot üst sınıf olan $Integral$ 'den kalıt alınıyor. Ancak buradaki $self$, $on$ olgusuna karşılık geldiği, dolayısı ile $OrtaNokta$ 'nın bir olgusu olduğu için self.metot_olustur ile kastedilen $OrtaNokta$ sınıfındaki metot_olustur metodudur. Aynı isimle üst sınıf olan $Integral$ 'de de bir sınıf bulunuyor olsa da çalışan bu $OrtaNokta$ metodundaki metot_olustur metodu olur. En sonda çalıştırılan (çağrılan) $integralal$ metodu ise üst sınıfta yer almaktadır ve bütün alt sınıflar tarafından kalıt alınabileceği için bir alt sınıf olan $OrtaNokta$'nın $on$ olgusu tarafından da kalıt alınmıştır ve integrali hesaplar.
Yamuk yöntemi (trapezoid) için de aynı şekilde vektorize bir alt sınıf aşağıdaki şekilde kodlanabilir.
class YamukYontemi(Integral):
def metot_olustur(self):
from numpy import linspace, zeros
x = linspace(self.a, self.b, self.n)
h = (self.b-self.a)/float(self.n - 1)
w = zeros(len(x)) + h
w[0] /= 2
w[-1] /= 2
return x, w
Simpson yönteminin adaptasyonu biraz daha uzun bir kod parçası gerektirir zira formülü biraz daha komplikedir.
class Simpson(Integral):
def metot_olustur(self):
from numpy import linspace, zeros
if self.n % 2 != 1:
print('n={:d} tek olmak zorundadir, olmayinca 1 eklenir'.format(self.n))
self.n += 1
x = linspace(self.a, self.b, self.n)
h = (self.b-self.a)/float(self.n - 1)*2
w = zeros(len(x)) + h
w[0:self.n:2] = h*1./3
w[1:self.n-1:2] = h*2./3
w[0] /= 2
w[-1] /= 2
return x, w
Gauss-Legendre yöntemi de koda aynı şekilde adapte edilebilir. Bu kez dilimler kullanmak yerine biraz daha komplike bir iş olduğu için sıradan bir $for$ döngüsü tercih edilebilir. Ama aynı şeyi yine de dilimlerle yapmak olasıdır. (deneyiniz!).
class GaussLegendre(Integral):
def metot_olustur(self):
from numpy import linspace, zeros, sqrt
if self.n % 2 != 0:
print('n={:d} cift olmak zorundadir, olmayinca 1 cikarilir' .format(self.n))
self.n -= 1
naralik = int(self.n/2.0)
x = zeros(self.n)
h = (self.b-self.a)/float(naralik)
sqrt3 = 1./sqrt(3)
for i in range(naralik):
x[2*i] = self.a + (i+0.5)*h - 0.5*sqrt3*h
x[2*i+1] = self.a + (i+0.5)*h + 0.5*sqrt3*h
w = zeros(len(x)) + h/2.0
return x, w
Artık kod çalıştırılarak, farklı nümerik integrasyon tekniklerinin nasıl sonuç verdiğini görülebilir.
$x + 2$ fonksiyonunun $ a = 2 $ ile $ b = 3 $ arasında 4 nokta üzerinden Orta Nokta, Yamuk, Simpson ve Gauss Legendre yöntemleriyle alınan integralleri aşağıdaki şekilde bir fonksiyona kodlanıp, bu fonksiyon çalıştırılarak tüm sınıfları test etmek mümkündür.
def _test2():
def f(x): return x + 2
a = 2; b = 3; n = 4
for yontem in OrtaNokta, YamukYontemi, Simpson, GaussLegendre:
y = yontem(a, b, n)
print(y.__class__.__name__, y.integralal(f))
_test2()
Diferansiyel denklemleri temelde ikiye ayırmak mümkündür:
1) Skaler adi diferansiyel denklemler (tek bir diferansiyel denklem içerenler)
$$ \frac{du}{dt} = f(u,t) , u(0) = u_0 $$2) Adi diferansiyel denklem sistemleri
$$ \frac{du^{i}}{dt} = f(u^{(0)}, u^{(1)}, u^{(2)}, ..., u^{(n-1)}, t) $$Başlangıç koşulları: $u^{(i)}(0) = u_0^{(i)}, i = 0, 1, 2, ..., n-1$
$u^0$, $u^1$, …, $u^{(n-1)}$ fonksiyonlarını bir vektörde, başlangıç koşullarını $u_0 = (u_0^{(0)}, u_0^{(1)}, … , u_0^{(n-1)})$ başka bir vektörde toplamak iyi bir fikirdir. Bu iki vektörü bir adi diferansiyle denklem sistemi için NumPy dizileri ile tanımlanabileceği için aslında yazılması istenen kod hem tek denklem içeren skaler denklemler, hem de denklem sistemleri için kullanılabilir.
Çözülmesi istenen diferansiyel denklem sistemlerine bir örnek bir yayın ucuna asılı kütle ile oluşturulan bir sistemi tanımlayan denklemlerdir.
$$ F(t) = m u^{{\prime}{\prime}} + \beta u^{\prime} + k u, u(0) = u_0, u^{\prime}(0) = 0 $$$u^{(0)}(t) = u(t), u^{(1)}(t) = u^{\prime}(t)$ olmak üzere bu ikinci derece denklemin çözümü birinci dereceden iki fonksiyonla verilebilir. Buradaki bilinmeyenler $u^{(0)}(t) = u(t)$ (konum) ve $u^{(1)}(t) = u^{\prime}(t)$ (hız) 'dır.
Bu iki bilinmeyen aşağıdaki şekilde ifade edilebilir.
$$ \frac{d}{dt} u^{(0)} (t) = u^{(1)}(t),$$$$ \frac{d}{dt} u^{(1)} (t) = m^{-1} (F(t) - \beta u^{(1)} - k u^{(0)}$$Bu tür sistemleri $u^{\prime}(t) = f(u,t)$ şeklinde ifade etmek yaygın bir pratiktir. Bu durumda $u$ bir vektör olduğu için, $f$ de bir vektör olur.
$$ u(t) = (u^{(0)} (t) , u^{(1)}(t)),$$$$ f(t,u) = (u^{(1)}, m^{-1} (F(t) - \beta u^{(1)} - k u^{(0)}))$$Diferansiyel denklem sistemlerinin çözümü için önerilen yöntemler $u$ fonksiyonuna $t_k, (k = 1, 2, …)$ eşit aralıklı ($t_k = k \Delta t$) zaman dilimleri için $u_k$ yaklaştırmasını hesaplarlar.
$u_{(k+1)} = u_{k} + \Delta t f(u_k, t_k) $
$u_{(k+1)} = u_{(k-1)} + 2 \Delta t f(u_k, t_k) $
$K_1 = \Delta t f(u_k, t_k)$ ve $K_2 = \Delta t f(u_k + \frac{1}{2} K_1, t_k + \frac{1}{2} \Delta t)$ olmak üzere
$u_{(k+1)} = u_{(k)} + K_2 $
$K_3 = \Delta t f(u_k + \frac{1}{2} K_2, t_k + \frac{1}{2} \Delta t)$ ve $K_4 = \Delta t f(u_k + K_3, t_k + \Delta t)$ olmak üzere
$u_{(k+1)} = u_{(k)} + \frac{1}{6} (K_1 + 2 K_2 + 2 K_3 + K_4) $
$u_{(k+1)} = u_{k} + \Delta t f(u_{(k+1)}, t_{(k+1)}) $
Üst sınıf (superclass): Diferansiyel denklemlerin çözümü için üst sınıf olarak $DiferansiyelDenklemCozucu$ üst sınıfı seçilmiş osun. Bu sınıf diğer sınıfların ihtiyaç duyacağı tüm kodu sağlamalı, alt sınıflar bu sınıftaki kodları kalıt alabilmelidir. Bunun için
1) $t$ anı için $u(t)$ çözümünü tutmalı, 2) karşılık gelen $t$ anını tumalı, 3) $f(u,t)$ fonksiyonunu çağrılabilir bir Python nesnesi olarak tutmalı, 4) $dt$ özniteliğinde $\Delta t$ zaman aralıklarını tutmalı, 5) Hangi zaman aralığında olduğunun belirlenmesi için aralık sayısı $k$'yi $k$ isimlli bir öznitelikte tutmalı, 6) $u_0$ başlangıç koşulu başlatmalı, 7) Aralıktaki tüm zaman basamakları için bir döngü çalıştırmalıdır.
İyi bir çözüm için döngüyü kurmak üzere bir $dongu$ metodu ve çözümü formüle uygun olarak bir adım ilerletmek için de bir $ileri$ metodu kodlayacağız. Ancak bu ikinci metod şimdilik boş olacak, zira bu metodu her bir çözüm yöntemi ayrı bir formülle tekrar kuracak.
Yukarıda stratejisi verilen sınıf sinif_diferansiyeldenklem.py adlı dosyada bulunmaktadır. Bu kodu iyice inceleyiniz.
Bu sınıfı bazı diferansiyel denklemleri çözmek için test etmeye en basit diferansiyel denklem uygulamasıyla başlanabilir. Bu denklemi İleri Euler yöntemi ile çözmek üzere yazılması gereken kod temelde aşağıdaki gibi olacaktır.
from sinif_diferansiyeldenklem import *
from matplotlib import pyplot as plt
from numpy import linspace
def f(u, t):
return u
T = 3
N = 100
yontem = IleriEuler(f)
yontem.baslangic_kosulu(1.0)
u, t = yontem.cozum(linspace(0,T,N+1))
plt.plot(t,u)
plt.show()
Görülen fonksiyon $\frac{1}{2}u^2 + C$ fonksiyonunun grafiğidir ki bu diferansiyel denklemin çözümüdür.
Runge-Kutta yönteminin (4. basamak) zamandaki artışın büyük değerleri için (büyük $\Delta t$, küçük $N$) dahi ne kadar etkin bir yöntem olduğunu kolaylıkla göstermek mümkündür.
N = 10 # kucuk N buyuk adim araligi demektir!
for cozum_yontemi in IleriEuler, RungeKutta4:
yontem = cozum_yontemi(f)
yontem.baslangic_kosulu(1.0)
u, t = yontem.cozum(linspace(0,T,N+1))
plt.plot(t, u)
plt.legend('%s' % cozum_yontemi.__name__)
plt.show()
Kodu bu kez yatay atış problemini çözmek üzere kullanalım. Yatay atış problemi aşağıdaki denklemlerle tanımlanabilir. $(x,y)$ cismin sırasıyla yatay ve düşey konumunu, $g$ yer çekimi ivmesini göstermektedir.
$\frac{d^2 x}{d t^2} = 0$, $\frac{d^2 y}{d t^2} = -g$,
Bu ikinci dereceden diferansiyel denklemleri, birinci derece diferansiyel denklemlere dönüştürerek yazmak mümkündür. Ancak bu kez 4 denkleme ihtiyaç duyulur.
$\frac{dx}{dt} = v_x$, $\frac{d v_x}{dt} = 0$, $\frac{dy}{dt} = v_y$, $\frac{dy}{dv} = -g$
Başlangıç koşulları da aşağıdaki gibidir. $V_0$ cismin ilk hızı, $\theta$ ise atış sırasında yatayla yapılan açıdır.
$x(0) = 0$, $v_x(0) = v_0 cos(\theta)$, $y(0) = y_0$, $v_y(0) = v_0 sin(\theta)$
Diferansiyel denklemlerin sağ tarafını döndüren bir fonksiyon aşağıdaki gibi yazılabilir.
def f(u, t):
x, vx, y, vy = u
g = 9.81
return [vx, 0, vy, -g]
Problemi çözmek için yazılacak ana kod aşağıdaki gibi olabilir.
from math import pi
from numpy import linspace,cos,sin
from sinif_diferansiyeldenklem import *
from matplotlib import pyplot as plt
v0 = 5
theta = 80*pi/180
u0 = [0, v0*cos(theta), 0, v0*sin(theta)]
T = 1.2
N = 120 # dt = 0.01
t_noktalari = linspace(0,T,N)
yontem = IleriEuler(f)
yontem.baslangic_kosulu(u0)
u, t = yontem.cozum(t_noktalari)
Problemin çözülmesiyle her bir elemanı bir dizi $(x, vx, y, vy)$ olan 4 elemanlı bir dizi elde edilir. Örneğin zamanla $x$'in (yatay konum) nasıl değiştiğini öğrenmek istiyorsanız $x$ dizisini bu diziden çekmeniz gerekir.
x_degerleri = u[:,0]
plt.plot(t, x_degerleri)
Eğer yatay atışın grafiğini çizmek istiyorsanız cismin $t$ anı için sadece yatay konumunu ($x$) değil aynı zamanda düşey konumunu ($y$) da bilmeli ve $x$'e karşılık $y$'yi çizdirmelisiniz.
x_degerleri = u[:,0]
y_degerleri = u[:,2]
plt.plot(x_degerleri, y_degerleri)
Problemin tam (analitik) çözümü vardır ve nümerik çözümün başarısını test etmek için kolaylıkla kullanılabilir.
def tam(x):
g = 9.81; y0 = u0[2]
return x*tan(theta) - g*x**2/(2*v0**2)*1/(cos(theta))**2 + y0
plt.plot(x_degerleri, y_degerleri, "r-", label="numberik")
plt.plot(x_degerleri, tam(x_degerleri), "b-", label="tam")
plt.legend(loc="best")
plt.show()
Doğada kara cisme en benzeyen cisim yıldızlardır (yıldızların merkezi bölgeleri demek daha doğru). Bu nedenle kara cisim ışınımını tanımlayan yasalar en iyi yıldızlara uygulanır. Sıcaklığı $T$ olan bir kara cismin $\lambda$ dalgaboyunda yaptığı ışınım miktarının hesabı uzun süre astrofizikçileri meşgul etmiş, 1900 yılında Max Planck'ın modern fiziğin ve kuantum teorisinin temellerini atan önerisiyle tam bir çözüme kavuşturulabilmiştir. Öncesinde Wihelm Wien tarafından 1896 yılında kısa dalgaboylarında geçerli, Rayleigh-Jeans tarafından ise uzun dalgaboylarında geçerli iki yaklaşım önerilmiştir.
Sınıf hiyerarşisi dahlinde bu üç yöntemi kodlayarak sıcaklığı $T$ olan bir kara cismin $\lambda$ dalgaboyunda yaptığı ışınım miktarını $W sr^{-1} m^3$ biriminde hesabeden bir program aşağıda örneklenmiştir.
from decimal import *
getcontext().prec = 64
class TED:
def __init__(self, T):
self.T = Decimal(T)
self.h = Decimal(4.135667662e-15) # Planck sabiti (eV.s)
self.k = Decimal(8.6173324e-5) # Boltzmann sabiti (ev / K)
self.c = Decimal(2.99792458e8) # isik hizi (m /s)
def hesap(self):
raise NotImplementedError('%s enerji hesaplama yontemi uyarlanmadi' % \
self.__class__.__name__)
class Planck(TED):
def hesap(self, lamda):
T, h, k, c = self.T, self.h, self.k, self.c
lamda = Decimal(lamda)
return (Decimal(2.0)*h*c**2 / lamda**5)*\
(Decimal(1.) / ((h*c/(lamda*k*T)).exp()-1))
class Wien(TED):
def hesap(self, lamda):
T, h, k, c = self.T, self.h, self.k, self.c
lamda = Decimal(lamda)
return (Decimal(2.0)*h*c**2 / lamda**5)/\
((h*c/(lamda*k*T)).exp())
class RayleighJeans(TED):
def hesap(self, lamda):
T, k, c = self.T, self.k, self.c
lamda = Decimal(lamda)
return (Decimal(2.0)*c*k*T / lamda**4)
Sonrasında bu kod geniş bir dalgaboyu aralığı dahlinde enerji miktarlarını her üç yöntemle de hesap eden ve birbirleri ile bir grafik üzerinde karşılaştıran birkaç test uygulamak faydalı olacaktır.
def _test():
T = Decimal(5000) # K (gunes)
lamda = Decimal(5.5e-7) # lamda (gorsel dalgaboyu merkezi [m])
p = Planck(T)
w = Wien(T)
rj = RayleighJeans(T)
# ev s sr-1 m-3 ==> kW sr-1 m-2 nm-2
dks = Decimal(1.60217662e-31)
print(p.__class__.__name__+' '+str(p.hesap(lamda)*dks))
print(w.__class__.__name__+' '+str(w.hesap(lamda)*dks))
print(rj.__class__.__name__+' '+str(rj.hesap(lamda)*dks))
_test()
def _test2():
T = 5780
lamda1 = 3e-7
lamda2 = 8e-7
n = 100 # integral icin nokta sayisi
p = Planck(5780)
gl = GaussLegendre(lamda1,lamda2,n)
print("T={:g} olan bir karacismin {:g} ile {:g} arasinda yaydigi toplam enerji {:g} kW sr-1 m-2 nm-2".
format(T, lamda1, lamda2, gl.integralal(p.hesap)))
_test2()
1.a. Sınıfınızın sıcaklk ($T$), yarıçap ($R$) ve uzaklık ($d$) parametrelerinin yanı sıra bir de gerekli sabitleri (Güneş'in ilgili parametreleri gibi) başatan __init__ başlatıcı metodu olmalıdır.
1.b. Yıldızın toplam ışınım gücünü (lüminosite) hesap eden bir $L$ metodu olmalıdır.
1.c. Yıldızın mutlak bolometrik parlaklığını hesaplayan $Mbol$ metodu olmalıdır.
1.d. Uzaklık modülünü hesaplayan uzaklik_modulu metodu olmalıdır.
1.e. Yıldızın görünen bolometrik parlaklığını hesaplayan $mbol$ metodu olmalıdır.
1.f. Yıldızın görünen görsel parlaklığını hesaplayan $mV$ metodu olmalıdır.
1.g. Yıldızdan Dünya'ya ulaşan akıyı hesaplayan gorunen_aki metodu olmalıdır.
1.h. Yıldızın maksimum ışınım yaptığı dalgaboyunu hesaplayan wien_kayma_yasasi metodu olmalıdır.
1.i. Sınıfın ve fonksiyonlarının ne yaptığnı, hangi öznitelik ve metotları içerdiğini anlatan bir iç dokümantasyonu olmalıdır.
1.j. Sınıfınızın hangi parametreleri alıp, sonuçta hangi parametreleri hesapladığını anlatan bir __str__ metodu olmalıdır.
Yazdığınız sınıfı test etmek üzere bir _test() fonskiyonu yazınız (aynı kodun içine) ve sınıfınızı Dschubba (T = 28000 K, R = 5.16 x 1011 m, d = 180 pc) ve Güneş yıldızları için test ediniz.
Gerekli sabitler: $\sigma = 5.670373 \times 10^{-5} erg cm^{-2} s^{-1} K^{-4}$, $M_{V, \odot} = 4^m.83$, $M_{bol, \odot} = 4^m.75$, $L_{\odot} = 3.826 \times 10^{33} erg / s$
Dalgalar fiziğinin en önemli filkirlerinden biri her kompleks dalga formunun kosinüs ve sinüs fonksiyonlarının harmoniklerinin toplamı olarak ifade edilebilmesidir.
$$ f (x) = a_0 + a_1 cos x + a_2 cos 2x + a_3 cos 3x + a_4 cos 4x + …. + b_1 sin x + b_2 sin 2x + b_3 sin 3x + b_4 sin 4x + … $
Bu şekilde oluşturulan serilere Fourier serileri adı verilir.
$FourierSerisi$ adında ve aşağıdaki özellikleri sağllayan bir sınıf kodlayanız.
Sınıfınızın $a = [a_0, a_1, a_2, a_3, …, a_{n-1}]$ ve $b = [b_0, b_1, b_2, b_3, …, b_{n-1}]$ harmonik terimlerinin katsayılarını birer numpy
dizisi olarak başlatan bir __init__(self, a, b, n) metodu bulunmalıdır (n: terim sayısı).
Sınıfınızın herhangi bir $x$ için serinin değerini hesaplayan bir __call__(self, x) metodu olmaldır.
Sınıfınızın verilen katsayilar için oluşan Fourier serisini ekrana güzel bir şekilde yazdıran bir __str__(self) metodu olmaldır.
Test: Bu kodu kullanarak aşağıdaki dalga formunu oluşturan bir _test() fonksiyonu yazınız. $\psi = \frac{2}{(n +1)} (sin x – sin 3x + sin 5x – sin 7x + … \pm sin nx) = \frac{2}{(n + 1)}, \Sigma_n^{k=1} (-1)^{(k-1) / 2} sin kx$; $k$ : tek tam sayı. Baştaki 2 / (n + 1) terimi dalga formunun şeklini değiştirmeyecek sadece maksimum değeri 1 olacak şekilde ölçeklendirecektir.
$x = [0, \pi]$ aralığında (radyan cinsinden) $n = 5$ nokta için $\psi$ dalga formunun grafiğini çiziniz. Daha sonra $n$'i sırasıyla 11, 21 ve 41'e çıkararak grafik çizimini tekrarlaynız ve dalga formunun nasıl değiştiğini gözleyiniz.