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

Стандартные ноды

Всё, что вы делаете в Динамо, — это так или иначе обработка данных в списках, потому что всю информацию из модели вы получаете в виде списка.

В Динамо есть свои стандартные ноды для работы со списками. С их помощью можно либо создать свои списки, либо обработать списки. Давайте глянем, что есть.

Блок нодов List
Блок нодов List

Возьму два нода — Range (диапазон) и Sequence (последовательность). Результат они выдают практически одинаковый, но есть разница. На Range подаётся: начало диапазона, конец диапазона и шаг. На Sequence подаётся: начало последовательности, количество элементов в ней и шаг. В результате я получаю разные списки, хотя подаю одинаковые данные.

Нод Рэндж воспринимает данные так: мне нужно взять начало диапазона, конец диапазона и создать ряд чисел с заданным шагом между этими числами. То есть он видит начало и конец и между ними вставляет данные с указанным шагом. Нод Сэквенс видит только начало, шаг и то, сколько надо добавить ещё элементов. В итоге из одних и тех же данных получаются разные списки.

С помощью этих нодов я по сути создал новые списки. В зависимости от нода результат разный
С помощью этих нодов я по сути создал новые списки. В зависимости от нода результат разный
Если меняю шаг, то в Секвенсе количество элементов в списке не изменилось, так как количество задаётся изначально, а Рэндж засовывает данные между указанным диапазоном, поэтому количество элементов растёт при уменьшении шага
Если меняю шаг, то в Секвенсе количество элементов в списке не изменилось, так как количество задаётся изначально, а Рэндж засовывает данные между указанным диапазоном, поэтому количество элементов растёт при уменьшении шага

Возьму теперь пару нодов из блока Inspect. Верхний List.Count на скриншоте считает количество элементов в списке, а нижний List.GetItemAtIndex получает элемент по указанному индексу. Нод-счётчик выдал значение 11, потому что в списке 11 элементов, а их индексы — это номера в серых прямоугольниках, и счёт начинается всегда с 0. Соответственно, элемент с индексом 5 — это буква «f». Хоть она и шестая в списке, её индекс — 5, так как отсчёт идёт от нуля. Не забывайте об этой особенности.

Создал список — обработал список
Создал список — обработал список

Получение списков из модели

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

Duct — воздуховоды. Слева индексы позиций в списке, справа в зелёном — айдишники элементов в Ревите
Duct — воздуховоды. Слева индексы позиций в списке, справа в зелёном — айдишники элементов в Ревите

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

Значения полученного списка соответствуют исходному
Значения полученного списка соответствуют исходному

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

Чтобы воссоздать такую ситуацию, я воспользуюсь нодом Group by key — сгруппировать по значению. Здесь я подаю список воздуховодов на вход List, а их размер — на вход Key, то есть воздуховоды сгруппируются на подсписки по сечениям. В результате я получу список из подсписков воздуховодов с одинаковыми сечениями.

Сгруппировал воздуховоды по сечениям
Сгруппировал воздуховоды по сечениям

Наверное, стрелочки не очень понятны, поэтому поясню словами. У меня есть 4 вида сечений: ø100, 300х400, 400х300 и 550х100. Воздуховоды сгруппировались по подспискам так, что в первом подсписке идут все воздуховоды с диаметром 100, во втором подсписке те, у которых сечение 300х400, в третьем — 400х300, в четвёртом — 550х100.

Воздуховоды и их сечения
Воздуховоды и их сечения

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

Таким образом вся наша работа в Динамо с данными из Ревита по сути представляет собой обработку списков либо самих элементов, либо значений из их параметров.

Переплетения

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

Вот у меня есть список из 9 чисел и список из 6 чисел.

На блок суммы подаю два списка, важный момент — они разной длины
На блок суммы подаю два списка, важный момент — они разной длины

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

В данном примере у нас явно проблема: списки разной длины, а значит шестой и далее элементы первого списка останутся без пары. Как же быть? Использовать переплетения. Для этого нужно нажать правой кнопкой мыши по блоку суммы и выбрать нужное.

Виды переплетений у нода
Виды переплетений у нода

Кратчайший — используется по умолчанию, чаще всего он скрывается за выбором Авто. В кратчайшем переплетении элементы двух списков будут попарно взаимодействовать, в нашем примере — суммироваться. Когда один список закончится, то и суммирование прекратится. На скриншоте ниже пример такой суммы, обратите внимание на правый нижний угол нода суммы, там значок | — это указатель на кратчайшее переплетение.

Посуммировали, конечный список равен по длине самому короткому
Посуммировали, конечный список равен по длине самому короткому

Самый длинный — в этом варианте при разной длине списков последний элемент короткого провзаимодействует с более длинной частью длинного. Самое длинное переплетение обозначается на ноде символами || На картинке ниже будет понятнее. На ней я показал, какие элементы и из какого списка суммируются. В скобочках указатель на список (1 — длинный, 2 — короткий).

Как работает сумма при самом длинном переплетении
Как работает сумма при самом длинном переплетении

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

Векторное произведение — при этом методе берётся первый элемент из первого списка, взаимодействует со всеми элементами второго списка, из этих данных создаётся подсписок. Потом берётся второй элемент и взаимодействует с каждым элементом второго списка. Создаётся второй подсписок. И такой цикл повторяется до тех пор, пока все элементы первого списка не провзаимодействуют с элементами второго. Каждый с каждым. Обозначается на ноде векторное произведение как XXX. Где-то улыбнулся Вин Дизель.

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

Результат суммы при векторном переплетении
Результат суммы при векторном переплетении

Здесь у нас берётся первый элемент первого списка — 0 — и суммируется со всеми значениями второго списка. Получаем первый подсписок. Потом взяли 1 из первого списка, сложили со всеми элементами второго списка. Получили второй подсписок. И так далее. Просто внимательно посмотрите на числа и увидите закономерность.

Теперь соединю на ноде суммы списки наоборот, второй список станет первым, а второй — первым. Просто на первый вход суммы подам второй список, а на второй вход — первый. В данном случае от перестановки слагаемых кое-что зависит.

Пересуммировал списки векторно в другом порядке
Пересуммировал списки векторно в другом порядке

Теперь у нас взаимодействие происходит наоборот. Берётся первый элемент второго списка и суммируется с каждым элементом первого. В итоге получаем 9 значений (помните, что нумерация идёт от 0), из них формируется первый подсписок. Потом берём второй элемент из второго списка, суммируем его по-очереди с каждым элементом первого списка. Получаем второй подсписко в результате. И так далее.

Отличие в том, что в первом случае у нас получилось 9 списков по 6 значений в каждом, а во втором — 6 подсписков по 9 значений в каждом. Однако суммарное количество элементов в обоих случаях одинаковое — 54, это значение легко получить умножением длины двух списков. Это и есть векторность, перемножение: 9 × 6 = 54.

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

Результат суммы первого списка со вторым при векторном переплетении
Результат суммы первого списка со вторым при векторном переплетении
Результат суммы второго списка с первым при векторном переплетении
Результат суммы второго списка с первым при векторном переплетении

В своей практике чаще всего использую короткое переплетение и разные уровни списков. Про них и поговорим.

Уровни списков

В примере с числами и векторным переплетением выше у нас есть два типа списков: те списки, которые я подаю на нод суммы состоят из конечных элементов, просто чисел. А тот список, что я получаю, состоит не просто из элементов, а из списков с элементами. То есть второй тип — это список списков или список из подсписков.

Из-за этого есть смысл говорить об уровнях списков и их иерархии. В Динамо такая многоуровневость обозначается через @Lx, где x — номер уровня. @Lx обозначает at level x, на уровне икс.

Уровни в списке
Уровни в списке

На скриншоте выше @L1 — это уровень отдельных элементов списка. @L2 — уровень подсписков. @L3 — это самый верхний уровень, список, который состоит из списков. Если бы мы взяли и засунули весь этот список в ещё один список, то появился бы следующий уровень @L4 — уровень списка из списков с подсписками. И так до бесконечности.

В Динамо списки обрабатываются сверху вниз. Это означает, что по умолчанию берётся самый верхний уровень — тот, у которого максимальное число при L, в нашем примере это L3, то есть список из списков.

Давайте посмотрим обработку списка нодом List.Count. Он считает длину списка, то есть количество элементов в списке. Поскольку у нас не просто список, а список из списков, то есть несколько уровней, то и обработать нодом список можно с разным результатом.

Если я просто подам список на List.Count, то получу вот такой результат:

Счётчик выдал число 9
Счётчик выдал число 9

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

Чтобы задать уровень в ноде, нужно нажать на стрелочку вправо > на входе нода и выбрать нужный уровень.

Включаем уровни у нода
Включаем уровни у нода

Рядом с текстом на входном ноде появляется указатель уровня. Обычно появляется @L2. Его и оставлю, вновь запущу скрипт и посмотрим результат.

Счётчик выдал длину подсписков
Счётчик выдал длину подсписков

Теперь мы сказали Динамо: «Возьми список из списков и провались до уровня L2, то есть до списков, из которых состоит весь список. Когда провалишься, то посчитай, сколько там элементов в каждом списке». В итоге мы получили длину каждого подсписка и результат в виде списка из значений. Получили 9 значений.

Если я поменяю местами входные списки на ноде суммы, то счётчик пересчитает итог. Теперь у нас будет 6 списков. А так как в каждом из них одинаковое количество значений, то и длины будут одинаковыми — по 9 штук каждый. Вспоминайте, что при векторном переплетении важен порядок входа данных.

Счётчик пересчитал длины подсписков с новыми данными
Счётчик пересчитал длины подсписков с новыми данными

Если я поставлю в ноде Count уровень L1, то результат будет таким:

Считаю длины списка на уровне L1
Считаю длины списка на уровне L1

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

Кроме включения уровня, при нажатии на > у нода можно выбрать вторую галочку — Сохранять структуру списков. Эта опция позволяет разбить данные на такие же подсписки, какие были в изначальном списке.

Структура списка наследовалась
Структура списка наследовалась

Нод обрабатывает данные так, как вы ему сказали при задании уровня, а результат просто группирует так же, как были сгруппированы исходные данные. На саму обработку данных это никак не влияет.

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

Для этого есть нод List.Flatten. Он проходится по списку и тупо убивает все уровни, оставляя в конечном счёте один список из конечных элементов. Если его подать на нод Count, то получим нашу длину в 54 элемента. Довольно регулярно вы будете пользоваться этим нодом.

Flatten выровнял список, то есть убрал все уровни, оставив список из конечных элементов
Flatten выровнял список, то есть убрал все уровни, оставив список из конечных элементов

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

Часто используемые ноды

Здесь расскажу про ноды, которые часто применяю в работе, это субъективный список. Название и короткое описание с примером.

Блок Generate

List.Create — создание нового списка. Каждый элемент становится отдельным элементом списка. В примере ниже объединяю в список просто число и список из чисел, получается вот такая неровная многоуровневая структура.

List.Create в деле
List.Create в деле

Нод полезен, если нужно какие-то данные, которые не были списком, привести к списку.

List.Cycle — создаёт список из повторяющихся элементов указанное количество раз. Полезно при создании наименований с одинаковыми префиксами или суффиксами.

Бойся тушканчика
Бойся тушканчика

Итоговый список одноуровневый. Если нужно создать список из подсписков с повторяющимися элементами, то используйте List.OfRepeatedElements. Редко использую.

List.Join — объединяет несколько списков в один, не делит каждый список на подсписок.

Но суслика бойся ещё больше
Но суслика бойся ещё больше

Если бы я подал два эти списка на List.Create, то получил бы два подсписка в одном списке, а тут это просто один список.

Основной минус нодов List.Create и List.Join — они работают с фиксированным количеством входных списков, которое пользователь задаёт плюсиком или минусом. Если у вас меняется количество списков на входе, то эти ноды сработают плохо, такие вариативные задачи нужно решать уже с помощью кода в Питоне.

Блок Inspect

List.Count — уже упоминался выше, выдаёт длину списка (количество элементов в списке).

List.FirstItem и LastItem — выдают соответственно первый и последний элемент в списке.

Чаще всего нужен FirstItem
Чаще всего нужен FirstItem

List.GetItemAtIndex — получить элемент из списка по индексу (порядковому номеру элемента в списке).

GetItemAtIndex
GetItemAtIndex

Как правило, самое сложное обычно — получить этот самый индекс. Сделать это можно с помощью нода List.IndexOf, однако он выдаёт индекс первого совпавшего элемента, а не список индексов всех совпавших элементов.

У меня там ещё куча сусликов, но выдаёт мне индекс только самого первого
У меня там ещё куча сусликов, но выдаёт мне индекс только самого первого

List.MaximumItem — и без примера понятно, нод выдаёт самое большое значение в списке. Его симметричный нод — List.MinimumItem.

List.SetDifference и SetIntersection — ноды соответственно для выявления различий и совпадающих элементов в двух списках.

SetDifference выдаст элементы из первого списка, которых нет во втором, а SetIntersection только общие для обоих списков элементы
SetDifference выдаст элементы из первого списка, которых нет во втором, а SetIntersection только общие для обоих списков элементы

List.UniqueItems — нод выдаёт из списка только уникальные значения, то есть убирает все повторы.

Остались только уникальные суслик и тушкан
Остались только уникальные суслик и тушкан

Блок Modify

Тут всего два нода я использую прям часто.

List.Flatten уже упоминался в статье выше, а нод List.FilterByBoolMask подробно разбирается в отдельной статье про фильтрацию данных.

Блок Organize

List.GroupByKey — нод группирует данные по одинаковым значениям. Например, берём список труб из проекта, получаем для каждой трубы имя системы. Имя системы в данном случае — это ключи (key), которые мы подаём на соответствующий вход нода. В результате получаем список из подсписков труб с одинаковым именем системы, и список уникальных ключей. В этом же порядке, как указаны ключи, и сгруппировались элементы.

Сгруппировал трубы по именам системы
Сгруппировал трубы по именам системы

Важный момент: количество ключей должно совпадать с количеством элементов, которые хотим сгруппировать.

List.Sort — нод упорядочивает элементы по алфавиту или по возрастанию числа. Всё просто, обычная сортировка, даже пример прилагать не буду.

List.SortByKey — чуть более сложная сортировка, так как элементы сортируются не по самим себе, а по ключам. Например, можно отсортировать все трубы по их длинам. Подаём на вход со списком трубы из проекта, на ключи — длины этих труб, в итоге получаем упорядоченные по длинам трубы и список длин, по которым производилась сортировка.

Сортировка труб по длинам
Сортировка труб по длинам

List.Transpose — нод для транспонирования данных. Транспонирование — это по сути разворот таблицы, когда строки становятся заголовками, а заголовки — новыми строками. В Экселе есть специальный режим для копирования и вставки с транспонированием, выглядит операция вот так:

Транспонировал таблицу в Экселе
Транспонировал таблицу в Экселе

В Динамо транспонирование выглядит немного сложнее, но суть та же.

В зависимости от уровня результат будет отличаться
В зависимости от уровня результат будет отличаться

Чаще всего я пользуюсь транспонированием, либо чтобы соединить данные из нескольких подсписков, либо чтобы поделить на отдельные подсписки элементы. На скриншоте выше Transpose без уровня делает первую операцию, а с уровнем — пилит всё на отдельные подсписки.

На этом всё. Надеюсь, тему объяснил понятно, так как легко запутаться во всех этих списках и подсписках. Практика — лучший учитель.

Полезные материалы

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

Что такое списки — вводная статья про то, что такое списки, здесь есть хороший пример про переплетение данных.

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

Списки списков — здесь изучите уровни в списках, советую ознакомиться.

Многомерные списки — тут происходит какая-то дичь, новичкам можно не читать, только страшно будет. Я сам прокрутил и закрыл, не вижу особой пользы для себя, так как не работаю с геометрией.

Обновления статей удобно получать в Телеграм-канале «Блог Муратова про Revit MEP». Подписывайтесь и приглашайте коллег. Можно обсудить статью и задать вопросы в специальном чате канала.

Отблагодарить автора

Я много времени уделяю блогу. Если хотите отблагодарить меня, то можете сделать небольшой подарок (именно подарок, такой перевод не облагается налогом).