Тонкости построения сетевых моделей в Python
Что является основным инструментом, который использует руководитель при управлении проектом? Принято считать, что основным инструментом руководителя проекта является календарный план, в основе которого лежит сетевая модель работ по проекту. Однажды мне довелось реализовать сетевую модель работ на языке Python (код и описание здесь). Ниже приведены уроки, извлеченные по результатам проделанной работы.
Сначала создай работы, потом устанавливай связи
При построении сетевой модели часто возникает вопрос, в каком порядке создавать работы и устанавливать связи между ними? Наиболее очевидным является двухэтапный подход – сначала создаются все работы модели, затем между ними устанавливаются связи. Такой подход позволяет избежать ошибок типа KeyError: ‘101’, возникающих при параллельном выполнении этих двух операций, когда система пытается установить связь с работой, которая еще не была создана.
Конечно, суммарное время выполнения двух последовательных операций по созданию работ и установке связей между ними может быть оптимизировано за счет использования алгоритма, в котором эти операции выполняются параллельно. Однако даже на больших моделях с десятком тысяч работ последовательные алгоритмы работают достаточно быстро. Поэтому в условиях временных ограничений на реализацию проекта вполне можно обойтись классическим двухэтапным подходом.
Пересчитывай модель после её построения
Стоит ли выполнять пересчет всякий раз, когда происходит установка связи между работами при построении сетевой модели? С одной стороны, постоянный пересчет позволяет держать модель в актуальном состоянии. С другой, пересчет увеличивает время ее построения.
Для сравнения были реализованы две функции:
- build_model_by_method() – построение с пересчетом модели;
- build_model_by_assignment() – построение без пересчета модели.
from predict import Activity import xml.etree.ElementTree as ET import sys import timeit from timeit import Timer # вычисляем значение параметра задачи def get_child(child, activity_field): text = child.find(activity_field).text if text is None: return None return int(text) # строим модель с использованием метода пересчета def build_model_by_method(filename): sys.setrecursionlimit(10000) f = open(filename,'r') tree = ET.parse(f) root = tree.getroot() schedule = <> next = <> for child in root.findall('Activity'): start_date = get_child(child,'start_date') finish_date = get_child(child,'finish_date') duration = get_child(child,'duration') not_early_date = get_child(child,'not_early_date') a = Activity(id, start_date, finish_date, duration, not_early_date) schedule[id] = a next_activity = '' if child.find('next_activity').text is None else child.find('next_activity').text next[id] = next_activity for key in schedule: if nextКак составлять сетевую модель != '': for next_id in nextКак составлять сетевую модель.split(';'): scheduleКак составлять сетевую модель.append_next(schedule[next_id]) sys.setrecursionlimit(1000) # строим модель без использования метода пересчета def build_model_by_assignment(filename): f = open(filename,'r') tree = ET.parse(f) root = tree.getroot() schedule = <> next = <> for child in root.findall('Activity'): start_date = get_child(child,'start_date') finish_date = get_child(child,'finish_date') duration = get_child(child,'duration') not_early_date = get_child(child,'not_early_date') a = Activity(id, start_date, finish_date, duration, not_early_date) schedule[id] = a next_activity = '' if child.find('next_activity').text is None else child.find('next_activity').text next[id] = next_activity for key in schedule: if nextКак составлять сетевую модель != '': for next_id in nextКак составлять сетевую модель.split(';'): scheduleКак составлять сетевую модель.next_activity.append(schedule[next_id]) # считаем скорость построения модели print('Test for 100 activities:') t1 = Timer("build_model_by_method('data/activity_100.xml')", "from __main__ import build_model_by_method") print("build_model_by_method", t1.timeit(number = 1000)) t2 = Timer("build_model_by_assignment('data/activity_100.xml')", "from __main__ import build_model_by_assignment") print("build_model_by_assignment", t2.timeit(number = 1000)) print('Test for 1000 activities') t3 = Timer("build_model_by_method('data/activity_1000.xml')", "from __main__ import build_model_by_method") print("build_model_by_method", t3.timeit(number = 1000)) t4 = Timer("build_model_by_assignment('data/activity_1000.xml')", "from __main__ import build_model_by_assignment") print("build_model_by_assignment", t4.timeit(number = 1000)) print('Test for 10000 activities') t5 = Timer("build_model_by_method('data/activity_10000.xml')", "from __main__ import build_model_by_method") print("build_model_by_method", t5.timeit(number = 1000)) t6 = Timer("build_model_by_assignment('data/activity_10000.xml')", "from __main__ import build_model_by_assignment") print("build_model_by_assignment", t6.timeit(number = 1000))
$ python network.py Test for 100 activities: build_model_by_method 1.7820062519999738 build_model_by_assignment 1.426311435999878 Test for 1000 activities build_model_by_method 18.998158786999966 build_model_by_assignment 14.216093206999858 Test for 10000 activities build_model_by_method 249.93449528199994 build_model_by_assignment 148.85600239800033
Как видно, чем больше работ, тем медленнее работает функция построения сетевой модели с использованием пересчета по сравнению с функцией, в которой пересчет не используется.
Контролируй глубину рекурсии
Сетевая модель проекта состоит из работ и связей между ними. В отсутствии дополнительной информации о сети для ее пересчета используются рекурсивные алгоритмы. Такие алгоритмы состоят в последовательном проходе по работам сети и пересчете их параметров (например, длительности, дат начала и окончания). Чем больше работ в сети, тем больше глубина рекурсии.
Известно, что в Python по умолчанию установлено ограничение на глубину рекурсии – не больше 1000 рекурсивных вызовов. Для получения этого ограничения предназначен метод getrecursionlimit() модуля sys.
>>> import sys >>> sys.getrecursionlimit() 1000
При работе с большими сетями, число работ в которых измеряется десятками тысяч, обычной ситуацией является превышение глубины рекурсии и, как следствие, возникновение ошибки типа RecursionError: maximum recursion depth exceeded in comparison. Ошибка приводит к остановке построения либо пересчета модели и падению системы.
Чтобы предотвратить ошибку, построить и пересчитать большую сеть, необходимо увеличить ограничение на глубину рекурсии. И в этом нам поможет метод setrecursionlimit() модуля sys.
>>> import sys >>> sys.setrecursionlimit(10000) >>> sys.getrecursionlimit() 10000
До какого значения требуется увеличить глубину рекурсии? Ответ на этот вопрос зависит от структуры сетевой модели. Если структура неизвестна или достаточно сложна, рекомендую устанавливать ограничение в значение, равное количеству работ в сети. Рекурсивный алгоритм проходится либо по всем либо по части работ. Поэтому глубина рекурсии не должна превысить количество работ в сети.
И напоследок…
Управление проектами – это модно. Но модно еще не значит эффективно. На рынке существуют программные решения по пересчету сетевых моделей, такие как Microsoft Project. Однако алгоритмы, зашитые в них остаются доступными только вендерам соответствующего программного обеспечения.
Настоящая статья написана на основе опыта разработки открытого модуля по построению и пересчету сетевых моделей проектов. Я надеюсь, что приведенные в статье извлеченные уроки будут полезны читателю как с теоретической, так и с чисто практической точки зрения. Если настоящая статья вызовет интерес, то я поделюсь новыми знаниями, которые возникнут в дальнейшем, по мере развития модуля.
1. Элементы построения сетевых моделей
В основе метода сетевого планирования и управления (СПУ) лежит построение графика, по своему виду напоминающего сеть (переплетение нитей и узелков), поэтому график и получил название сетевого.
Сетевой моделью называется отображение процессов, выполнение которых подчинено достижению одной или нескольких целей, с указанием взаимосвязей между этими процессами.
Сетевым графиком называется график производства работ с установленными расчётом сроками их выполнения. Сетевой график представляет собой графическое изображение сетевой модели с рассчитанными параметрами.
Элементами сетевой модели являются работа, событие и путь:
а) работа – это трудовой процесс, требующий затрат времени и ресурсов.
Название работы является минимальной информацией о работе, содержащейся в сетевой модели (например, отрывка котлована, возведение каркаса, устройство кровли, поставка оборудования и т.д.).
Работа на графике изображается сплошной стрелкой, направленной слева направо с указанием над стрелкой продолжительности работы.
Работа, которая требует лишь затрат времени, называется работа – ожидание. Ожидание на графике изображается пунктирной стрелкой с указанием над стрелкой её продолжительности (например, процесс твердения бетона или ожидание поставки материалов). Эти работы требуют только затрат времени.
Для отображения правильной технологической последовательности между работами применяется зависимость. Ни времени, ни ресурсов «зависимость» не требует. На графике зависимость изображают пунктирной стрелкой, продолжительность которой равна нулю. В литературных источниках зависимость называют фиктивной работой.
Итак, понятие «работа» может иметь три значения:
работа
работа – ожидание
зависимость
б) событие – это итог какой-нибудь деятельности (работы), происходящей мгновенно. Любая работа начинается и заканчивается событием.
Событие не потребляет ни времени, ни трудовых ресурсов, оно обозначает только факт начала и окончания одной или нескольких работ. Событие графически обозначается кружком, внутри которого ставится его номер, или может обозначаться буквами.
Событие, не имеющее непосредственно предшествующих работ, называется исходным, не имеющее непосредственно следующих работ – завершающим. Событие, не являющееся ни исходным, ни завершающим, называется промежуточным.
На рис. 1 событие 1 – исходное, событие 6 – завершающее, события 2, 3, 4, 5 – промежуточные.
Все работы комплекса по отношению друг к другу подразделяются на данную, предшест-вующую и последующую работы. Обозначение работ см. на рис. 2.
в) путь – это непрерывная технологическая последовательность работ от исходного события к завершающему.
На рис. 3 дан сетевой график из восьми работ, одной зависимости и шести событий. На графике можно выделить 7 путей:
1-й путь проходит по событиям 1, 2, 3, 4, 6;
2-й путь проходит по событиям 1, 3, 5, 6;
3-й путь проходит по событиям 1, 2, 4, 6;
4-й путь проходит по событиям 1, 2, 3, 5, 6;
5-й путь проходит по событиям 1, 2, 3, 4, 5, 6;
6-й путь проходит по событиям 1, 2, 4, 5, 6;
7-й путь проходит по событиям 1, 3, 4, 5, 6.
Зная продолжительность каждой работы tij , можно определить продолжительность любого пути сетевого графика.
Продолжительность пути определяется как сумма продолжительностей работ, составляющих этот путь:
Критический путь – это путь, имеющий максимальную продолжительность. Он определяет конечный срок строительства, это самый трудоемкий и неблагоприятный путь.
Подкритический путь – это путь, продолжительность которого близка к продолжительности критического пути.
На рис. 3 длина различных путей от исходного события до завершающего равна:
1-й путь Т1 = 5 + 10 + 14 + 9 = 38;
4-й путь Т4 = 5 + 10 + 2 + 3 = 20;
5-й путь Т5 = 5 + 10 + 0 + 3 = 32;
6-й путь Т6 = 5 + 7 + 0 + 3 = 15;
7-й путь Т7 = 12 + 14 + 0 + 3 = 29.
Первый путь имеет наибольшую продолжительность из всех путей, значит, он является критическим.
Критическим путь назван потому, что, во-первых, из всех путей сетевого графика только он определяет общую продолжительность строительства; во-вторых, он указывает на работы, которые являются ведущими для выполнения заданного комплекса работ. Работы, лежащие на критическом пути, называются критическими.
На рис. 3 критическими работами являются 1-2; 2-3; 3-4; 4-6.
На сетевом графике критический путь выделяют красной двойной или жирной линией.
В сетевом графике может быть несколько критических путей одинаковой продолжительности. Определение продолжительности (длины) критического пути и критических работ – одна из основных задач, решаемых в методе сетевого планирования и управления (СПУ).