5. Állandó fázisú pont módszere, SPPMethod

Ez a módszer alapjaiban kissé különbözik a többitől. Az előzőleg leírt globális metódusok, mint domain átváltás, kivágás, stb. itt is működnek, de másképpen kell kezelni őket. Megjegyezném, hogy mivel ez a módszer interaktív elemet tartalmaz, az egyelőre csak Jupyter Notebook-ban stabil.

[1]:
import numpy as np
import matplotlib.pyplot as plt
import pysprint as ps

Példaként a korábban már bemutatott ps.Generator segítségével generálni fogok egy sorozat interferogramot, majd azon bemutatom a kiértékelés menetét. Valós méréseknél teljesen hasonlóképpen végezhető a kiértékelés. A legegyszerűbb módszer, hogy különböző karok közti időbeli késleltetésnél generáljunk és elmentsük azokat az alábbi cellában látható. A megkülönböztethetőség miatt minden fájlt a hozzá tartozó karok közti időbeli késleltetésnek megfelelően nevezem el.

[9]:
for delay in range(-200, 201, 50):
    g = ps.Generator(1, 3, 2, delay, GDD=400, TOD=-500, normalize=True)
    g.generate_freq()
    np.savetxt(f'{delay}.txt', np.transpose([g.x, g.y]), delimiter=',')

A kód lefuttatásával a munkafüzet környtárában megjelent 7 új txt fájl.

Ehhez a kiértékelési módszerhez először fel kell építeni egy listát a felhasználandó interferogramok fájlneveivel. Ezt manuálisan is megtehetjük, itt ezt elkerülve egy rövidítést fogok használni.

[10]:
ifg_files = [f"{delay}.txt" for delay in range(-200, 201, 50)]
[11]:
print(ifg_files)
['-200.txt', '-150.txt', '-100.txt', '-50.txt', '0.txt', '50.txt', '100.txt', '150.txt', '200.txt']

Ha nem hasonló sémára épülnek a felhasználandó fájlok nevei, akkor természetesen a fenti trükk nem működik és egyenként kell beírnunk őket. Miután definiáltuk a fájlneveket a következő lépés a

ps.SPPMethod(ifg_names, sam_names=None, ref_names=None, **kwargs)

meghívása:

[12]:
myspp = ps.SPPMethod(ifg_files, decimal=".", sep=",", skiprows=0, meta_len=0)

A **kwargs keyword argumentumok itt elfogadják a korábban már bemutatott parse_raw funkció argumentumait (a kódban belül azt is hívja meg egyesével minden interferogramon), hiszen a fájlok sémáját itt is fontos megadni a helyes betöltéshez. A tárgy- és referencianyaláb spektrumai természetesen opcionális argumentumok, mi dönthetjük el, hogy normáljuk-e az interferogramokat.

Az SPPMethod objektum először ellenőrzi, hogy a listában lévő fájlnevek valóban léteznek-e, és ha nem, akkor hibával tér vissza. Az SPPMethod-nak vannak további metódusai, ilyen pl. a len(..), vagy az SPPMethod.info. Az első visszaadja, hogy hány interferogram van jelenleg az objektumban (ez jelen esetben 9), a második pedig a kiértékelés során mutatja majd, hogy hány interferogramból rögzítettünk információt (ez jelenleg 0/9). Később talán append (ilyen már van a 0.12.5 verziótól), insert és delete metódusokat is beépítek.

[13]:
print(len(myspp))
9
[14]:
print(myspp.info)
Progress: 0/9

Az SPPMethod objektum listaszerűen viselkedik: lehet indexelni is. Mivel benne 9 darab interferogram van, ezért egy ilyen indexelés egy ps.Dataset objektumot ad vissza. Ez az alapja minden kiértékelési módszernek, így ez ismeri a korábban bemutatott metódusokat. Tegyük fel, hogy a 3. interferogram adatait ki szeretnénk iratni, és szeretnénk megkapni az y értékeit np.ndarray-ként. Ekkor a 2 indexet használva (mivel itt is 0-tól indul a számozás):

[15]:
# a harmadik interferogram adatainak kiíratása
print(myspp[2])
Dataset
----------
Parameters
----------
Datapoints: 12559
Predicted domain: frequency
Range: from 0.99998 to 3.00000 PHz
Normalized: False
Delay value: Not given
SPP position(s): Not given
----------------------------
Metadata extracted from file
----------------------------
{}
[16]:
# a harmadik interferogram y értékeinek kinyerése, mint np.array
y_ertekek = myspp[2].data.y.values
print(y_ertekek)
print(type(y_ertekek))
[4.22567237e-08 4.23424876e-08 4.24102757e-08 ... 4.41394357e-08
 4.30227521e-08 4.18888151e-08]
<class 'numpy.ndarray'>

Újra hangsúlyozom, minden eddig bemutatott metódus ezeken a kvázi listaelemeken is működik, köztük a chdomain, vagy slice is. Ezt használjuk ki a kiértékeléshez egy for ciklusban. A kiértékeléshez a definiált SPPMethod-on meg kell hívni egy for ciklust. Ez végigfut a benne lévő összes interferogramon. Azt, hogy mit akarunk csinálni adott interferogrammal, azt a cikluson belül tudjuk megadni. Az alapvető séma a következő:

for ifg in myspp:
    - előfeldolgozása az adott interferogramnak
    - az interaktív SPP Panel megnyitása és adatok rögzítése

- a calculate metódus meghívása a cikluson kívül(!)

Ez kód formájában az alábbi cellában látható. Itt külön jelöltem, hogy melyik rész meddig tart.

[17]:
# az interaktív számításokat fontos a with blokkon belülre írni

with ps.interactive():

    for ifg in myspp:

        # -----------------------------------Előfeldolgozás-----------------------------------------
        # Ha valós mérésünk van, érdemes valamilyen módon kiíratni a kommentet,
        # ami az interferogram fájlban van, hogy meg tudjuk állapítani milyen késleltetésnél készült.
        # Jelen esetben ennek nincs értelme, mivel a szimulált fájlokkal dolgozom.
        # Ezt legegyszerűbben az alábbi sorral tehetnénk meg:
        # print(ifg.meta['comment'])
        # vagy esetleg a teljes metaadatok kiíratása:
        # print(ifg.meta)

        # Ha hullámhossztartományban vagyunk, először át kell váltani.
        # Én frekvenciatartományban szimuláltam, ezért itt kihagyom. Ha szükség van rá a
        # következő sort kell használni.
        # ifg.chdomain()

        # Pl. 1.2 PHz alatti körfrekvenciaértékek kivágása. Mivel nem adtam meg stop értéket, így a felső
        # határt érintetlenül hagyná, ha futtatnám. Nyilván ez is opcionális.
        # ifg.slice(start=1.2)

        # -----------------------------Az interaktív panel megnyitása-------------------------------
        ifg.open_SPP_panel()

# ---------------------------------A ciklus utáni rész------------------------------------------
# A cikluson kívül a save_data metódus meghívása, hogy elmentsük a beírt adatainkat fájlba is.
# Ez természetesen opcionális, de annak érdekében, hogy biztosan ne veszítsünk adatot érdemes ezt is elvégezni.
myspp.save_data('spp.txt')


# a cikluson kívül meghívjuk a calculate függvényt
myspp.calculate(reference_point=2, order=3);
$\displaystyle GD = -0.60975 ± 0.00000 fs^1$
$\displaystyle GDD = -391.93908 ± 20.48631 fs^2$
$\displaystyle TOD = 277.28636 ± 226.70828 fs^3$

A magyarázatok nélkül szimulált esetben az egész kód az alábbi, összesen 8 sorra egyszerűsödik. Valós mérés esetén néhány előfeldolgozási lépés és kiíratás természetesen még hozzáadódhat ehhez.

import pysprint as ps

ifg_files = [f"{delay}.txt" for delay in range(-200, 201, 50)]

s = ps.SPPMethod(ifg_files, decimal=".", sep=",", skiprows=0, meta_len=0)

with ps.interactive():
    for ifg in s:
        ifg.open_SPP_panel()

s.save_data('spp.txt')
s.calculate(reference_point=2, order=2, show_graph=True)

Miután a számolást már elvégezte a program, akkor elérhetővé válik rajta a GD property. Ez az illesztett görbét reprezentálja, típusa ps.core.phase.Phase. Bővebben erről a Phase leírásában.

[18]:
myspp.GD
[18]:
<pysprint.core.phase.Phase at 0x2d2223e1a48>

5.1 Számolás nyers adatokból

Mivel az spp.txt fájlba elmentettük az bevitt adatokat, azokból egyszerűen lehet újraszámolni az illesztést. Töltsük be np.loadtxt segítségével, majd használjuk a ps.SPPMethod.calculate_from_raw függvényt.

[19]:
delay, position = np.loadtxt('spp.txt', delimiter=',', unpack=True)

myspp.calculate_from_raw(delay, position, reference_point=2, order=3);
$\displaystyle GD = -0.60975 ± 0.00000 fs^1$
$\displaystyle GDD = -391.93908 ± 20.48631 fs^2$
$\displaystyle TOD = 277.28636 ± 226.70828 fs^3$

Az előbbi esetben látható, hogy ugyan azt az eredményt kaptuk, mint előzőleg. Ez akkor is hasznos lehet, ha már megvannak a leolvasott SPP pozícióink a hozzá tartozó késleltetésekkel és csak a számolást akarjuk elvégezni. Ekkor még létre sem kell hozni egy új objektumot, csak meghívhatjuk a függvényt következő módon:

[20]:
# ehhez beírtam egy teljesen véletlenszerű adatsort
delay_minta = [-100, 200, 500, 700, 900]
position_minta = [2, 2.1, 2.3, 2.45, 2.6]

ps.SPPMethod.calculate_from_raw(delay_minta, position_minta, reference_point=2, order=3);
$\displaystyle GD = 1.99335 ± 0.00000 fs^1$
$\displaystyle GDD = 0.00038 ± 0.00006 fs^2$
$\displaystyle TOD = 0.00000 ± 0.00000 fs^3$

FONTOS MEGJEGYZÉS:

Az order argumentum a program során mindig a keresett diszperzió rendjét adja meg.

5.2 Számolás egy további módon

Mivel továbbra is ugyan ezekkel az adatsorokkal és a myspp objektummal dolgozom, most törlöm az összes rögzített adatot belőlük. Ehhez a SPPMethod.flush függvényt használom. (Valószínűleg ez a felhasználónak kevésszer szükséges, de elérhető.)

[21]:
myspp.flush()

Korábban már észrevehettük, hogy a kiíratás során - legyen bármilyen módszerről is szó - megjelentek olyan sorok is, hogy Delay value: Not given és SPP position(s): Not given. Például a myspp első interferogramja esetén most ez a helyzet:

[22]:
print(myspp[0])
Dataset
----------
Parameters
----------
Datapoints: 12559
Predicted domain: frequency
Range: from 0.99998 to 3.00000 PHz
Normalized: False
Delay value: Not given
SPP position(s): Not given
----------------------------
Metadata extracted from file
----------------------------
{}

Ahogyan a Dataset leírásában már szerepelt, lehetőségünk van megadni a betöltött interferogramokon az SPP módszerhez szükséges adatokat. Ekkor a ps.SPPMethod.calculate_from_ifg(ifgs, reference_point, order) függvénnyel kiértékelhetjük a benne lévő interferogramokat a következő módon:

[23]:
# kicsomagolok öt interferogramot a generált 7 közül

elso_ifg = myspp[0]
masodik_ifg = myspp[1]
harmadik_ifg = myspp[2]
negyedik_ifg = myspp[3]
otodik_ifg = myspp[4]
[24]:
# beállítok rájuk véletlenszerűen SPP adatokat

elso_ifg.delay = 0
elso_ifg.positions = 2

masodik_ifg.delay = 100
masodik_ifg.positions = 2

harmadik_ifg.delay = 150
harmadik_ifg.positions = 1.6

negyedik_ifg.delay = 200
negyedik_ifg.positions = 1.2

otodik_ifg.delay = 250
otodik_ifg.positions = 1, 3, 1.2


# listába teszem őket
ifgs = [elso_ifg, masodik_ifg, harmadik_ifg, negyedik_ifg, otodik_ifg]
[25]:
# meghívom a calculate_from_ifg függvényt
ps.SPPMethod.calculate_from_ifg(ifgs, reference_point=2, order=3);
$\displaystyle GD = 71.99708 ± 0.00000 fs^1$
$\displaystyle GDD = -19.90037 ± 30.07946 fs^2$
$\displaystyle TOD = 381.52310 ± 94.83666 fs^3$

Ez úgy lehet hasznos, hogy amikor más módszerrel több interferogramot is kiértékelünk egymás után, csak rögzítjük az SPP adatokat is, aztán a program ezekből egyenként összegyűjti a szükséges információt a kiértékeléshez, majd abból számol.

5.3 Az SPPMethod működéséről mélyebben, cache, callbacks, from_log

Az SPPMethod alapvető működését az adatok rögzítése közben az alábbi ábra mutatja.

SPP működése

A hurok az SPPMethod-ból indul, ahol a használandó fájlok neveit, betöltési adatokat, stb. adunk meg. Ezen a ponton még semmilyen számolás és betöltés nem történik. Ezután az SPPMethod bármely elemének hívására egy Dataset objektum jön létre. Ezen megnyitható az SPPEditor, amiben az állandó fázisú pont(ok) helyét és a karok közti késleltetést lehet megadni. Hitelesítés után az SPP-vel kapcsolatos információk az interaktív szerkesztőből visszakerülnek a létrehozott Dataset objektumba és ott rögzítődnek. Minden így létrejött Dataset objektum kapcsolva van az SPPMethod-hoz, amiből felépült, így amikor megváltozik egy SPP-vel kapcsolatos adat, az egyből megváltozik az SPPMetod-ban is. A Registry gondoskodik arról, hogy minden objektum ami a memóriában van az rögzítődjön, illetve szükség esetén elérhető legyen.

Cache

Ha próbálunk elérni egy adott elemet (akár a for ciklussal, akár indexelve, vagy egyéb módon), létrejön egy Dataset objektum. Ez a Dataset objektum miután már egyszer elértük a memóriában marad és megtart minden rajta végrehajtott változtatást, beállítást. Alapértelmezetten 128 db interferogram marad a memóriában egyszerre, de ez a határ szükség esetén megváltoztatható. Az éppen aktuálisan a memóriában lévő interferogramok száma (az adott SPPMethod-hoz tartozó) a kiíratás során a Interferograms cached cellában látható.

Callbacks

A fenti ábrán a ciklus utolsó lépése során (ahol a Dataset átadja az SPP-vel kapcsolatos adatait a SPPMethod-nak) lehetőség van további ún. callback függvények meghívására. Egy ilyen beépített callback függvény a pysprint.eager_executor. Ez arra használható, hogy minden egyes SPP-vel kapcsolatos adat rögzítése/változtatása után a program azonnal kiszámolja az éppen meglévő adatokból a diszperziót. A korábbiakhoz teljesen hasonlóan kell eljárnunk, csupán a callback argumentumot kell megadnunk kiegészítésként. Itt a kötelező argumentumokon túl megadtam a logfile és verbosity értékeit is: ez minden lépés során a "mylog.log" fájlba el fogja menteni az adott illesztés eredményeit és egyéb információkat, továbbá a verbosity=1 miatt a rögzített adatsort is. Ezzel akár könnyen nyomon követhető a kiértékelés menete.

[27]:
# a folyamatos kiértékeléshez szükséges callback függvény importálása
from pysprint import eager_executor

myspp2 = ps.SPPMethod(
    ifg_files,
    decimal=".",
    sep=",",
    skiprows=0,
    meta_len=0,
    callback=eager_executor(reference_point=2, order=3, logfile="mylog.log", verbosity=1)
)
[28]:
myspp2
[28]:
SPPMethod
Interferograms accumulated 9
Interferograms cached 0
Data recorded from 0
Eagerly calculating True

Ekkor láthatjuk, hogy az Eagerly calculating már True értékre változik.

Természetesen a program csak értelmes esetekben fogja elvégezni a számolást (pl. szükséges, hogy az adatpontok száma nagyobb legyen, mint az illesztés rendje ). A teljesség kedvéért megemlítendő, hogy könnyen írható akár saját callback függvény is. Futtassuk le az újonnan létrehozott myspp2-n a már megismert for ciklust:

[29]:
with ps.interactive():
    for ifg in myspp2:
        ifg.open_SPP_panel()
$\displaystyle GD = 49.67951 ± 0.00000 fs^1$
$\displaystyle GDD = -380.81053 ± 16.67232 fs^2$
$\displaystyle TOD = 402.32343 ± 179.81894 fs^3$

A fenti cella futtatása közben - miután rögzítettünk elég adatot - megjelentek az eredmények, és minden új adatpont hozzáadása esetén frissültek is. Az adatok rögzítését itt ugyan az interaktív felületet használva végeztem, de akár kódban is megtehető: a myspp elemein kell az delay és positions argumentumokat beállítani, és minden új adat hozzáadásánál újra fogja számolni a program. Az előző számolásom közben készült logfile a következő:

[30]:
!type mylog.log
---------------------------------------------------------------------------------------
Date: 2020-12-30 09:35:32.569563
Datapoints used: 3
R^2: 1.00000
Results:
GD = 50.00000 fs^1
GDD = -378.39901 fs^2
TOD = 368.22696 fs^3
Values:
x: [2.31146, 2.14193, 1.99999]
y: [-50.,   0.,  50.]
---------------------------------------------------------------------------------------
Date: 2020-12-30 09:35:36.344006
Datapoints used: 4
R^2: 0.99987
Results:
GD = 50.92928 fs^1
GDD = -398.34978 fs^2
TOD = 481.48866 fs^3
Values:
x: [2.31146, 2.14193, 1.99999, 1.88565]
y: [-50.,   0.,  50., 100.]
---------------------------------------------------------------------------------------
Date: 2020-12-30 09:35:42.355376
Datapoints used: 5
R^2: 0.99807
Results:
GD = 51.99789 fs^1
GDD = -360.91805 fs^2
TOD = 186.61942 fs^3
Values:
x: [2.31146, 2.14193, 1.99999, 1.88565, 1.73977]
y: [-50.,   0.,  50., 100., 150.]
---------------------------------------------------------------------------------------
Date: 2020-12-30 09:35:48.140150
Datapoints used: 6
R^2: 0.99455
Results:
GD = 49.67951 fs^1
GDD = -380.81053 fs^2
TOD = 402.32343 fs^3
Values:
x: [2.31146, 2.14193, 1.99999, 1.88565, 1.73977, 1.68063]
y: [-50.,   0.,  50., 100., 150., 200.]
---------------------------------------------------------------------------------------
Date: 2020-12-30 09:35:50.094888
Datapoints used: 6
R^2: 0.99455
Results:
GD = 49.67951 fs^1
GDD = -380.81053 fs^2
TOD = 402.32343 fs^3
Values:
x: [2.31146, 2.14193, 1.99999, 1.88565, 1.73977, 1.68063]
y: [-50.,   0.,  50., 100., 150., 200.]

Ebből a fájlból akár be is tölthetjük a benne szerepelő adatokat. Ehhez használjuk a from_log konstruktort:

[31]:
spp_checkpoint = ps.SPPMethod.from_log('mylog.log')

Ez visszaállította a log fájlban rögzített adatokat. Ezen az objektumon új kiértékelést is végezhetünk, sőt módosíthatjuk a korábban beírt adatokat. Az interferogram értékeit azonban csak ebből a fájlból nem ismerjük, így azok x-y értékek nélkül kerülnek felépítésre. Pl. a visszaállított spp_checkpoint első interferogramja:

[33]:
spp_checkpoint[0]
[33]:

Ez egy ún. MimickedDataset objektum, amelyen megkötés nélkül módosíthatjuk az SPP-vel kapcsolatos adatokat. Az adatok változtatására továbbra is reagálni fog az spp_checkpoint:

[35]:
spp_checkpoint.calculate(2, 3);
$\displaystyle GD = 49.67961 ± 0.00000 fs^1$
$\displaystyle GDD = -380.80775 ± 16.67256 fs^2$
$\displaystyle TOD = 402.30335 ± 179.82198 fs^3$
[36]:
# egy adat megváltoztatása
spp_checkpoint[0].delay = -70

# újraszámolás már más adatokat ad
spp_checkpoint.calculate(2, 3);
$\displaystyle GD = 50.86642 ± 0.00000 fs^1$
$\displaystyle GDD = -407.56908 ± 18.75457 fs^2$
$\displaystyle TOD = 178.54695 ± 202.27753 fs^3$

Egy ilyen MimickedDataset objektum visszakonvertálható igazi Dataset objektummá - amennyiben emlékszünk melyik interferogram fájlhoz tartozott. Ehhez használhatjuk a to_dataset függvényt.

[37]:
mydataset = spp_checkpoint[0].to_dataset('-100.txt', parse=True, skiprows=8, meta_len=0, decimal=".", sep=",")
[38]:
mydataset
[38]:

Egy egyszerűbb módszer a betöltésre a from_pattern konstruktorral történik. Itt egy mintát kell megadnunk, amire a fájlnevek épülnek. Opcionálisan a mod argumentummal csoportosíthatjuk őket a megadott modulo szerint (általában a mod=3 lehet hasznos, ekkor ugyanis a karok spektrumaival könnyen csoportosíthatjuk a fájlokat.) Az általam generált fájlokhoz a "*.txt" mintát használom, illetve a betöltési opciók változatlanok:

[44]:
spp2 = ps.SPPMethod.from_pattern('*.txt', skiprows=8, meta_len=0, decimal=".", sep="," )
[45]:
spp2
[45]:
SPPMethod
Interferograms accumulated 9
Interferograms cached 0
Data recorded from 0
Eagerly calculating False