Python DSL в AnimaGeo¶
Обычное Python-окружение для создания геометрических конструкций. DSL-кодом
может быть любой корректный код Python, а вызовы фабрик (Point, Midpoint,
Intersect, …) одновременно выполняются и регистрируются в графе зависимостей
Construction.
Статус: движок на основе
exec— единственный движок DSL.Точки входа:
scene.putCode(code)/scene.loadCode(filepath)scene.loadGGB(filepath)— GGB → exec черезggb_parserdsl.run(constr, code)— прямой вызовwith dsl.scope(constr): ...— привязка ContextVar для файлового режима
Содержание¶
- Быстрый старт
- Синтаксис
- Фабрики
- Формулы
- Имена элементов
- Доступ к полям
- Стили
- Что запрещено
- Что поддерживает exec-движок
- Текущие ограничения
Быстрый старт¶
С отдельным объектом Construction:
from animageo.geo.construction import Construction
from animageo.parsers import dsl
c = Construction()
dsl.run(c, """
A = Point(0, 0)
B = Point(4, 0)
C = Point(0, 3)
p, s1, s2, s3 = Polygon(A, B, C)
M = Midpoint(A, B)
style(M, stroke='#ff0000', size=10)
""")
Со сценой Manim (AnimaGeoScene):
class MyScene(AnimaGeoScene):
def construct(self):
self.putCode("""
A = Point(0, 0)
B = Point(3, 4)
M = Midpoint(A, B)
""")
self.addAllGeometry(show=True)
Фабрики также можно импортировать напрямую в файле .py, чтобы получать
подсказки IDE:
from animageo.parsers.dsl.namespace import Point, Midpoint, style
# Для прямых вызовов нужен активный Construction в ContextVar —
# см. dsl.registrar.set_current_construction либо используйте
# scene.putCode(code).
Синтаксис¶
Доступны все конструкции Python:
# циклы
for i in range(5):
p = Point(i, 0) # создаёт p, p_2, p_3, p_4, p_5
# условия
for i in range(10):
if i % 2 == 0:
p = Point(i, 0)
# функции
def triangle(prefix, side):
A = Point(0, 0, name=f"{prefix}_A")
B = Point(side, 0, name=f"{prefix}_B")
C = Point(side/2, side*0.866, name=f"{prefix}_C")
return A, B, C
triangle("t1", 3)
triangle("t2", 5)
# списковые включения тоже работают
radii = [r for r in range(1, 6)]
circles = [Circle(Point(0, 0, name=f"O_{i}"), r) for i, r in enumerate(radii)]
# математика — import не нужен: функции и константы math
# (pi, sqrt, …) доступны глобально в пространстве имён DSL
A = Point(sqrt(2), pi/2)
Фабрики¶
Любое имя в CamelCase, которому соответствует диспетчеризуемая функция в
animageo/geo/lib_commands.py, автоматически становится фабрикой. Ручная
регистрация не требуется.
Основные фабрики:
| Конструкторы | Команды |
|---|---|
Point, Line, Segment, Ray, Circle, Arc, CircleSector, Angle, Polygon, Vector, Conic, Function, ImplicitCurve |
Midpoint, Distance, Length, Radius, Center, Vertex, Focus, Intersect, AreCollinear, PerpendicularLine, Tangent, Polar, Rotate, Dilate, … (всего 100 фабрик команд, которые диспетчеризуются в 477 сигнатур для конкретных сочетаний типов аргументов в COMMAND_REGISTRY) |
Неизвестное имя вызывает NameError: ошибка всегда явная и никогда не
превращается в незаметную пустую операцию.
Формулы¶
Function, Conic, ImplicitCurve и Line принимают уравнение строкой.
Строка вида name(var) = expr — сокращённая запись Function:
f = Function("y = x^2 + 1")
c = Conic("x^2 + y^2 = 4")
h = ImplicitCurve("x^3 + y^3 = 3x y")
l = Line("y = 2x + 1")
f(x) = x^2 + 1 # f = Function("y = x^2 + 1")
g(t) = 2t + 1 # функция от t
Line читает строковый литерал как уравнение; Line(A, B), Line(s) и
другие формы строят прямую, как и раньше. Уравнение, которое не задаёт
прямую (Line("y = x^2")), оставляет элемент неопределённым.
Формула может ссылаться на объекты конструкции и тогда следует за ними:
числа и ползунки, логические значения, измерения, углы, координаты точек и
векторов (x(A), y(A)), другие функции (g(x)). Логическое значение —
условие в If(b, x, -x), а в остальных местах считается как 0 или 1
(y = x + b):
a = 2
A = Point(1, 3)
p(x) = a x^2 # следует за a
q = Function("y = x(A) + x") # следует за A
g(x) = x^2
r(x) = g(x - 2) + 1 # следует за g
k = Conic("(x - x(A))^2 + (y - y(A))^2 = 4")
m = Line("y = a x + 1") # следует за a
b = AreCollinear(A, Point(0, 0), Point(2, 6))
s = Function("y = If(b, x, -x)") # следует за b
Такая формула хранится как команда, к которой дописаны имена, на которые она
ссылается, — Function("y = a x^2", a), — и перечитывается с их текущими
значениями при каждой пересборке. Объекты должны существовать до того, как
записана формула; имя, определённое через само себя
(f = Function("y = f(x) + 1") при уже существующей f,
A = Midpoint(A, B)), вызывает ValueError о цикле зависимостей.
Синтаксис формул:
^(или**) — степень. Знак*можно опускать:2x,k x,2(x + 1),(x + 1)(x - 1),x (x + 1).x(A)без пробела перед скобкой — абсцисса точки A, поэтомуx(x + 1)— не произведение: пишитеx (x + 1)илиx*(x + 1).2x(A)— удвоенная абсцисса A.- Число перед скобкой — произведение:
k(x + 1)означаетk*(x + 1), если только число не названо как математическая функция:gamma(x + 1)— это Γ(x + 1). - Формула вызывает только математические функции:
sin,cos,tan,cot,sec,csc,asin,acos,atan,sinh,cosh,tanh,exp,log,ln,sqrt,cbrt,root,abs,sign,floor,ceiling,round,frac,min,max,gamma,erf, … — функции и неявные кривые понимают и написания GeoGebra (arcsin,sgn,ceil,lg,ld) иIf(cond, a, b). Любое другое имя, которое формула вызывает, — объект конструкции.round(x)округляет половину от нуля:round(2.5)— 3,round(-2.5)— −3. piиπ— это π,ℯ— число Эйлера.e— тоже число Эйлера в функциях и неявных кривых; в уравнении коники или прямой это обычное имя — пишите тамℯилиexp(1).2π,πx,2ℯ— произведения, как2x.- Типографские символы тоже читаются: знак минуса
−,·,⋅и×для умножения,÷для деления. - Функция, которая вызывает другую функцию с ограниченной областью определения, не определена там же, где и та.
- Уравнение коники или прямой должно быть многочленом в том виде, как
записано:
(x^2 + x)/x = y— не коника. - Текст формулы ограничен 4000 символами, а формула, требующая огромного
точного вычисления (
7^(9^9),(10^7)!), отклоняется.
Формула, которую не удалось прочитать или которая называет то, чего нет в конструкции, оставляет свой элемент неопределённым, а в журнал пишется предупреждение.
Имена элементов¶
Из переменной слева¶
В цикле — автоматическая уникализация¶
В функции — также с уникализацией¶
Тело def считается областью цикла:
Явное имя через name=¶
def triangle(prefix):
A = Point(0, 0, name=f"{prefix}_A") # элемент: <prefix>_A
B = Point(1, 0, name=f"{prefix}_B")
return A, B
Если передан name=, а имя уже занято, возникает ValueError: тихого
столкновения имён не будет. Python-переменная A всё равно привязывается к
proxy-объекту.
Распаковка кортежа для команд с несколькими результатами¶
a, b = Intersect(c, L) # две точки пересечения
p, s1, s2, s3 = Polygon(A, B, C) # многоугольник и три стороны
Трансформер добавляет в вызов _outputs=N, чтобы фабрика выделила нужное
количество имён.
Если нужна одна конкретная точка из нескольких пересечений, используйте индекс
с единицы: p = Intersect(c, L, index=1) или позиционную форму
p = Intersect(c, L, 1).
Порядок пересечений является частью публичного контракта. Первый элемент
распакованного кортежа соответствует Intersect(..., index=1), второй —
Intersect(..., index=2) и так далее. Для окружностей применяется эвристика,
похожая на GeoGebra: если окружность построена через уже известные точки, то
совпавшие с ними точки пересечения идут первыми в порядке входов окружности;
затем учитываются точки второго входного объекта, а оставшиеся пересечения
сохраняют внутренний детерминированный порядок. Он должен совпадать при импорте
.ggb, в Python DSL и в путях экспорта/JSXGraph.
Доступ к полям¶
Через proxy-объект, который возвращает фабрика:
A = Point(3, 4)
print(A.x, A.y) # 3.0, 4.0
print(A.coords) # [3., 4.]
circ = Circle(Point(0, 0), 5)
print(circ.center) # [0., 0.]
print(circ.radius) # 5
s = Segment(Point(0, 0), Point(3, 4))
print(s.start, s.end, s.length)
Полный список приведён в разделе Поля геометрических объектов.
Чтение всегда отражает актуальное состояние: после любого dsl.run поля
содержат текущие значения.
Стили¶
elem.style — это StyleProxy, унаследованный от dict. Одновременно
работают оба варианта API.
Через атрибуты¶
Как словарь — прежний API сохранён¶
A.style["stroke"] = "#ff0000"
A.style.get("stroke", "black")
"stroke" in A.style
for k, v in A.style.items(): ...
json.dumps(A.style) # продолжает работать
Групповые функции¶
Отсутствующий атрибут возвращает None, а не AttributeError. Это повторяет
семантику CSS: if A.style.stroke: означает «задан ли этот стиль?».
Что запрещено¶
На этапе преобразования возникает DSLSyntaxError с номером строки и столбца:
global x/nonlocal x;A += expr— составное присваивание;(A := expr)— walrus-оператор;A: int = 5— аннотированное присваивание со значением;A = B = expr— цепочка присваиваний;- имя слева, начинающееся с подчёркивания: такие имена зарезервированы для фантомных объектов.
На этапе преобразования молча удаляются:
import module/from module import name— они могут оставаться в исходнике для статического анализа IDE, но имя не привязывается во время выполнения.
Во время выполнения вызывают NameError:
open,eval,exec,__import__,compile— этих функций нет в sandbox.
Что поддерживает exec-движок¶
for,if,while,def, включения и lambda — как в Python;**kwargsв фабриках:Point(3, 4, name='A');- неизвестная команда в CamelCase →
NameErrorс понятным сообщением; - повторное присваивание в той же области обновляет элемент, а в цикле или
defавтоматически создаётname_2, name_3, …; - доступ к полям через proxy:
A.x,A.y,circ.center,seg.length; - стили как атрибуты:
A.style.stroke = '#f00'; - f-строки, распаковка кортежей (
a, b = Intersect(c, l)) и арифметика над proxy (A - Bсоздаёт командуSub); - запись функций
f(x) = x^2 + 1,g(t) = 2t + 1(см. Формулы); - опережающие ссылки для имён со строчной буквы:
B = Rotate(R, x*deg, Q)работает, даже еслиxещё не определена — значение можно позже передать черезscene.addVar('x', 115). Исключение — строки формул: имена в них должны существовать к моменту, когда записана формула; - имя, определённое через само себя (
A = Midpoint(A, B)), вызываетValueErrorо цикле зависимостей, а не зависание.
Текущие ограничения¶
- Стабы
.pyiобъявляют 86 сигнатур фабрик: 51 подробную типизированную и 35 общих*args: Any. Конструкторы элементов (Point,Line, …) отдельно типизированы вproxy.pyi. Для редко используемых команд IDE может показатьAnyвместо точного типа. Расширять список нужно вnamespace.pyi; проверка запускается командойpython3 -m animageo.parsers.dsl._regen_stubs. - Sandbox блокирует
open,eval,exec,__import__,compile: во время выполнения возникаетNameError. Верхнеуровневыйimportмолча удаляется, чтобы оставить возможность использовать IDE-стабы, но имя во время выполнения не связывается.