Automation01 сентября 20261316 слов

Dynamo и числа: AsDouble, футы и почему размеры не сходятся

AsDouble возвращает футы, AsValueString — строку по настройкам проекта. Разбираем внутренние единицы Revit, UnitTypeId, округление и выгрузку размеров в мм.

Dynamo и числа: AsDouble, футы и почему размеры не сходятся

В модели дверь шириной 3000. Скрипт выводит 9.842519685039369. Ошибки нет, всё честно: Revit хранит длины в футах, а 3000 / 304.8 как раз и есть 9.8425.

На этом месте застревает каждый, кто впервые открыл узел Python Script и дёрнул AsDouble(). Дальше начинается интереснее: округление, накопление погрешности, запятая вместо точки в русской локали и спецификация, где объём отличается от правильного в 35 раз. Разберём по порядку, с кодом.

Что Revit хранит внутри

Отображение на экране и хранение в базе — разные вещи. Настройки единиц проекта (вкладка «Управление» → «Единицы проекта») меняют только показ. Внутри всегда одно и то же:

Коэффициенты, которые стоит держать в голове: 304.8 мм в футе, 0.09290304 м² в квадратном футе, 0.028316846 м³ в кубическом.

Отсюда классика жанра. Забыли пересчитать объём — цифры в ведомости бетона отличаются в 35 раз. Умножили длину на 304.8 дважды — получили километры вместо метров. И самое обидное: записали в параметр 3000, имея в виду миллиметры, а Revit прочитал 3000 футов и построил элемент длиной 914 метров.

AsDouble, AsValueString, AsString: чем отличаются

Три метода параметра, которые путают чаще всего.

AsDouble() возвращает число во внутренних единицах. Для длины — футы. Работает только если у параметра StorageType = Double, иначе вернёт мусор или ноль.

AsValueString() возвращает строку ровно в том виде, в каком значение показано в интерфейсе: «3000» или «3000,0 мм», в зависимости от настроек единиц проекта. Удобно для отчёта, опасно для вычислений.

AsString() для числового параметра вернёт None. Он для текстовых. Отсюда любимая ошибка новичка: AttributeError: 'NoneType' object has no attribute 'strip'.

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

from Autodesk.Revit.DB import StorageType

def prochitat(p):
    if p is None or not p.HasValue:
        return None
    st = p.StorageType
    if st == StorageType.Double:
        return p.AsDouble()          # футы, радианы, кубофуты
    if st == StorageType.Integer:
        return p.AsInteger()         # в том числе Да/Нет: 1 и 0
    if st == StorageType.String:
        return p.AsString()
    if st == StorageType.ElementId:
        return p.AsElementId()
    return None

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

Конвертация: UnitUtils и UnitTypeId

С Revit 2021 единицы описываются через ForgeTypeId, и рабочая пара выглядит так:

from Autodesk.Revit.DB import UnitUtils, UnitTypeId

futy = param.AsDouble()
mm = UnitUtils.ConvertFromInternalUnits(futy, UnitTypeId.Millimeters)

# обратно, когда пишем в модель
param.Set(UnitUtils.ConvertToInternalUnits(3000.0, UnitTypeId.Millimeters))

Полезные значения: UnitTypeId.Millimeters, Meters, SquareMeters, CubicMeters, Degrees, Kilograms.

Старый код на DisplayUnitType.DUT_MILLIMETERS писался под Revit 2020 и раньше. В 2021 он помечен устаревшим, в 2022 из API убран, и скрипт падает с AttributeError: 'DisplayUnitType' object has no attribute ... или сообщением о неподдерживаемом типе. Если тащите библиотеку скриптов из старых версий, это первое, что придётся переписать.

Почему не просто делить на 304.8? Для длин разницы нет. Для площадей, объёмов и всего, что связано с массой и температурой, коэффициенты нетривиальны, и UnitUtils избавляет от их поиска. Плюс код читается: ConvertFromInternalUnits(v, UnitTypeId.CubicMeters) понятнее, чем v * 0.028316846.

Отдельная деталь — общие параметры типа «Число» (Number). У них нет единиц вообще, значение хранится как есть. Конвертировать их нельзя, иначе коэффициент армирования 0.85 превратится в 259.

Почему в ноде 3000, а в питоне 9.84

Дело в том, что штатные узлы Dynamo уже конвертируют значения. Element.GetParameterValueByName отдаёт число в единицах проекта, а вызов API внутри Python Script — во внутренних. Один и тот же параметр, два разных числа, и оба правильные.

Практический вывод: не смешивайте источники в одном графе. Либо всё читается узлами и дальше идёт как есть, либо всё читается через API и конвертируется на выходе. Гибрид даёт ошибку в 304.8 раза, которую замечают уже в напечатанной ведомости.

То же касается геометрии. Координаты точек, полученных из API, живут в футах, и складывать их с координатами из Dynamo-узлов без пересчёта нельзя.

Как устроен сам граф и куда вставлять питон, разбирал раньше: первый скрипт в Dynamo.

Округление и накопление ошибки

Правило одно: считать в футах, округлять только на выводе.

Соблазн округлить сразу понятен — числа страшные. Цена ошибки видна на простом примере. Берём 500 участков воздуховода, каждый по 1.2345 фута:

dliny = [1.2345] * 500

# плохо: округлили каждый кусок в мм, потом сложили
a = sum(round(UnitUtils.ConvertFromInternalUnits(d, UnitTypeId.Millimeters))
        for d in dliny)

# правильно: сложили в футах, конвертировали один раз, округлили в конце
b = round(UnitUtils.ConvertFromInternalUnits(sum(dliny), UnitTypeId.Millimeters), 1)

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

Второе следствие — сравнение. Два числа с плавающей точкой никогда не сравнивают знаком равенства:

# так нельзя
if dlina_a == dlina_b:
    ...

# так можно: допуск 0,5 мм, переведённый в футы
DOPUSK = 0.5 / 304.8
if abs(dlina_a - dlina_b) < DOPUSK:
    ...

У самого Revit есть встроенный порог: doc.Application.ShortCurveTolerance — примерно 1/256 фута, около 0.8 мм. Короче этого линию он просто не создаст, и попытка кончится исключением.

Локаль: запятая, которая ломает всё

Самый неочевидный баг в русском Revit. AsValueString() на длине вернёт что-то вроде 3 000,00, где разделитель дробной части — запятая, а разделитель тысяч — неразрывный пробел (\xa0).

s = param.AsValueString()          # '3 000,00'
float(s)                           # ValueError: could not convert string to float

Чинится в одну строку, но знать про это надо заранее:

def v_chislo(s):
    if not s:
        return None
    s = s.replace('\xa0', '').replace(' ', '').replace(',', '.')
    s = ''.join(c for c in s if c.isdigit() or c in '.-')
    return float(s) if s else None

Отсюда общее правило обмена: AsValueString годится для того, что человек прочитает глазами. Для расчётов, выгрузок и сверок берётся AsDouble с явной конвертацией. Настройки единиц у соседнего файла могут отличаться, и тогда одна и та же выгрузка в понедельник даст миллиметры, а в среду сантиметры.

Обратная операция — SetValueString("3000") — разбирает строку по настройкам проекта. Удобно, когда значение пришло от пользователя из диалога, а не из расчёта.

Выгрузка в спецификацию

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

stroki = []
for e in UnwrapElement(IN[0]):
    p_dl = e.LookupParameter("Длина")
    p_ob = e.LookupParameter("Объем")
    dlina_mm = UnitUtils.ConvertFromInternalUnits(p_dl.AsDouble(), UnitTypeId.Millimeters) if p_dl else 0
    obem_m3 = UnitUtils.ConvertFromInternalUnits(p_ob.AsDouble(), UnitTypeId.CubicMeters) if p_ob else 0
    stroki.append([
        e.Id.IntegerValue,
        round(dlina_mm, 1),      # мм с одним знаком
        round(obem_m3, 3),       # м³ с тремя
    ])
OUT = stroki

Точность в 0.1 мм и 0.001 м³ покрывает практически любую форму. Больше знаков — таблица становится нечитаемой, меньше — итоги перестают сходиться с моделью.

Как довести такую выгрузку до готовой формы заказчика, расписано в статье про спецификации Revit в Excel.

Проверка себя за минуту

Прежде чем гнать скрипт по всей модели, возьмите один элемент с известным размером. Стену длиной ровно 6000 мм, круглый воздуховод диаметром 200, плиту толщиной 200. Выведите по нему все три величины рядом: сырое значение из AsDouble, результат конвертации и строку из AsValueString.

Дальше сверяете глазами. Сырое 19.685 и конвертированное 6000 при показанных «6000» — цепочка собрана правильно. Конвертированное 19.685 означает, что конвертацию где-то потеряли по дороге. Конвертированное 1828800 — что применили её дважды.

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

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

Где это не поможет

Конвертация не чинит неправильно заданный тип параметра. Если общий параметр создан как «Текст», а туда записывают размеры, никакой UnitUtils не спасёт: там строка, и в ней рано или поздно окажется «3000 (уточнить)».

Не поможет и там, где параметр вычисляемый. Формула в семействе считается по своим правилам, и запись в результат просто не пройдёт: параметр будет помечен как read-only, а Set() выбросит исключение о попытке изменить неизменяемое значение.

Углы отдельная песня. Радианы конвертируются штатно, но поворот 90° внутри семейства и поворот экземпляра в проекте — разные величины, и путать их UnitUtils не мешает.

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

Короткие ответы

Почему AsDouble возвращает 9.84 вместо 3000? Потому что внутренние единицы Revit — футы. 3000 мм / 304.8 = 9.8425. Конвертируйте через UnitUtils.ConvertFromInternalUnits.

Чем AsValueString отличается от AsDouble? Первый отдаёт строку в единицах проекта и годится для показа человеку, второй — число во внутренних единицах и годится для расчётов.

Как узнать, в каких единицах показан параметр? doc.GetUnits().GetFormatOptions(SpecTypeId.Length).GetUnitTypeId() вернёт текущую настройку длины для документа.

Работает ли старый код с DisplayUnitType? В Revit 2020 и раньше да, начиная с 2022 нет. Заменяется на UnitTypeId без изменения остальной логики.

Почему сумма длин не совпадает со спецификацией Revit? Чаще всего из-за раннего округления в скрипте. Складывайте во внутренних единицах, округляйте один раз на выводе.

Что сделать прямо сейчас

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

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

Нужен похожий BIM/AI процесс?

Напишите в Telegram или на email — разберём задачу и предложим архитектуру решения.

Telegram Оставить заявку