Опыт применения D в химической лаборатории

Наверное, название этой статьи слишком громкое для того, о чем я собираюсь рассказать, но как говорится за неимением лучшего, воспользуемся тем, что есть под рукой в данный момент…

Итак, одно время я работал лаборантом на кафедре общей и физической химии в ЯРГУ им. П.Г. Демидова (как видите, я не программист по специальности) и тогда приходилось довольно часто заниматься рутинными расчетами, наиболее часто из которых встречался расчет массы некоторого вещества для приготовления раствора с заданной концентрацией в колбе определенного объема. Надо сказать, что в этой небольшой лаборатории имелся персональный компьютер под нужды преподавателей и лаборантом, а меж тем, при наличии такого средства автоматизации никому в голову не приходила мысль о переносе расчетов в компьютер и облегчения собственной жизни на работе. Мне такая перспектива не нравилась, а кроме того, постоянные расчеты (записи об уже проделанных, понятное дело, никем не велись, ибо считалось, что это не столь необходимо) съедали довольно значительное количество времени, которого и так не хватало на подготовку лабораторных…

В общем, мне надоело мириться с проблемой (а студентов заставлять проводить такие расчеты просто бессмысленно) и я решил что-то предпринять, дабы облегчить себе жизнь. Конечно, у меня существовало уже готовое решение по подсчетам на Python, но внезапно, в один из рабочих дней была обнаружена его весьма некорректная работа, а с учетом того, что реализация была сделана 2 года назад (и по совсем другому поводу) и того факта, что я давно отошел от Python, передо мной встала задача пересоздания плохо работающего (но работающего!) инструмента.

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

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

Регулярные выражения на тот момент времени еще не были освоены (по крайней мере, в D я их применять не умел) и тогда созрела идея делать разбор брутто-формулы (формула, которая не содержит ни круглых, ни квадратных скобок) самостоятельно, что является довольно крутой и интересной задачей…

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

import std.conv;
import std.stdio;

// шаблон для создания предикатов, проверяющих вхождение символа во множество символов
template stringPredicate(string name, string set) {
	const char[] stringPredicate = "bool " ~ name ~ "(T = string)(T source) {
       bool flag = false;
       string set = \"" ~ set ~ "\";
       string tmp = to!string(source);
       foreach (elem; tmp) {
          foreach (sym; set) {
            if (elem == sym) {
              flag = true;
              break;
            }
            else flag = false;
          }
       }
       return flag;
      }";
}

Создание трех предикатов уже дело абсолютно простое и занимает ровно три строки (не считая комментариев):

// строка только из цифр ?
mixin(stringPredicate!("isDigit", "0123456789"));

// строка только из английских букв в нижнем регистре ?
mixin(stringPredicate!("isLower", "abcdefghijklmnopqrstuvwxyz"));

// строка только из английских букв в верхнем регистре ?
mixin(stringPredicate!("isUpper", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"));

Эти предикаты пригодятся нам для поиска и опознания в формуле химических элементов и коэффициентов соответственно.

Как видно по коду, был использован параметризованный контекст (он же шаблон) с достаточно широкими полномочиями (в частности, обработка любых типов, для которых определен метод toString()) в сочетании с использованием примесей (mixin) для автоматической кодогенерации, однако, при внешней сложности, все довольно таки просто, если переписать содержимое шаблона и рассмотреть это как самостоятельный код (несмотря на то, что я еще только осваивал D к тому моменту, я уже провел пару экспериментов с шаблонами, чтобы немного пользоваться ими).

Теперь можно приступать к разбору строки, содержащей брутто-формулу.

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

// Ищем элемент в строке начиная с некоторой позиции
string findElement(string s, int pos) {
	string acc = "";
	if (isUpper(s[pos])) {
		acc ~= s[pos];
		for (int i = pos + 1; i < s.length; i++) {
			if (isUpper(s[i])) break;
			else {
				if (!isDigit(s[i])) acc ~= s[i];
			}
		}
	}
	return acc;
}

Помимо символов химических элементов, в формулах встречаются еще и коэффициенты, обозначающие количество каждого химического элемента, при этом, принято по умолчанию, что если после элемента отсутствует коэффициент, то количество элемента равно единице (пожалуйста, запомните этот факт, он потребуется в дальнейшей реализации!). Функция извлечения коэффициентов из строковой репрезентации формулы значительно проще, ведь для того, чтобы выделить коэффициент начиная с некоторой позиции, необходимо просто накапливать в строку-аккумулятор только те символы, которые соответствуют цифрам, а условие окончания накопления — нахождение любого символа, отличного от цифрового:

// Ищем коэффициент начиная с некоторой строки
string findCoeff(string s, int pos) {
	string acc = "";
	foreach (elem; s[pos .. $]) {
		if (isDigit(elem)) acc ~= elem;
		else break;
	}
	return acc;
}

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

Итак, простейший вариант словаря под нашу задачу выглядит так:

float[string] tableOfElements;
	
tableOfElements["H"] = 1.0;
tableOfElements["N"] = 14.0;
tableOfElements["C"] = 12.0;
tableOfElements["O"] = 16.0;
tableOfElements["S"] = 32.0;
tableOfElements["Cl"] = 35.5;
tableOfElements["Na"] = 23.0;

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

// Молярная масса
float molarMass(string formula, float[string] tableOfElements) {
	float index, molar_mass = 0.0;
	string elem, coeff, tmp;
	for (int i = 0; i < formula.length; i++) {
		elem = findElement(formula, i);
		if (elem != "") {
			try {
				tmp = formula[i + elem.length .. $];
				coeff = findCoeff(tmp, 0);
			} catch {
				coeff = "";
			}
			if (coeff != "") index = to!int(coeff);
			else index = 1;
			molar_mass += index * tableOfElements[elem];
		}
	}
	return molar_mass;
}

Работает это так: задаем две переменные index (хранит текущий коэффициент элемента) и molar_mass (хранит молярную массу, которая последовательно вычисляется для формулы), а затем проходим по всем символам формулы. Если встреченный символ представляет собой элемент (строка elem не является пустой), то следующим шагом является поиск коэффициента для элемента, начиная с позиции на которой был найден символ химического элемента плюс длина этого символа. Если коэффициент после элемента нашелся (строка coeff не пуста), то нужно преобразовать найденный коэффициент в число и присвоить его переменной index; в противном случае (вспомните, тот факт, который я упоминал в поиске коэффициентов) переменной index нужно присвоить единицу. Далее, воспользовавшись ассоциативным массивом с элементами и их молярными массами, добавляем в молярную массу вклад, вносимый найденным элементом и его количеством.

Как видите, это весьма просто, хотя на это я убил три дня напряженных размышлений и экспериментов!

Переходим, к … приготовлению растворов.

Известно, что раствор готовится в колбе определенного объема и имеет определенную молярную концентрацию (а также определенную химическую формулу, того соединения, которое растворено), что на D можно записать примерно так:

// Описываем раствор
struct Solution {
	string formula;
	float molarConcentration;
	float solutionVolume;
}

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

/*
 масса навески, необходимая для приготовления заданного объема раствора заданной
 молярной концентрации
 */
float molarConcentration(Solution s, float[string] tableOfElements) {
	return molarMass(s.formula, tableOfElements) * s.molarConcentration * s.solutionVolume;
}

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

// Повтор строки
string repl(T = string)(T source, uint n) {
	string tmp = to!string(source);
	string acc = tmp;
	if (n == 0) acc = tmp;
	else {
		while (n >= 1) {
			acc ~= tmp;
			n--;
		}
	}
	return acc;
}

// Таблица для приготовления растворов по известной молярной концентрации
void mcTable(Solution[] s, float[string] tableOfElements) {
	string sep = repl("-",77);
	foreach (solution; s) {
		float molar_mass = molarMass(solution.formula, tableOfElements);
		writeln(sep);
		writefln("| %20s | m = %6.2f | Cm = %6.2f | M = %6.2f | V = %6.2f  |", solution.formula, molarConcentration(solution, tableOfElements), solution.molarConcentration, molar_mass, solution.solutionVolume);
		
	}
	writeln(sep);
}

Для этого, нам потребовалась вспомогательная функция repl, повторяющая некоторую строку заданное количество раз и функция writefln, печатающая текст в заданном формате.

Таким образом, можно избавиться от химической рутины:

void main()
{
	float[string] tableOfElements;
	tableOfElements["H"] = 1.0;
	tableOfElements["N"] = 14.0;
	tableOfElements["C"] = 12.0;
	tableOfElements["O"] = 16.0;
	tableOfElements["S"] = 32.0;
	tableOfElements["Cl"] = 35.5;
	tableOfElements["Na"] = 23.0;
	tableOfElements["K"] = 39.0;

	Solution[] solutions = [
		Solution("NaCl", 0.5, 0.250),
		Solution("KSCN", 0.1, 0.250),
		Solution("Na2S", 1.0, 0.1),
		Solution("N2H8S2O8", 0.5, 0.5),
		Solution("KNO2", 0.1, 0.5),
		Solution("N2H4SO4", 0.25, 0.5),
	];

	mcTable(solutions, tableOfElements);
}

Что выглядит примерно так:

Как видите, D очень мне помог на работе :)

Добавить комментарий