Показаны сообщения с ярлыком python. Показать все сообщения
Показаны сообщения с ярлыком python. Показать все сообщения

пятница, 25 марта 2011 г.

Стрелки в Pylab

Pylab представляет собой очень мощную библиотеку для графопостроения. При проведении расчётов и анализа данных в Python, при помощи pylab не представляет труда вывести результат на график. Однако просто графики зачастую не являются информативными для неподготовленного читателя. В этом случае, помогают подсказки, сделанные на графиках, в частности, стрелки с указаниями. Кроме того, стрелки являются незаменимыми при построении графиков с несколькими y-осями. В этом случае весьма желательно указывать с помощью стрелок к какой оси относится тот или иной график.

Pylab.arrow

Казалось бы, при всей мощи, от pylab следовало бы ожидать удобной возможности нанесения такого рода деталей на графики. Но, надо признать, что здесь всё не очень очевидно. К примеру, имеется функция pylab.arrow, принимающая начальные координаты (x,y) и (dx,dy). Казалось бы, нет ничего проще.

import pylab as pl
import numpy as np
pl.figure(figsize=(4,4))
pl.title('pylab.arrow, square ratio')
z = np.arange(10)
pl.plot(z,z,'ro')
pl.arrow(2,2,3,4, head_width=0.5, head_length=1)
pl.show()

Однако, если нарисовать график в осях, существенно отличающихся по масштабу, то вид будет ужасный. Например:

x = np.arange(0.1, 1000, 0.1)
y = np.log(x)

pl.figure(figsize=(6,4))
pl.title('pylab.arrow, different axis scale')
pl.plot(x, y, 'b-')
pl.arrow(400,2,200,3, head_width=0.5, head_length=1)
pl.arrow(200,2,300,4, head_width=0.5, head_length=100)
pl.ylim(ymin = 0)
pl.show()

Мало того, что форма стрелки искажённая, так ещё и надо вручную задавать размеры наконечника head_length и head_width под каждый масштаб.

Pylab.annotate

Однако оказалось, что есть более приличный способ отрисовки стрелок, pl.annotate. На самом деле основное предназначение pl.annotate состоит в том, чтобы отмечать стрелками заданные точки, и одновременно указывать подписи. Например, таким образом:

Для этого был использован следующий код:

pl.figure(figsize=(6,4))
pl.plot(x, y, 'b-')
pl.title('pylab.annotate example')
pl.annotate('Label', xy = (500, np.log(500)), xytext=(300,1),\
                arrowprops=dict(facecolor='red', width=0.5,\
                                    headwidth=10, shrink=0.05),\
                ha='center', va='baseline', fontsize='large')

В качестве аргументов передаются

  • строка с надписью
  • координаты точки, куда указывает стрелка
  • координаты надписи
  • свойства стрелки в ввиде словаря
  • остальные свойства, касающиеся свойств текста

Все свойства текста описаны в мануале по pylab.annotate, посмотреть который можно в интерактивном режиме питона. Например:

$ ipython
> import pylab as pl
> help(pl.annotate)

Что касается свойств стрелки, то их можно посмотреть так:

$ ipython
> import matplotlib
> help(matplotlib.lines.Line2D)

Из основных характеристик стрелок, на которые следует обратить внимание, отмечу следующие:

  • facecolor — цвет наконечника
  • width — толщина линии в пунктах
  • headwidth — ширина наконечника в пунктах
  • frac — доля длины, которую занимает наконечник
  • shrink — отступ, от заданной точки xy до конца наконечника (чтобы наконечник не втыкался точно в заданную точку, а оставил некоторое пространство). Измеряется в долях длины.

Что касается свойств текста, то я бы в первую очередь отметил следующие:

  • ha (horizontalalignment) — способ выравнивания по горизонтали ('center', 'right', 'left')
  • va (verticalalignment) — соответственно, по вертикали ('center', 'top', 'bottom', 'baseline')
  • rotation — поворот текста (угол в градусах, 'vertical', 'horizontal')
  • fontsize — размер шрифта (можно указать в пунктах, а можно и словами 'small', 'medium', 'large', 'x-large', 'xx-large')

Как видно, при использовании pylab.annotate форма стрелки получилась адекватной. А если вместо надписи оставить пустую строку, то просто получится стрелка от xytext до xy (при условии, что shrink = 0)

Лично мне весьма неудобно выписывать все свойства arrowprops всякий раз, когда надо нарисовать стрелку, поэтому я нацарапал простейшую функцию, включающую в себя обязательные аргументы, такие как строка с надписью и координаты начальной и конечной точек, а также необязательные, касающиеся характеристик стрелок и текста:

def my_arrow(label, xy_from, xy_to, color='blue', shrink=0.05, width=0.5, headwidth=10):
    pl.annotate(label, xy = xy_to, xytext=xy_from,\
                    arrowprops=dict(facecolor=color, width=width,\
                                        shrink=shrink, frac=0.1),\
                    ha='center', va='baseline', fontsize='large')

Теперь нет ничего проще, нарисовать стрелку:

pl.figure(figsize=(6,4))
pl.title('pylab.annotate')
pl.plot(x, y, 'b-')
my_arrow('Label', (100,1), (600, 5))
my_arrow('', (300,1), (800, 5))
# красные точки для понимания каким образом позиционируется текст
pl.plot([100,600],[1,5], 'ro')
pl.ylim(ymin = 0)

Результат, на мой взгляд, приличный. А характеристики текста и свойства стрелок можно один раз задать в функции, и больше о них не вспоминать.

Стоит заметить, что этим вид стрелок не ограничивается. Можно также рисовать стрелки с рюшечками, стоит только в arrowprops упомянуть про arrowstyle.

pl.figure(figsize=(6,4))
pl.title('Fancy arrow')
pl.plot(x, y, 'b-')
pl.annotate('Fancy arrow', xy = (500, np.log(500)), xytext=(300,1),\
                arrowprops=dict(facecolor='red',arrowstyle='fancy',\
                           connectionstyle='angle3, angleA=0, angleB=90'),\
                fontsize='x-large')
pl.ylim(ymin = 0)

Для более детального ознакомления следует посмотреть в примеры mpl_examples/pylab_examples/annotation_demo2.py


Читать далее…

четверг, 3 марта 2011 г.

Однотипные срезы массивов в python

Известно, что в python есть такая замечательная вещь, как срезы массивов (списков, кортежей и т.д.). По сути, в качестве начала и конца среза передаются положения элементов в массиве. Например,

x = range(10)
print x
> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print x[1:3]
> [1, 2]
print x[0:5]
> [0, 1, 2, 3, 4]

Видно, что по сути отсчёт ведётся не по элементам, а по промежуткам между элементами, при этом на выходе даются все элементы, попадающие в заданный интервал промежутков. Особый интерес представляют отрицательные индексы, являющиеся по сути обратным отсчётом промежутков с конца массива.

print x[1:-1]
> [1, 2, 3, 4, 5, 6, 7, 8]
print x[0:-2]
> [0, 1, 2, 3, 4, 5, 6, 7]

Однако, что делать, если, скажем, имеется набор экспериментальных данных, которые мы хотим обрезать, основываясь на значениях, нежели на положении элементов в массивах? Допустим, есть список x,

from math import *
import numpy as np
x = np.arange(0, 2*pi, 0.1)
из которого надо вырезать 1.2<x<2.7.

Традиционный способ

В принципе, это можно осуществить перебором по всем элементам списка:

begin,end = 0,0
x0, x1 = 1.2, 2.7
for i in range(len(x)):
    if begin == 0:
        if x[i] > x0:
            begin = i
        else: pass
    else: pass
    if int(end) == 0:
        if x[i] > x1:
            end = i
        else: pass
    else: pass
print begin, end
print x[begin:end]

В результате получим:

> 12 28
[ 1.2  1.3  1.4  1.5  1.6  1.7  1.8  1.9  2.   2.1  2.2  2.3  2.4  2.5  2.6 2.7]

В принципе. то, чего и ожидали. Теперь можно этот цикл for оформить внутри функции, которая будет принимать в качестве аргументов список и границы.

def conventional_cut(x, (x0,x1)):
    begin,end = 0,0
    for i in range(len(x)):
        if begin == 0:
            if x[i] > x0:
                begin = i
            else: pass
        else: pass
        if end == 0:
            if x[i] > x1:
                end = i
            else: pass
        else: pass
    return x[begin:end]
print conventional_cut(x, (0.9, 1.4))
> [ 1.   1.1  1.2  1.3]

Несколько массивов

С практической точки зрения удобно резать сразу два списка: агрумент и функцию. Допустим, отснимали большой спектр y от x, из которого интерес представляет только часть. Таким образом, необходимо откусить часть от x, а также соответствующую часть y.

Сделать это легко, слегка модернизировав функцию:

def cut_xy((x,y), (x0,x1)):
    begin,end = 0,0
    for i in range(len(x)):
        if begin == 0:
            if x[i] > x0:
                begin = i
            else: pass
        else: pass
        if end == 0:
            if x[i] > x1:
                end = i
            else: pass
        else: pass
    return x[begin:end], y[begin:end]

То есть мы будем использовать те же индексы для списка y, что и для x. Приведу пример, для большей наглядности, графический:

from math import *
import numpy as np
import pylab as pl

x = np.arange(0, 4*pi, 0.01)
y = np.sin(x)/x
y = y + 0.1*(np.random.rand(len(y))-0.5)
# представим, что x,y - экспериментальные данные

xnew, ynew = cut_xy((x,y), (0.75*pi, 1.5*pi))
pl.plot(x, y, 'r-')
pl.plot(xnew, ynew, 'b-')

pl.xlabel("x")
pl.ylabel("Signal")
pl.show()

В результате получим то, что и следовало ожидать.

Выборочное соответствующее вырезание двух массивов
Выборочное соответствующее вырезание двух массивов

Разумеется, никто не запрещает резать не два, а три и более массивов одновременно, немного видоизменив функцию cut_xy.

Использование numpy.nonzero

Традиционный метод работает, однако может показаться корявым. По-видимому, более элегантное решение может быть получено с использованием функции nonzero из библиотеки numpy.

Пример работы numpy.nonzero:

from math import *
import numpy as np
x = np.arange(0, 2*pi, 0.1)
print x[np.nonzero(x<2.7)]
print x[np.nonzero(x>1.4)]
> [ 0.   0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  1.   1.1  1.2  1.3  1.4
  1.5  1.6  1.7  1.8  1.9  2.   2.1  2.2  2.3  2.4  2.5  2.6]
> [ 1.4  1.5  1.6  1.7  1.8  1.9  2.   2.1  2.2  2.3  2.4  2.5  2.6  2.7  2.8
  2.9  3.   3.1  3.2  3.3  3.4  3.5  3.6  3.7  3.8  3.9  4.   4.1  4.2  4.3
  4.4  4.5  4.6  4.7  4.8  4.9  5.   5.1  5.2  5.3  5.4  5.5  5.6  5.7  5.8
  5.9  6.   6.1  6.2]

В скобках np.nonzero() указывается булево выражение, при этом nonzero возвращает индексы ненулевых элементов. Выражение x<2.7 является булевым и имеет следующий вид:

[ True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False]
То есть, если x[i]>2.7, то результат True, в противном случае False. Стало быть, передавая x<2.7 в качестве аргумента функции nonzero, и скармливая результат исходному массиву x в качестве индексов среза, получаем обрезанный x, где каждый элемент x не превышает 2.7.

Трудность заключается в том, что не удаётся в качестве аргумента nonzero затолкать сразу несколько условий. Таким образом, если надо откусить массив с двух краёв, то необходимо сначала откусить массив слева, а потом результат — справа:

X = x[np.nonzero(x>1.4)]
X = X[np.nonzero(X<2.7)]
print X
> [ 1.4  1.5  1.6  1.7  1.8  1.9  2.   2.1  2.2  2.3  2.4  2.5  2.6]

Замечу, что при использовании nonzero левая граница промежутка включается в искомый массив.

Если мы хотим соответствующим образом откусить и второй массив такой же размерности, который в нашем эксперименте является функцией, то мы должны поступить аналогично:

X = x[np.nonzero(x>1.4)]
X = X[np.nonzero(X<2.7)]
Y = y[np.nonzero(x>1.4)]
Y = Y[np.nonzero(X<2.7)]

print "X =", X
print "Y =", Y

На выходе:

X = [ 1.4  1.5  1.6  1.7  1.8  1.9  2.   2.1  2.2  2.3  2.4  2.5  2.6]
Y = [ 0.66167563  0.62029339  0.59024345  0.57468639  0.5094329   0.5281753
  0.4542492   0.39952115  0.37289637  0.36049412  0.266617    0.19706189
  0.247443  ]

Можно аналогичным образом затолкать это в функцию:

def cut((x,y),(x0,x1)):
    after = np.nonzero(x>x0)
    X = x[after]
    before = np.nonzero(X<x1)
    X = X[before]
    Y = y[after]
    Y = Y[before]
    return X,Y
которая принимает в качестве аргументов исходные (x,y) и интервалы x.

Графический пример:

from math import *
import numpy as np
import pylab as pl

x = np.arange(0, 2*pi, 0.1)
y = np.sin(x)/x
y = y + 0.1*(np.random.rand(len(y))-0.5)
# представим, что x,y - экспериментальные данные

pl.plot(x, y, 'r-')
X,Y = cut((x,y),(4,5))
pl.plot(X,Y,'g-')

pl.xlabel("x [channels]")
pl.ylabel("Signal [counts]")
pl.show()
Результат аналогичной процедуры при использовании numpy.nonzero
Результат аналогичной процедуры при использовании numpy.nonzero

Читать далее…

среда, 29 апреля 2009 г.

Уточнение данных полиномом n-степени в Python (scipy, polyfitw)

Задача - произвести подгонку данных полиномом n-степени. Известно, что каждое, экспериментально измеренное значение, представляет собой два числа: собственно значение и погрешность измерения. Таким образом, желательно произвести подгонку с учётом весовых коэффициентов каждой точки. Ведь может оказаться так, что в эксперименте есть точки, измеренные с меньшей точностью и степенью достоверности, но которые не желательно просто так выбрасывать, но и нельзя учитывать наравне с другими, более достоверными.

В качестве инструмента выбираем Python, благодаря расширяемости этого языка.

Модули в Python

Scipy

В Python для численных операций имеются модули SciPy и NumPy. Однако, несмотря на всю мощь этих модулей, я не нашёл в них возможности производить уточнение именно с учетом весовых коэффициентов. В scipy содержится функция polyfit, которая принимает только значения аргумента, функции и степени полинома, но не учитывает погрешности.

Модуль CARSMath

На сайте The Consortium for Advanced Radiation Sources есть разработанный Марком Риверсом модуль CARSMath. Он содержит функцию polyfitw, которая помимо аргумента, функции и степени полинома принимает также и весовые коэффициенты (именно весовые коэффициенты, а не погрешность, то есть 1/погрешность).

Установка модуля CARSMath

Скачайте модуль отсюда. Бросьте тарбол, куда-нибудь, например, в ~/python/python_epics.

Распакуйте:

tar xvf python_epics.tar
Нас интересует файл CARSMath.py.

Теперь, чтобы использовать этот модуль, надо его бросать в рабочую директорию с вашим скриптом, что очень неудобно. Поэтому надо воспользоваться переменной окружения $PYTHONPATH. Для этого отредактируйте файл ~/.bashrc. Необходимо добавить следующую строчку:

export PYTHONPATH="$HOME/distrib/python/lib"

Естественно, директория должна быть указана та, где лежит скачанный модуль CARSMath (я его перекинул в ~/distrib/python/lib, там же у меня лежат и другие модули, не распространяемые с дистрибутивом).

После этого инициализируем оболочку заново:

source ~/.bashrc

Если вы запускаете иксы не через startx, а через менеджер входа в систему, то вам надо узнать каким образом в этом случае инициализировать переменные окружения. Ключевое слово .XClients, если я ничего не путаю.

Теперь можно, находясь в любой директории, импортировать модуль CARSMath стандартным образом:

import CARSMath

Сравнение двух способов подгонки

Сравним два способа подгонки экспериментальных данных полиномом n-й степени:

  • с помощью polyfit из scipy
  • с помощью polyfitw из CARSMath

Для этого создадим массив точек, сделаем несколько «выбросов» с большими погрешностями и аппроксимируем полиномом (в одном случае без учёта погрешностей, а во втором - с учётом).

Ниже проведён скрипт, реализующий эти действия.

#!/usr/bin/env python
#-*-coding: utf-8 -*-

import scipy
from numpy import arange
import pylab as pl
from CARSMath import polyfitw

x = arange(0, 2, 0.01)
y = 0.5 * x**3 - 2 * x**2 + x
error = 0.005 * scipy.rand(len(x))

# генерим "выпавшие" точки с большими погрешностями
for i in range(30, 60):
    y[i] = y[i] - 2* abs(scipy.rand(1))
    error[i] = 1 * abs(scipy.rand(1))

# находим коэффициенты полинома без учёта погрешностей
fit_coeffs = scipy.polyfit(x, y, 3)
# структура коэффициентов: (x**(max), x**(max-1), x**(max-2), ...)

def fit_func(x, fit_coeffs):
    """
    фит без учёта весов

    """
    fit_func = 0
    # максимальная степень полинома.
    # В данном случае 3 = 4 - 1
    max_degree = len(fit_coeffs)-1

    for i in range(max_degree):
        fit_func += fit_coeffs[i]* x**(max_degree-i)
    return fit_func


# коэффициенты полинома с учетом весов 1/error
# i-й коэффициент соответствует i-й степени полинома
# здесь полная степень полинома = 3
weight_coeffs = polyfitw(x, y, 1/error, 3)

def weight_func(x, weight_coeffs):
    """
    фит с учётом весов

    """
    weight_func = 0

    # обратите внимание на другую структуру вектора weight_coeffs
    # (x**0, x**1, x**2, x**3)
    for i in range(len(weight_coeffs)):
        weight_func += weight_coeffs[i] * x**i
    return weight_func


pl.errorbar(x, y, error, fmt= 'bo')
# график уточнения без "весов"
pl.plot(x, fit_func(x, fit_coeffs), 'r-', \
        linewidth=2, label='no weights')
# график уточнения с "весами"
pl.plot(x, weight_func(x, weight_coeffs), 'g-', \
        linewidth=2, label='with weights')

pl.title('Comparison between two fitting methods')
pl.legend()
pl.show()

Здесь я создал функции fit_func, weight_func, возвращающие значения подгоночной функции, хотя в таком простом примере этого можно было и не делать.

В результате получим следующую картину:

Видно, что в случае уточнения без весовых коэффициентов мы получили совсем не то, что следует, в то время как учёт погрешностей дал адекватный результат.


Читать далее…

среда, 15 апреля 2009 г.

Пишем скрипт для Fityk

Предположим, у нас есть большое количество практически идентичных дифрактограмм. Допустим, снимались они на одном и том же дифрактометре (стало быть, имеем одну и ту же базовую линию), на одной и той же длине волны (то есть имеем приблизительно одно и то же положение рефлексов). Также, пусть на картине отсутствует перекрытие рефлексов (к нему надо подходить всё-таки творчески и не доверять целиком компьютеру).

Задача — обработать потоково несколько дифракционных картин сразу, результаты аппроксимации (положение центра, площадь под кривой, ширина на полувысоте) требуется записать в общий файл.

Команды в fityk

Команды, которые мы собираемся давать описаны в руководстве программы, однако программа настолько удобна, что при работе в графическом режиме автоматически пишет в эхо-области какую команду вы выполнили. Стало быть, если вы открыли дифракционную картину, то увидите что-то типа:

@0 < '/home/user/path/to/profile.dat'
Более общие способы загрузки файлов, типа использования определенных колонок, стандартного отклонения и прочие способы работы с данными смотрите в официальной документации. Но общий синтаксис таков:
слот < имя_файла [:xcol:ycol:scol:block ] [доп. опции …]

Если вы удалите базовую линию, то в эхо области выведется сообщение типа (разумеется, в зависимости от того, какой вид базовой линии вы выбрали, результат будет отличаться):

Y = y - spline[29.9167, 8.58396, 70.1091, 7.88817](x)
Для выделения активной области служит команда:
a = (x_min < x < x_max)
Для того, чтобы установить нулевое приближение профильной линии пика, служит команда (в данном случае для функции PseudoVoigt):
%p = guess PseudoVoigt
Как я говорил в предыдущем посте, программа достаточно хорошо «угадывает» начальные профильные параметры.

Уточнение профиля начинается по команде

fit
Экспортирование данных производится командой
info слот (выражение, ...) > имя_файла
Я использую команду info+ %имя_функции для получения полной информации о пике. Итак, полный цикл обработки одной дифракционной картины должен быть таким:
  1. Открываем файл с дифрактограммой
  2. Удаляем базовую линию
  3. Определяем активную область
  4. «Угадываем» положение пика, описанного определенной функцией
  5. Производим уточнение
  6. Записываем параметры пика в файл
  7. Удаляем аппроксимирующую функцию
  8. Переходим к другой активной области и возвращаемся к пункту 3.
  9. Открываем новый файл с дифрактограммой

Для одного файла на языке cfityk это будет выглядеть так:

@0 < '/home/user/path/to/profile.dat'
Y = y - spline[29.9167, 8.58396, 70.1091, 7.88817](x)

a = (30<x<34)
%p = guess Voigt
fit
info+ %p > ./peaks
delete %p

a = (38<x<41)
%p = guess Voigt
fit
info+ %p >> ./peaks
delete %p

a = (44<x<48)
%p = guess Voigt
fit
info+ %p >> ./peaks
delete %p

a = (56<x<59)
%p = guess Voigt
fit
info+ %p >> ./peaks
delete %p

a = (65<x<70)
%p = guess Voigt
fit
info+ %p >> ./peaks
delete %p
Чтобы сделать это для нескольких файлов, надо написать скрипт. Примерно год назад, я уже его написал, одну часть на Perl, а другую на bash с применением sed и awk. Недавно я на него взглянул и ужаснулся. Не то, чтобы было непонятно, но как-то некрасиво. С Perl я только лишь познакомился, и, вероятно, сделал всё совсем не так, как это следует делать. Недавно стал изучать Python, поэтому на нём и написал скрипт, который будет потоково обрабатывать сразу несколько файлов и распихивать все выходные данные по файлам.

Пишем скрипт на Python

Для начала определимся с концепцией. Допустим у нас в директории n дифрактограмм, которые надо обработать совершенно одинаковым способом. Я предлагаю запихнуть информацию о базовой линии и активных областях в один файл, его я назвал base_active.list, у меня он выглядит так:

Y = y - spline[29.9167, 8.58396, 70.1091, 7.88817](x) #baseline
30 33
38 41
44 48
55 59
65 69
То есть, в первой строчке та самая формула, описывающая базовую линию, а далее x_min x_max, фигурирующие в определении активной области.

Мы должны указать какой функцией аппроксимировать все пики. Я решил это сделать в интерактивном режиме. То есть после запуска скрипта появляется сообщение какую функцию выбрать - у меня всё повешено на цифры.

Итак, вводная часть выглядит так:

#!/usr/bin/env python
#-*-coding: utf-8 -*-
import sys, math, os, re

try:
    os.rename('outfile.list', 'outfile.list.bak')
except: pass
try:
    os.rename('center.out', 'center.out.bak')
except: pass
outfile = open('outfile.list', 'w')
center_outfile = open('center.out', 'w')

try:
    input_file = sys.argv[1]
except:
    print "Usage:", sys.argv[0], "input files"
    sys.exit(1)

try:
    active = open('base_active.list', 'r')
except:
    print "Need base_active.list file of baseline formula and active areas"
    sys.exit(1)

input_FUNCTION = input("Enter type of function:\n\
1 - Pearson7A\n\
2 - Pearson7\n\
3 - PseudoVoigt\n\
4 - Voigt\n\
5 - Lorentzian\n\
6 - Gaussian\n\
")
if input_FUNCTION == 1:
    FUNCTION = 'Pearson7A'
elif input_FUNCTION == 2:
    FUNCTION = 'Pearson7'
elif input_FUNCTION == 3:
    FUNCTION = 'PseudoVoigt'
elif input_FUNCTION == 4:
    FUNCTION = 'Voigt'
elif input_FUNCTION == 5:
    FUNCTION = 'Lorentzian'
elif input_FUNCTION == 6:
    FUNCTION = 'Gaussian'
else:
    print "Inappropriate value"
    sys.exit(1)

Первым делом делаем бэкап старых выходных файлов.

try:
    os.rename('outfile.list', 'outfile.list.bak')
except: pass
try:
    os.rename('center.out', 'center.out.bak')
except: pass

Я сохранял один файл outfile.list, в котором по порядку идут файлы и параметры их пиков:

20.dat   Pearson7A
N       center  area    FWHM    shape
0       32.4379 330.0   0.159   1.606
1       40.0144 356.4   0.121   0.481
2       46.5596 177.8   0.169   0.863
3       57.8951 175.0   0.191   0.832
4       67.9423 189.1   0.228   0.624

diffrac.dat      Pearson7A
N       center  area    FWHM    shape
0       31.9074 435.0   0.164   1.571
1       39.3616 1876.0  0.146   0.504
2       45.7733 190.3   0.150   0.712
3       56.9166 223.9   0.175   0.717
4       66.7720 698.2   0.164   0.485
и файл center.out в котором строки содержат имя файла и положения центров пиков по порядку:
20      32.4379 40.0144 46.5596 57.8951 67.9423
diffrac 31.9074 39.3616 45.7733 56.9166 66.7720
Затем открываю на запись файлы outfile.list и center.out:
outfile = open('outfile.list', 'a')
center_outfile = open('center.out', 'a')

Проверяем есть ли хотя бы один передаваемый скрипту параметр:

try:
    input_file = sys.argv[1]
except:
    print "Usage:", sys.argv[0], "input files"
    sys.exit(1)
sys.srgv[0] соответствует самому скрипту.

Аналогично проверяем есть ли в данной директории файл с базовой линией и активными зонами base_active.list.

Ну и наконец, задаём в интерактивном режиме функцию профиля.

Далее, я решил код оформить в виде двух функций: формирующей входной файл для cfityk и выкусывающей из выходного файла нужные параметры.

Первая функция выглядит следующим образом:

def create_cfityk_input(active):
    base = active.readline()
    myfile = ""
    while True:
        line = active.readline()
        if line == "":
            break

        x = line.split()
        x1 = x[0]
        x2 = x[1]
        myfile += "a = ("+ x1+ " < x < "+ x2+ ")\n"
        myfile += "%p = guess" + FUNCTION + "\n"
        myfile += "fit\n"
        myfile += "info+%p >> ./peaks\n"
        myfile += "delete %p\n\n"
    active.close()
    return myfile, base

Сначала читается первая строчка - базовая линия. Затем, в цикле, все оставшиеся строчки до конца файла. Элементы в этих строчках разделены пробельными символами, поэтому легко выкусываем x1 и x2 в активной области. И формируем тело файла. Параметры пиком пишем в файл 'peaks'. Функция возвращает основное тело файла и строку с базовой линией. Возвращаемое значение я присвоил кортежу:

(fit_section, base) = create_cfityk_input(active)

Далее необходимо скормить полученный файл программе cfityk. Это я реализовал во второй функции:

def fit_one_file(input_file):
    try:
        os.remove('peaks')
    except:
        pass
    myfirstlines = '@0<' + input_file + "\n" + base + "\n"

    cfityk_input = open('cfityk_input', 'w')
    cfityk_input.write(myfirstlines)
    cfityk_input.write(fit_section)
    cfityk_input.close()

    os.system("cfityk < cfityk_input")  # запускаем fityk
    os.remove("cfityk_input")  # удаляю входной файл для fityk - больше не нужен

Сначала удаляем старый файл 'peaks' (если есть), в который мы пишем параметры рефлексов. Затем формируем первые две линии во входном файле, содержащие собственно имя файла и базовую линию. Наконец, запускаем cfityk и удаляем входной файл.

После прохода по каждому входному файлу формируется файл peaks, имеющий вид:

%p = Pearson7($_1, $_2, $_3, $_4)
Pearson7(height, center, hwhm, shape=2) = height/(1+((x-center)/hwhm)^2*(2^(1/shape)-1))^shape
height = $_1 = ~1951.83 = 1951.83  [auto]
center = $_2 = ~31.9079 = 31.9079  [auto]
hwhm = $_3 = ~0.0920368 = 0.0920368  [auto]
shape = $_4 = ~2.59149 = 2.59149  [auto]
FWHM: 0.184074
Area: 421.841
%p = Pearson7($_5, $_6, $_7, $_8)
Pearson7(height, center, hwhm, shape=2) = height/(1+((x-center)/hwhm)^2*(2^(1/shape)-1))^shape
height = $_5 = ~153.244 = 153.244  [auto]
center = $_6 = ~39.3615 = 39.3615  [auto]
hwhm = $_7 = ~0.090969 = 0.090969  [auto]
shape = $_8 = ~1.1861 = 1.1861  [auto]
FWHM: 0.181938
Area: 39.7215
…

Чтобы вырезать отсюда значения положения центра, полуширины, площади под кривой и параметра shape (который присутствует только для функций Pearson7A, Pearson7, PseudoVoigt, Voigt, представляющих различные комбинации гаусса и лоренца) можно воспользоваться регулярными выражениями.

Ценную информацию о регулярных выражениях в Python можно найти на INTUIT.RU.

Для определения положения центра сгодятся выражения вида:

pattern_center = r"center.+=\s(.*)\[auto\]"
Аналогично будет для определения shape.

В зависимости от выбранной функции будут различные выражения для вырезания площади под кривой. Для Voigt, PseudoVoigt, Pearson7, Gaussian, Lorentzian:

pattern_area = r"Area:\s(.*)"
Для Pearson7A:
pattern_area = r"area.+=\s(.*)\[auto\]"

И для ширины на полувысоте:

pattern_fwhm = r"FWHM:\s(.*)"

Собственно, и всё, остальное чисто технические моменты. Полный скрипт:

#!/usr/bin/env python
#-*-coding: utf-8 -*-
import sys, math, os, re

try:
    os.rename('outfile.list', 'outfile.list.bak')
except: pass
try:
    os.rename('center.out', 'center.out.bak')
except: pass
outfile = open('outfile.list', 'w')
center_outfile = open('center.out', 'w')

try:
    input_file = sys.argv[1]
except:
    print "Usage:", sys.argv[0], "input files"
    sys.exit(1)

try:
    active = open('base_active.list', 'r')
except:
    print "Need base_active.list file of baseline formula and active areas"
    sys.exit(1)

input_FUNCTION = input("Enter type of function:\n\
1 - Pearson7A\n\
2 - Pearson7\n\
3 - PseudoVoigt\n\
4 - Voigt\n\
5 - Lorentzian\n\
6 - Gaussian\n\
")
if input_FUNCTION == 1:
    FUNCTION = 'Pearson7A'
elif input_FUNCTION == 2:
    FUNCTION = 'Pearson7'
elif input_FUNCTION == 3:
    FUNCTION = 'PseudoVoigt'
elif input_FUNCTION == 4:
    FUNCTION = 'Voigt'
elif input_FUNCTION == 5:
    FUNCTION = 'Lorentzian'
elif input_FUNCTION == 6:
    FUNCTION = 'Gaussian'
else:
    print "Inappropriate value"
    sys.exit(1)

def create_cfityk_input(active):
    base = active.readline()
    myfile = ""
    while True:
        line = active.readline()
        if line == "":
            break

        x = line.split()
        x1 = x[0]
        x2 = x[1]
        myfile += "a = ("+ x1+ " < x < "+ x2+ ")\n"
        myfile += "%p = guess" + FUNCTION + "\n"
        myfile += "fit\n"
        myfile += "info+%p >> ./peaks\n"
        myfile += "delete %p\n\n"
    active.close()
    return myfile, base

def fit_one_file(input_file):
    try:
        os.remove('peaks')
    except:
        pass
    myfirstlines = '@0<' + input_file + "\n" + base + "\n"

    cfityk_input = open('cfityk_input', 'w')
    cfityk_input.write(myfirstlines)
    cfityk_input.write(fit_section)
    cfityk_input.close()

    os.system("cfityk < cfityk_input")  # запускаем fityk
    os.remove("cfityk_input")  # удаляю входной файл для fityk - больше не нужен

    # регулярные выражения для выделения положения центра,
    # площади под кривой и ширины на полувысоте
    pattern_center = r"center.+=\s(.*)\[auto\]"
    center_re = re.compile(pattern_center)

    if FUNCTION == 'Voigt' or FUNCTION == 'PseudoVoigt' \
        or FUNCTION == 'Pearson7' or FUNCTION == 'Gaussian' \
        or FUNCTION == 'Lorentzian':
        pattern_area = r"Area:\s(.*)"
    elif FUNCTION == 'Pearson7A':
        pattern_area = r"area.+=\s(.*)\[auto\]"
    area_re = re.compile(pattern_area)

    pattern_fwhm = r"FWHM:\s(.*)"
    fwhm_re = re.compile(pattern_fwhm)

    if FUNCTION == 'Pearson7A' or FUNCTION == 'Pearson7' \
           or FUNCTION == 'PseudoVoigt' or FUNCTION == 'Voigt':
        complex_functions = 1
        pattern_shape = r"shape.+=\s(.*)\[auto\]"
        shape_re = re.compile(pattern_shape)

    # открываю вывод cfityk
    buff = ""
    peaks_params = open('peaks', 'r')
    while True:
        line = peaks_params.readline()
        if line == "":
            break
        # создаем буфер из выходного файла
        buff += line
    peaks_params.close()

    param_center = center_re.findall(buff)
    param_area = area_re.findall(buff)
    param_fwhm = fwhm_re.findall(buff)
    if complex_functions == 1:
        # функции Pearson7A, Pearson7, PseudoVoigt, Voigt
        param_shape = shape_re.findall(buff)

    outfile.write('%s\t %s\n' % (input_file, FUNCTION) )
    outfile.write("N\tcenter\tarea\tFWHM\tshape\n")

    center, area, fwhm, shape = [], [], [], []
    for i in range(len(param_center)):
        center.append(float(param_center[i]))
        try:
            area.append(float(param_area[i]))
        except:
            pass
        fwhm.append(float(param_fwhm[i]))
        if complex_functions ==1:
            shape.append(float(param_shape[i]))

        outfile.write('%g\t%0.4f\t%0.1f\t%0.3f\t%0.3f\n' \
                      % (i, center[i], area[i], fwhm[i], shape[i]) )

    outfile.write("\n")

    pattern_name = r'(\S+)\..*$'
    name_re = re.compile(pattern_name)
    nameWithoutExtension = name_re.findall(input_file)[0]

    center_outfile.write('%s\t' % nameWithoutExtension)
    for i in range(len(center)-1):
        center_outfile.write('%0.4f\t' % center[i])
    center_outfile.write('%0.4f\n' % center[-1])

(fit_section, base) = create_cfityk_input(active)

for i in range(1, len(sys.argv)):
    """ фит всех входных файлов, кроме собственно скрипта """
    fit_one_file(sys.argv[i])

outfile.close()
center_outfile.close()

Запускаем командой:

python script_fityk.py file1.dat file2.dat file3.dat

На выходе получим файлы outfile.list формата:

имя_файла профильная_функция
номер_пика  положение_центра  площадь_под_кривой  ширина_на_полувысоте форма(если есть)

и center.out:

имя_файла положения_центров_пиков
Меня больше всего интересуют положения центров, поэтому я их и выделил в отдельный файл, хотя ничто не мешает сделать подобное для площадей пиков и полуширин. Можно это же сделать всё и средствами командной оболочки bash, обработав файл outfile.list.

Делаем скрипт исполняемым

Для того, чтобы запускать скрипт из любой директории, нужно сделать его исполняемым:

chmod u+x script_fityk.py

и положить его куда-нибудь, где хранятся в домашней директории бинарники. У меня это ~/bin. Эта директория должна быть в $PATH, для этого в файле ~/.bashrc должны быть строки типа:

export PATH=$PATH:$HOME/bin

то есть к уже существующей переменной окружения $PATH добавляем ещё одну директорию. После этого обновите оболочку посредством перезапуска терминала или

source ~/.bashrc

На этом я заканчиваю описание скрипта, существенно упрощающего мне жизнь. Пожелания и предложения приветствуются! Далее попробую использовать положения рефлексов для определения параметров элементарной ячейки структуры.


Читать далее…