gdarid commited on
Commit
5f59ba1
·
1 Parent(s): ab9204c

add the interface with curves

Browse files
.gitignore CHANGED
@@ -1,2 +1,3 @@
1
  /.idea
2
  /_tmp
 
 
1
  /.idea
2
  /_tmp
3
+ /__pycache__
app.py CHANGED
@@ -1,4 +1,179 @@
 
 
 
 
 
1
  import streamlit as st
 
 
 
 
 
2
 
3
- x = st.slider('Select a value')
4
- st.write(x, 'squared is', x * x)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Streamlit application
3
+ """
4
+ from loguru import logger
5
+ import mpld3
6
  import streamlit as st
7
+ import streamlit.components.v1 as components
8
+ from streamlit.errors import StreamlitAPIException
9
+ import yaml
10
+ import lsystc as ls
11
+ import specific_values as sv
12
 
13
+ st.set_page_config(page_title="Curves with L-systems", page_icon="🖼️", layout="wide")
14
+
15
+
16
+ @st.cache_data
17
+ def load_result(axiom, mult_axiom, rules, rotation_angle, starting_angle, skipped, nb_iterations, coeff):
18
+ """
19
+ Return the result of the L-system with the specified parameters
20
+
21
+ :param axiom:
22
+ :param mult_axiom:
23
+ :param rules:
24
+ :param rotation_angle:
25
+ :param starting_angle:
26
+ :param skipped:
27
+ :param nb_iterations:
28
+ :param coeff:
29
+ :return: the result of the "L-System rendering"
30
+ """
31
+ try:
32
+ config = ls.Config(skipped=skipped)
33
+ rules = rules.strip("; ")
34
+ rules_list = []
35
+
36
+ if rules:
37
+ for item in rules.split(";"):
38
+ if ':' in item:
39
+ splits = item.split(":")
40
+ if len(splits) == 2 and splits[0].strip():
41
+ left = splits[0].strip()
42
+ right = splits[1].strip()
43
+ rules_list.append((left, right))
44
+ else:
45
+ raise ValueError(f"Every rule must be correctly written ( {item} )")
46
+ else:
47
+ raise ValueError(f"Every non empty rule must include a column character ( {item} ) ")
48
+
49
+ rls = ls.Lsystc(config, axiom * mult_axiom, rules_list, nbiter=nb_iterations, verbose=sv.verbose)
50
+ rls.turtle(step=sv.step, angle=rotation_angle, angleinit=starting_angle, coeff=coeff,
51
+ color_length=sv.color_length, color_map=sv.color_map)
52
+
53
+ result = rls.render(sv.renderer, save_files=sv.save_files, show_more=sv.show_more, show_3d=sv.show_3d,
54
+ return_type=sv.return_type)
55
+ except ValueError as ex:
56
+ st.warning(f"Please verify your parameters - {ex}")
57
+ st.stop()
58
+ except Exception as ex:
59
+ st.warning("Please verify your parameters")
60
+ if sv.verbose:
61
+ logger.exception(f"Something went wrong : {ex}")
62
+ st.stop()
63
+ else:
64
+ return result
65
+
66
+
67
+ def write_specific(content):
68
+ """
69
+ Write streamlit content
70
+
71
+ :param content: streamlit content
72
+ :return: None
73
+ """
74
+ try:
75
+ st.write(content)
76
+ except StreamlitAPIException as exc:
77
+ # Currently : streamlit.errors.StreamlitAPIException: `_repr_html_()` is not a valid Streamlit command.
78
+ if sv.verbose:
79
+ logger.error(exc)
80
+
81
+
82
+ def on_change_selection():
83
+ """
84
+ Change the parameters when the starting example is changed
85
+
86
+ :return: None
87
+ """
88
+ current_selection = st.session_state.my_selection
89
+ axiom, mult_axiom, rules, rotation_angle, starting_angle, nb_iter, skipped, coeff = examples_data[current_selection]
90
+ st.session_state.my_axiom = axiom
91
+ st.session_state.my_mult_axiom = mult_axiom
92
+ st.session_state.my_rules = rules
93
+ st.session_state.my_rotation_angle = rotation_angle
94
+ st.session_state.my_starting_angle = starting_angle
95
+ st.session_state.my_nb_iter = nb_iter
96
+ st.session_state.my_skipped = skipped
97
+ st.session_state.my_coeff = coeff
98
+ sv.redraw_auto = True
99
+
100
+
101
+ with open("curves_parameters.yaml", 'r', encoding='utf8') as f:
102
+ curves_parameters = yaml.safe_load(f)
103
+
104
+ EXAMPLES_DEFAULT = 0
105
+ examples_names = []
106
+ examples_data = {}
107
+ for c_name, c_params in curves_parameters.items():
108
+ examples_names.append(c_name)
109
+ c_axiom = c_params.get('axiom', '')
110
+ c_axiom_multiplier = c_params.get('axiom_multiplier', 1)
111
+ c_rules = c_params.get('rules', '')
112
+ c_rotation_angle = c_params.get('rotation_angle', 90.0)
113
+ c_starting_angle = c_params.get('starting_angle', 0)
114
+ c_nb_iter = c_params.get('nb_iter', 1)
115
+ c_skipped = c_params.get('skipped', '')
116
+ c_coeff = c_params.get('coeff', 1.0)
117
+ examples_data[c_name] = (c_axiom, c_axiom_multiplier, c_rules,
118
+ c_rotation_angle, c_starting_angle, c_nb_iter, c_skipped, c_coeff)
119
+
120
+
121
+ EXAMPLES_DEFAULT_name = examples_names[EXAMPLES_DEFAULT]
122
+ def_axiom, def_mult_axiom, def_rules, def_rotation_angle, def_starting_angle, def_nb_iter, def_skipped, def_coeff = \
123
+ examples_data[EXAMPLES_DEFAULT_name]
124
+
125
+ with st.sidebar:
126
+ MD_INTRO = """
127
+ You have the flexibility to select a starting example and to change the parameters :sunglasses:
128
+ """
129
+
130
+ st.markdown(MD_INTRO)
131
+
132
+ input_selection = st.selectbox('Starting example', examples_names,
133
+ index=EXAMPLES_DEFAULT, on_change=on_change_selection, key="my_selection")
134
+
135
+ input_axiom = st.text_input('Starting axiom', def_axiom, key="my_axiom")
136
+ input_mult_axiom = st.number_input('Multiplier for axiom', value=def_mult_axiom, min_value=1, key="my_mult_axiom")
137
+
138
+ input_rules = st.text_input('Rules', def_rules, help="Example for 2 rules -> A: ABC ; B: CAB ; ",
139
+ key="my_rules")
140
+ input_rotation_angle = st.number_input('Angle of rotation',
141
+ value=def_rotation_angle, min_value=1.0, max_value=360.0, step=1.0,
142
+ format='%f', key="my_rotation_angle")
143
+ input_starting_angle = st.number_input('Starting angle',
144
+ value=def_starting_angle, min_value=0, max_value=360,
145
+ format='%d', key="my_starting_angle")
146
+
147
+ input_skipped = st.text_input('Skipped characters', def_skipped, key="my_skipped")
148
+
149
+ input_coeff = st.number_input('Coefficient',
150
+ value=def_coeff, format='%f', key="my_coeff")
151
+
152
+ input_nb_iter = st.number_input('Number of iterations',
153
+ value=def_nb_iter, min_value=1, max_value=15, format='%d', key="my_nb_iter")
154
+
155
+ st.markdown("# Curves with L-systems")
156
+
157
+ st.markdown("""**Click on "Draw" to display the curve**""")
158
+
159
+ if st.button('Draw') or sv.redraw_auto or 'start' not in st.session_state:
160
+ sv.redraw_auto = False
161
+ st.session_state['start'] = True
162
+ res = load_result(input_axiom, input_mult_axiom, input_rules, input_rotation_angle, input_starting_angle,
163
+ input_skipped, input_nb_iter, input_coeff)
164
+
165
+ if sv.return_type == 'image':
166
+ write_specific(st.image(res, caption='Generated image'))
167
+ else:
168
+ if sv.renderer == 'matplot':
169
+ # Pyplot figure
170
+ # write_specific(st.pyplot(res))
171
+ fig_html = mpld3.fig_to_html(res)
172
+ components.html(fig_html, height=600)
173
+ elif sv.renderer == 'bokeh':
174
+ st.bokeh_chart(res, use_container_width=True)
175
+ elif sv.renderer == 'plotly':
176
+ st.plotly_chart(res, use_container_width=True)
177
+
178
+ st.markdown("---")
179
+ st.markdown("More details on Lindenmayer systems here : [Wikipedia](https://en.wikipedia.org/wiki/L-system)")
curves_parameters.yaml ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Dragon:
3
+ axiom: "FX"
4
+ axiom_multiplier: 1
5
+ rules: "X:X+YF+. ; Y:-FX-Y"
6
+ rotation_angle: 90.0
7
+ nb_iter: 12
8
+
9
+ Gosper:
10
+ axiom: "AB"
11
+ rules: "A: A-B--B+A++AA+B-. ; B: +A-BB--B-A++A+B."
12
+ rotation_angle: 60.0
13
+ nb_iter: 3
14
+
15
+ Hilbert:
16
+ axiom: "L"
17
+ rules: "L:-RF+LFL+FR-. ; R:+LF-RFR-FL+."
18
+ rotation_angle: 90.0
19
+ nb_iter: 5
20
+ skipped: LR
21
+
22
+ Icy:
23
+ axiom: "F+F+F+F"
24
+ rules: "F: FF+F++.F+F "
25
+ rotation_angle: 90.0
26
+ nb_iter: 5
27
+
28
+ Islands:
29
+ axiom: "F-F-F-F"
30
+ rules: "F:F-U+FF-F-FF-FU-FF+U-FF+F+FF+FU+FFF ; U:UUUUUU"
31
+ rotation_angle: 90.0
32
+ nb_iter: 2
33
+
34
+ Koch:
35
+ axiom: "F--F--F"
36
+ rules: "F: F+F-.-F+F"
37
+ rotation_angle: 60.0
38
+ nb_iter: 6
39
+
40
+ Minkowski:
41
+ axiom: "F"
42
+ rules: "F: F+F-F-F.F+F+F-F "
43
+ rotation_angle: 90.0
44
+ nb_iter: 3
45
+
46
+ Peano:
47
+ axiom: "++FA"
48
+ rules: "A:A-BA+CA+CA+CA-BA-BA-BA+CA. ; B:F-F-F-F. ; C:F+F+F+F."
49
+ rotation_angle: 22.5
50
+ nb_iter: 4
51
+ skipped: ABC
52
+
53
+ Penrose tiling:
54
+ axiom: "[N]++[N]++[N]++[N]++[N]"
55
+ rules: "A:O++B----N[-O----A]++ ; N:+O--B[---A--N]+ ; O:-A++N[+++O++B]- ; B:--O++++A[+B++++N]--N"
56
+ rotation_angle: 36.0
57
+ nb_iter: 5
58
+
59
+ Pentadendrite:
60
+ axiom: "F-F-F-F-F"
61
+ rules: "F:F-F-F+.+F+F-F"
62
+ rotation_angle: 72.0
63
+ nb_iter: 4
64
+
65
+ Polygon with angle 30 degrees:
66
+ axiom: "F+."
67
+ rules: ""
68
+ rotation_angle: 30.0
69
+ nb_iter: 1
70
+
71
+ axiom_multiplier: 12
72
+
73
+ Polygon with angle 150 degrees:
74
+ axiom: "F+."
75
+ rules: ""
76
+ rotation_angle: 150.0
77
+ nb_iter: 1
78
+
79
+ axiom_multiplier: 12
80
+
81
+ Polygon with angle 175 degrees:
82
+ axiom: "F+."
83
+ rules: ""
84
+ rotation_angle: 175.0
85
+ nb_iter: 1
86
+
87
+ axiom_multiplier: 72
88
+
89
+ Polygon with angle 179 degrees:
90
+ axiom: "F+."
91
+ rules: ""
92
+ rotation_angle: 179.0
93
+ nb_iter: 1
94
+
95
+ axiom_multiplier: 360
96
+
97
+ Polygon 9*5:
98
+ axiom: "r+"
99
+ rules: "r: F++F--F--F++F."
100
+ rotation_angle: 40.0
101
+ nb_iter: 1
102
+
103
+ axiom_multiplier: 9
104
+
105
+ Repetition 3:
106
+ axiom: "F#+F#+F#"
107
+ rules: ""
108
+ rotation_angle: 120.0
109
+ nb_iter: 1
110
+
111
+ axiom_multiplier: 1
112
+
113
+ Repetition 5:
114
+ axiom: "F#+F#+F#+F#+F#"
115
+ rules: ""
116
+ rotation_angle: 72.0
117
+ nb_iter: 1
118
+
119
+ axiom_multiplier: 1
120
+
121
+ Repetition 6:
122
+ axiom: "F#+F#+F#+F#+F#+F#"
123
+ rules: ""
124
+ rotation_angle: 60.0
125
+ nb_iter: 1
126
+
127
+ axiom_multiplier: 1
128
+
129
+ Returns with angle 5 degrees:
130
+ axiom: "|+."
131
+ rules: ""
132
+ rotation_angle: 5.0
133
+ nb_iter: 1
134
+
135
+ axiom_multiplier: 72
136
+
137
+ Round star:
138
+ axiom: "F"
139
+ rules: "F:F+.+F"
140
+ rotation_angle: 77.0
141
+ nb_iter: 7
142
+
143
+ Sierpinsky carpet:
144
+ axiom: "A"
145
+ rules: "A:B-A-B. ; B:A+B+A."
146
+ rotation_angle: 60.0
147
+ nb_iter: 6
148
+
149
+ Spider:
150
+ axiom: "F*+."
151
+ rules: ""
152
+ rotation_angle: 61.0
153
+ nb_iter: 1
154
+
155
+ axiom_multiplier: 500
156
+ coeff: 0.99
157
+
158
+ Spiral 1:
159
+ axiom: "Fu+."
160
+ rules: ""
161
+ rotation_angle: 10.0
162
+ nb_iter: 1
163
+
164
+ axiom_multiplier: 500
165
+
166
+ Spiral 2:
167
+ axiom: "F*+."
168
+ rules: ""
169
+ rotation_angle: 10.0
170
+ nb_iter: 1
171
+
172
+ axiom_multiplier: 500
173
+ coeff: 1.01
174
+
175
+ Spiral 3:
176
+ axiom: "F*+."
177
+ rules: ""
178
+ rotation_angle: 10.0
179
+ nb_iter: 1
180
+
181
+ axiom_multiplier: 500
182
+ coeff: -1.01
183
+
184
+ Squigly:
185
+ axiom: "r"
186
+ rules: "r: Fr+FL+Fr. ; L: FL-Fr-FL"
187
+ rotation_angle: 60.0
188
+ nb_iter: 9
189
+
190
+ Tree:
191
+ axiom: "B"
192
+ rules: "A: AA; B: A[-B][+B]"
193
+ rotation_angle: 20.0
194
+ nb_iter: 5
195
+
196
+ starting_angle: 90
197
+
198
+ Tree 2:
199
+ axiom: "F"
200
+ rules: "F: F[.+FF][.-FF]F[.-F][.+F]F"
201
+ rotation_angle: 36.0
202
+ nb_iter: 3
203
+
204
+ starting_angle: 90
205
+
206
+ Triangle with spiral:
207
+ axiom: "F*+."
208
+ rules: ""
209
+ rotation_angle: 122.0
210
+ nb_iter: 1
211
+
212
+ axiom_multiplier: 500
213
+ coeff: 0.94
214
+
215
+ 3D example:
216
+ axiom: "F+F+F .MFM. F+F+F"
217
+ rules: ""
218
+ rotation_angle: 90.0
219
+ nb_iter: 1
220
+
221
+ 3D spring:
222
+ axiom: "F+P."
223
+ rules: ""
224
+ rotation_angle: 10.0
225
+ nb_iter: 1
226
+
227
+ axiom_multiplier: 500
lsystc.py ADDED
@@ -0,0 +1,586 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Lindenmayer System (L-system) with personal customizations
3
+
4
+ See also
5
+
6
+ https://en.wikipedia.org/wiki/L-system
7
+
8
+ https://onlinemathtools.com/l-system-generator
9
+ """
10
+
11
+ import io
12
+ import math
13
+ from collections import deque
14
+ from bokeh.plotting import figure, show, output_file
15
+ from bokeh.io import export_png, export_svgs
16
+ from bokeh.io.export import get_screenshot_as_png
17
+ from loguru import logger
18
+ from scipy.spatial.transform import Rotation
19
+ import attrs as at
20
+ import matplotlib as mpl
21
+ import matplotlib.pyplot as plt
22
+ import numpy as np
23
+ import plotly.graph_objects as go
24
+ import PIL.Image as pimage
25
+
26
+
27
+ @at.define
28
+ class Config: # pylint: disable=too-few-public-methods
29
+ """
30
+ Configuration of the important active characters
31
+ """
32
+ reserved: str = ':; ' # reserved characters
33
+
34
+ color: str = '.'
35
+ move_lifted_pen: str = 'UVW'
36
+ move_angle_init: str = '_'
37
+ move: str = 'ABCDEFGHIJKLNOQRST' # M and P are reserved for 3d rotations
38
+
39
+ move_up_3d: str = '⇧'
40
+ move_down_3d: str = '⇩'
41
+
42
+ r3d_1_plus: str = 'p' # Axis of rotation : "X"
43
+ r3d_1_minus: str = 'm'
44
+
45
+ r3d_2_plus: str = 'P' # Axis of rotation : "Y"
46
+ r3d_2_minus: str = 'M'
47
+
48
+ r3d_all: str = at.field(init=False)
49
+
50
+ move_multi: str = at.field(init=False)
51
+ move_all: str = at.field(init=False)
52
+
53
+ delta_add: str = 'u'
54
+ delta_sub: str = 'v'
55
+
56
+ outer_repetition: str = '#'
57
+ outer_repetition_max: int = 100
58
+
59
+ skipped: str = ''
60
+ total_skipped: str = at.field(init=False) # all skipped characters
61
+
62
+ def __attrs_post_init__(self):
63
+ self.move_multi = self.move.lower()
64
+ self.move_all = self.color + self.move_lifted_pen + self.move_angle_init + self.move_multi + self.move
65
+ self.total_skipped = ' ' + self.skipped
66
+ self.r3d_all = self.r3d_1_plus + self.r3d_1_minus + self.r3d_2_plus + self.r3d_2_minus
67
+
68
+
69
+ class Lsystc:
70
+ """
71
+ Classic L-System with few customizations
72
+ """
73
+ def __init__(self, config: Config, axiom: str, rules: list[tuple[str, str]], nbiter: int,
74
+ dev_ini: bool = True, verbose: bool = False) -> None:
75
+ self.config = config
76
+ self.axiom = axiom
77
+ self.rules = rules
78
+ self.nbiter = nbiter
79
+ self.dev_ini = dev_ini
80
+ self.verbose = verbose
81
+
82
+ self.dev = ''
83
+ self.turt = []
84
+
85
+ self.dimension = 2
86
+
87
+ self.rotation_3d_done = False
88
+ self.mobile_vectors: list[np.array] = [np.array([1.0, 0.0, 0.0]),
89
+ np.array([0.0, 1.0, 0.0]),
90
+ np.array([0.0, 0.0, 1.0])]
91
+
92
+ self.log('info', f"Axiom: {self.axiom:.50} ; Rules : {self.rules} ; Nb iterations : {self.nbiter}")
93
+
94
+ if self.dev_ini:
95
+ self.develop()
96
+ self.log('info', f"Axiom: {self.axiom:.50} ; Rules : {self.rules} ; "
97
+ f"Nb iterations : {self.nbiter} Expanded value : {self.dev[:50]+'...'} ; After")
98
+
99
+ @staticmethod
100
+ def apply_rot(rot: Rotation, vec: np.array) -> np.array:
101
+ """
102
+ Apply a rotation on a vector
103
+
104
+ :param rot: rotation to apply
105
+ :param vec: concerned vector
106
+ :return: rotated vector
107
+ """
108
+ return rot.apply(vec).round(decimals=6)
109
+
110
+ @staticmethod
111
+ def dev_unit(source: str, rules: list[tuple[str, str]]) -> str:
112
+ """
113
+ Develop source with rules
114
+ """
115
+ result = source
116
+ position = 0
117
+ lreg = None
118
+
119
+ while True:
120
+ # The leftmost usable rule is applied
121
+ newpos = None
122
+ for lr, regle in enumerate(rules):
123
+ lpos = result.find(regle[0], position)
124
+ if lpos >= 0:
125
+ if newpos is None or lpos < newpos:
126
+ newpos = lpos
127
+ lreg = lr
128
+
129
+ if newpos is None:
130
+ break
131
+
132
+ result = result[0:newpos] + result[newpos:].replace(rules[lreg][0], rules[lreg][1], 1)
133
+ position = newpos + len(rules[lreg][1])
134
+
135
+ return result
136
+
137
+ @staticmethod
138
+ def color_from_map(name: str, index: int) -> tuple[int, int, int]:
139
+ """
140
+ :param name: name of the discrete colormap (matplotlib source) to be used
141
+ :param index: index of the color in the map
142
+ :return: tuple (red, green, blue)
143
+ """
144
+ r, g, b = mpl.colormaps[name].colors[index]
145
+ r, g, b = int(r * 255), int(g * 255), int(b * 255)
146
+
147
+ return r, g, b
148
+
149
+ def log(self, ltype: str, message: str, *args, **kwargs):
150
+ """
151
+ Log a message with consideration for the verbosity property
152
+
153
+ :param ltype: type of log
154
+ :param message: message
155
+ :return: None
156
+ """
157
+ if self.verbose:
158
+ func_dict = {'info': logger.info, 'debug': logger.debug, 'warning': logger.warning,
159
+ 'error': logger.error, 'exception': logger.exception}
160
+
161
+ func = func_dict.get(ltype)
162
+ if not func:
163
+ raise ValueError(f"This type of log is unknown : {ltype}")
164
+
165
+ func(message, *args, **kwargs)
166
+
167
+ def new_pos(self, ax: float, ay: float, az: float, astep: float, aangle: float) -> tuple[float, float, float]:
168
+ """
169
+ New position from (ax, ay, az) with the use of astep and aangle
170
+
171
+ The angle is not used if a 3D rotation has been done
172
+
173
+ :param ax: 1st coordinate of starting point
174
+ :param ay: 2nd coordinate of starting point
175
+ :param az: 3rd coordinate of starting point
176
+ :param astep: step size
177
+ :param aangle: step angle
178
+ """
179
+ if self.rotation_3d_done:
180
+ forward_vector = self.mobile_vectors[0]
181
+ lx = ax + astep * forward_vector[0]
182
+ ly = ay + astep * forward_vector[1]
183
+ lz = az + astep * forward_vector[2]
184
+
185
+ return lx, ly, lz
186
+ else:
187
+ if aangle == 0.0:
188
+ lx = ax + astep
189
+ ly = ay
190
+ elif aangle == 90.0:
191
+ lx = ax
192
+ ly = ay + astep
193
+ elif aangle == 180.0:
194
+ lx = ax - astep
195
+ ly = ay
196
+ elif aangle == 270.0:
197
+ lx = ax
198
+ ly = ay - astep
199
+ else:
200
+ lx = ax + astep * math.cos(math.radians(aangle))
201
+ ly = ay + astep * math.sin(math.radians(aangle))
202
+
203
+ return lx, ly, az
204
+
205
+ def develop(self) -> None:
206
+ """
207
+ Develop self.axiom from the list of rules (self.rules) with nbiter iterations
208
+
209
+ A rule is a couple (source, target) where source can be replaced by target
210
+
211
+ Example of Koch :
212
+ axiom = 'F'
213
+ rules = [('F','F+F-F-F+F')]
214
+
215
+ Example with 2 rules :
216
+ axiom = 'A'
217
+ rules = [('A','AB'),('B','A')]
218
+ """
219
+ result = self.axiom
220
+
221
+ if self.rules:
222
+ for _ in range(self.nbiter):
223
+ result = self.dev_unit(result, self.rules)
224
+
225
+ self.dev = result
226
+
227
+ def init_3d(self, angle: float) -> None:
228
+ """
229
+ Initialization of the 3D and of the mobile vectors
230
+
231
+ :param angle: current angle
232
+ :return: None
233
+ """
234
+ self.rotation_3d_done = True
235
+ self.dimension = 3
236
+
237
+ vec_x = self.mobile_vectors[0]
238
+ vec_y = self.mobile_vectors[1]
239
+ axis = self.mobile_vectors[2]
240
+
241
+ rot = Rotation.from_rotvec(angle * axis, degrees=True)
242
+ new_vec_x = self.apply_rot(rot, vec_x)
243
+ new_vec_y = self.apply_rot(rot, vec_y)
244
+
245
+ self.mobile_vectors[0] = new_vec_x
246
+ self.mobile_vectors[1] = new_vec_y
247
+
248
+ def rotate_3d(self, rtype: str, rangle: float) -> None:
249
+ """
250
+ Apply a 3D rotation on the mobile vectors
251
+
252
+ :param rtype: type of rotation
253
+ :param rangle: angle of rotation
254
+ :return: None
255
+ """
256
+ vec_x = self.mobile_vectors[0]
257
+ vec_y = self.mobile_vectors[1]
258
+ vec_z = self.mobile_vectors[2]
259
+ rsign = 1 if rtype in self.config.r3d_1_plus + self.config.r3d_2_plus + '+>' else -1
260
+ if rtype in self.config.r3d_1_minus + self.config.r3d_1_plus:
261
+ # Axis of rotation is "X"
262
+ axis = vec_x
263
+ rot = Rotation.from_rotvec(rsign * rangle * axis, degrees=True)
264
+ new_vec_x = axis
265
+ new_vec_y = self.apply_rot(rot, vec_y)
266
+ new_vec_z = self.apply_rot(rot, vec_z)
267
+ elif rtype in self.config.r3d_2_minus + self.config.r3d_2_plus:
268
+ # Axis of rotation is "Y"
269
+ axis = vec_y
270
+ rot = Rotation.from_rotvec(rsign * rangle * axis, degrees=True)
271
+ new_vec_x = self.apply_rot(rot, vec_x)
272
+ new_vec_y = axis
273
+ new_vec_z = self.apply_rot(rot, vec_z)
274
+ else:
275
+ # Axis of rotation is "Z" ( +->< )
276
+ axis = vec_z
277
+ rot = Rotation.from_rotvec(rsign * rangle * axis, degrees=True)
278
+ new_vec_x = self.apply_rot(rot, vec_x)
279
+ new_vec_y = self.apply_rot(rot, vec_y)
280
+ new_vec_z = axis
281
+
282
+ self.mobile_vectors[0] = new_vec_x
283
+ self.mobile_vectors[1] = new_vec_y
284
+ self.mobile_vectors[2] = new_vec_z
285
+
286
+ def turtle(self, step: float = 10.0, angle: float = 90.0, angleinit: float = 0.0, coeff: float = 1.1,
287
+ angle2: float = 10.0, color_length: int = 3, color_map: str = "Set1",
288
+ delta: float = 0.1) -> None:
289
+ """
290
+ Develop self.dev in [(lx, ly, lz, color),...] where lx, ly, lz are lists of positions
291
+ The result goes to self.turt
292
+
293
+ :param step: the turtle step size
294
+ :param angle: angle of rotation (in degrees) ( + - )
295
+ :param angleinit: initial angle
296
+ :param coeff: magnification or reduction factor for the step ( * / ) and factor for "lowered" characters
297
+ :param angle2: 2nd usable angle ( < > )
298
+ :param color_length: maximal number of colours
299
+ :param color_map: color map to use (matplotlib name)
300
+ :param delta: value to add to the step
301
+ """
302
+ res = []
303
+ stock: list = [] # List of ("point", angle, ...) kept for [] et ()
304
+
305
+ lix: list[float] = []
306
+ liy: list[float] = []
307
+ liz: list[float] = []
308
+
309
+ tx = 0.0
310
+ ty = 0.0
311
+ tz = 0.0
312
+ tstep = step
313
+ tangle = angleinit
314
+ tsens = 1
315
+ color_index = 0
316
+ tcouleur = self.color_from_map(color_map, color_index)
317
+
318
+ nb_iterations: int = 0
319
+ stock_outer: deque = deque([(tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors)])
320
+
321
+ while stock_outer:
322
+ nb_iterations += 1
323
+ if len(lix) > 1:
324
+ res.append((lix, liy, liz, tcouleur))
325
+
326
+ tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors = stock_outer.popleft()
327
+ lix = [tx]
328
+ liy = [ty]
329
+ liz = [tz]
330
+
331
+ for car in self.dev:
332
+
333
+ if car in self.config.total_skipped:
334
+ continue
335
+
336
+ npos = False
337
+ npospos = False
338
+ nliste = False
339
+ ncolor = False
340
+
341
+ if car in self.config.move_all:
342
+ if car in self.config.color:
343
+ ncolor = True # Change of color
344
+ else:
345
+ ltstep = tstep
346
+
347
+ if car in self.config.move_multi:
348
+ ltstep = tstep * coeff
349
+ elif car in self.config.move_angle_init:
350
+ tangle = angleinit
351
+
352
+ ltangle = tangle
353
+
354
+ tx, ty, tz = self.new_pos(tx, ty, tz, ltstep, ltangle)
355
+
356
+ # npos true <-> new position with the pen down
357
+ npos = car in self.config.move + self.config.move_multi + self.config.move_angle_init
358
+
359
+ # nliste true <-> new list because of a change of color or a raised pen
360
+ nliste = car in self.config.color + self.config.move_lifted_pen
361
+ elif car in self.config.move_up_3d or car in self.config.move_down_3d:
362
+ npos = True
363
+ self.dimension = 3
364
+ if car in self.config.move_up_3d:
365
+ tz += tstep
366
+ else:
367
+ tz -= tstep
368
+ elif car in '+-><' + self.config.r3d_all:
369
+ if self.rotation_3d_done:
370
+ if car in '+-' + self.config.r3d_all:
371
+ langle = angle
372
+ else:
373
+ langle = angle2
374
+
375
+ self.rotate_3d(car, langle * tsens)
376
+ else:
377
+ if car in '+':
378
+ tangle = (tangle + angle * tsens) % 360.0
379
+ elif car in '-':
380
+ tangle = (tangle - angle * tsens) % 360.0
381
+ elif car in '>':
382
+ tangle = (tangle + angle2 * tsens) % 360.0
383
+ elif car in '<':
384
+ tangle = (tangle - angle2 * tsens) % 360.0
385
+ else:
386
+ # There is a not trivial 3D rotation ( PMpm )
387
+ self.init_3d(tangle)
388
+ self.rotate_3d(car, angle * tsens)
389
+
390
+ elif car == '*':
391
+ tstep *= coeff
392
+ elif car == '/':
393
+ tstep /= coeff
394
+ elif car in self.config.delta_add:
395
+ tstep += delta
396
+ elif car in self.config.delta_sub:
397
+ tstep -= delta
398
+ elif car in '[(':
399
+ stock.append((tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors))
400
+ elif car in '])':
401
+ if stock:
402
+ tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors = stock.pop()
403
+ nliste = True # the pen is raised to go back to the stocked position
404
+ elif car == '|':
405
+ # Single "return" ("round-trip")
406
+ npospos = True
407
+ elif car == '!':
408
+ # Change the sens of rotation
409
+ tsens = tsens * -1
410
+ elif car in self.config.outer_repetition:
411
+ # A new possible item in the "outer stock"
412
+ if nb_iterations <= self.config.outer_repetition_max:
413
+ stock_outer.append((tx, ty, tz, tangle, tcouleur, tstep, self.mobile_vectors))
414
+ else:
415
+ continue
416
+
417
+ # Take into account the read character
418
+ # ------------------------------------
419
+ if nliste:
420
+ # New list because of new color or lifted pen
421
+ if len(lix) > 1:
422
+ res.append((lix, liy, liz, tcouleur))
423
+
424
+ if ncolor:
425
+ # Change of color
426
+ color_index = (color_index + 1) % color_length
427
+ tcouleur = self.color_from_map(color_map, color_index)
428
+
429
+ lix = [tx]
430
+ liy = [ty]
431
+ liz = [tz]
432
+ elif npos:
433
+ # New position and no new list
434
+ lix.append(tx)
435
+ liy.append(ty)
436
+ liz.append(tz)
437
+ elif npospos:
438
+ # 2 new positions for a "round-trip"
439
+ tnx, tny, tnz = self.new_pos(tx, ty, tz, tstep, tangle)
440
+
441
+ lix.append(tnx)
442
+ liy.append(tny)
443
+ liz.append(tnz)
444
+
445
+ lix.append(tx)
446
+ liy.append(ty)
447
+ liz.append(tz)
448
+
449
+ if len(lix) > 1:
450
+ # Finally, append the last points
451
+ res.append((lix, liy, liz, tcouleur))
452
+
453
+ self.turt = res
454
+
455
+ def render(self, show_type: str = 'matplot', image_destination: str = 'images_out/',
456
+ save_files: bool = True, show_more: bool = False, show_3d: bool = False,
457
+ return_type: str = ''):
458
+ """
459
+ Render self.turt using a specific show type
460
+
461
+ :param show_type: 'matplot' or 'bokeh'
462
+ :param image_destination: folder for images backup
463
+ :param save_files: True to save files
464
+ :param show_more: True to show with specific show_type
465
+ :param show_3d: True to show 3D (implemented with plotly only)
466
+ :param return_type: '', 'image' or 'figure'
467
+ :return: None or an image if return_type is 'image' or a figure if return_type is 'figure'
468
+ """
469
+ if show_type == 'matplot':
470
+ fig, ax = plt.subplots()
471
+
472
+ for (lx, ly, _, coul) in self.turt:
473
+ r, g, b = coul
474
+ ax.plot(lx, ly, color=(r / 255., g / 255., b / 255., 1.0))
475
+
476
+ ax.set_axis_off()
477
+ ax.grid(visible=False)
478
+
479
+ if show_more:
480
+ plt.show()
481
+
482
+ if save_files:
483
+ fig.savefig(f'{image_destination}plot_{show_type}.png', bbox_inches='tight')
484
+ fig.savefig(f'{image_destination}plot_{show_type}.svg', bbox_inches='tight')
485
+
486
+ if return_type == 'image':
487
+ fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
488
+ fig.canvas.draw()
489
+
490
+ # Return an image : PIL.Image
491
+ return pimage.frombytes('RGB', fig.canvas.get_width_height(), fig.canvas.tostring_rgb())
492
+
493
+ if return_type == 'figure':
494
+ return fig
495
+
496
+ elif show_type == 'bokeh':
497
+ if save_files:
498
+ output_file(f'{image_destination}lines_{show_type}.html')
499
+
500
+ fig = figure(title="LSyst", x_axis_label='x', y_axis_label='y', width=800, height=800)
501
+
502
+ for (lx, ly, _, coul) in self.turt:
503
+ cr, cg, cb = coul
504
+ fig.line(lx, ly, line_color=(cr, cg, cb))
505
+
506
+ fig.xgrid.grid_line_color = None
507
+ fig.ygrid.grid_line_color = None
508
+
509
+ if show_more:
510
+ _ = show(fig)
511
+
512
+ if save_files:
513
+ export_png(fig, filename=f'{image_destination}plot_{show_type}.png')
514
+
515
+ fig.output_backend = "svg"
516
+ export_svgs(fig, filename=f'{image_destination}plot_{show_type}.svg')
517
+
518
+ if return_type == 'image':
519
+ fig.toolbar_location = None
520
+ fig.axis.visible = False
521
+ fig.title = ""
522
+
523
+ # Return an image : PIL.Image
524
+ return get_screenshot_as_png(fig)
525
+
526
+ if return_type == 'figure':
527
+ return fig
528
+
529
+ elif show_type == 'plotly':
530
+ fig = go.Figure()
531
+
532
+ axis_dict = {
533
+ "showline": True,
534
+ "showgrid": False,
535
+ "showticklabels": True,
536
+ "zeroline": False,
537
+ "ticks": 'outside',
538
+ }
539
+
540
+ index = 0
541
+
542
+ if self.dimension == 2 or not show_3d:
543
+ fig.update_yaxes(
544
+ scaleanchor="x",
545
+ scaleratio=1,
546
+ )
547
+ for (lx, ly, lz, coul) in self.turt:
548
+ index += 1
549
+ cr, cg, cb = coul
550
+ fig.add_trace(go.Scatter(x=lx, y=ly, mode='lines',
551
+ name=f"t{index}", line={"color": f'rgb({cr},{cg},{cb})', "width": 1}))
552
+
553
+ else:
554
+ # 3D
555
+ for (lx, ly, lz, coul) in self.turt:
556
+ index += 1
557
+ cr, cg, cb = coul
558
+ fig.add_trace(go.Scatter3d(x=lx, y=ly, z=lz, mode='lines',
559
+ name=f"t{index}", line={"color": f'rgb({cr},{cg},{cb})', "width": 1}))
560
+
561
+ fig.update_layout(
562
+ xaxis=axis_dict,
563
+ yaxis=axis_dict,
564
+ autosize=True,
565
+ showlegend=False
566
+ )
567
+
568
+ if show_more:
569
+ fig.show()
570
+
571
+ if save_files:
572
+ fig.write_image(f'{image_destination}plot_{show_type}.png')
573
+ fig.write_image(f'{image_destination}plot_{show_type}.svg')
574
+
575
+ if return_type == 'image':
576
+ fig_bytes = fig.to_image(format="png")
577
+ buf = io.BytesIO(fig_bytes)
578
+
579
+ # Return an image : PIL.Image
580
+ return pimage.open(buf)
581
+
582
+ if return_type == 'figure':
583
+ return fig
584
+
585
+ else:
586
+ raise ValueError("The given show_type is not correct")
requirements.txt CHANGED
@@ -1 +1,13 @@
1
  # requirements apart from streamlit (sdk)
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # requirements apart from streamlit (sdk)
2
+ attrs
3
+ bokeh
4
+ kaleido
5
+ loguru
6
+ matplotlib
7
+ mpld3
8
+ numpy
9
+ Pillow
10
+ plotly
11
+ pyyaml
12
+ scipy
13
+ selenium
requirements_dev.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ mypy
2
+ pylint
3
+ pytest
4
+ types-Pillow
5
+ types-PyYAML
requirements_other.txt CHANGED
@@ -1 +1,2 @@
 
1
  streamlit
 
1
+ # possible sdk requirements
2
  streamlit
specific_values.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Specific parameters and values to customize the look of the app
3
+
4
+ The possible color maps are
5
+ ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b','tab20c']
6
+
7
+ """
8
+
9
+ # Initial value
10
+ redraw_auto = True
11
+
12
+ # Turtle parameters
13
+ step = 10.0
14
+
15
+ # Number of the maximum of colors and color map
16
+ color_length = 3
17
+ color_map = "Set1"
18
+
19
+ # Other
20
+ renderer = 'plotly' # 'matplot' or 'bokeh' or 'plotly'
21
+ return_type = 'figure' # 'figure' or 'image'
22
+ if (renderer, return_type) not in [('matplot', 'image'), ('matplot', 'figure'),
23
+ ('plotly', 'image'), ('plotly', 'figure'),
24
+ ('bokeh', 'image')]:
25
+ raise NotImplementedError("The current combination of renderer and return_type is not implemented")
26
+
27
+ save_files = False
28
+ verbose = False # Set to true to see more logs
29
+ show_more = False # Set to true to see more details "locally"
30
+ show_3d = True
test_app.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import pytest
2
+ from streamlit.testing.v1 import AppTest
3
+
4
+
5
+ @pytest.mark.filterwarnings("ignore:coroutine")
6
+ def test_app():
7
+ at = AppTest.from_file("app.py").run()
8
+ at.button[0].click().run()
test_curves.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import lsystc as ls
2
+ import pytest
3
+ from numpy.testing import assert_allclose
4
+
5
+
6
+ @pytest.mark.parametrize("axiom, rules, expected_result", [
7
+ ('A', [('A', 'AB'), ('B', 'BB')], 'AB'),
8
+ ('AB', [('A', 'AB'), ('B', 'BB')], 'ABBB'),
9
+ ])
10
+ def test_single_iteration(axiom, rules, expected_result):
11
+ syst = ls.Lsystc(ls.Config(), axiom, rules, nbiter=1)
12
+ assert syst.dev == expected_result
13
+
14
+
15
+ @pytest.mark.parametrize("axiom, rules, nbiterations, expected_result", [
16
+ ('A', [('A', 'AB'), ('B', 'BB')], 2, 'ABBB'),
17
+ ('AB', [('A', 'AB'), ('B', 'BB')], 3, 'ABBBBBBBBBBBBBBB'),
18
+ ])
19
+ def test_more_iterations(axiom, rules, nbiterations, expected_result):
20
+ syst = ls.Lsystc(ls.Config(), axiom, rules, nbiter=nbiterations)
21
+ assert syst.dev == expected_result
22
+
23
+
24
+ @pytest.mark.parametrize("axiom, expected_result", [
25
+ ('F', [([0, 10], [0, 0], [0, 0], (228, 26, 28))]),
26
+ ('F+F', [([0, 10, 10], [0, 0, 10], [0, 0, 0], (228, 26, 28))]),
27
+ ])
28
+ def test_turt(axiom, expected_result):
29
+ syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
30
+ syst.turtle()
31
+ assert syst.turt == expected_result
32
+
33
+
34
+ @pytest.mark.parametrize("axiom, expected_result", [
35
+ ('⇧F', [([0, 0, 10], [0, 0, 0], [0, 10, 10], (228, 26, 28))]),
36
+ ('⇩F', [([0, 0, 10], [0, 0, 0], [0, -10, -10], (228, 26, 28))]),
37
+ ])
38
+ def test_turt_3d_up_down(axiom, expected_result):
39
+ syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
40
+ syst.turtle()
41
+ assert syst.turt == expected_result
42
+
43
+
44
+ @pytest.mark.parametrize("axiom, expected_result", [
45
+ ('m+F', [([0.0, 0.0], [0.0, 0.0], [0.0, -10.0], (228, 26, 28))]),
46
+ ('p+F', [([0.0, 0.0], [0.0, 0.0], [0.0, 10.0], (228, 26, 28))]),
47
+ ])
48
+ def test_turt_3d_rotation_1(axiom, expected_result):
49
+ syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
50
+ syst.turtle()
51
+
52
+ lxr = syst.turt[0][0]
53
+ lxe = expected_result[0][0]
54
+
55
+ assert_allclose(lxr, lxe, atol=1e-6)
56
+
57
+ lyr = syst.turt[0][1]
58
+ lye = expected_result[0][1]
59
+
60
+ assert_allclose(lyr, lye, atol=1e-6)
61
+
62
+ lzr = syst.turt[0][2]
63
+ lze = expected_result[0][2]
64
+
65
+ assert_allclose(lzr, lze, atol=1e-6)
66
+
67
+ lcr = syst.turt[0][3]
68
+ lce = expected_result[0][3]
69
+
70
+ assert lcr == lce
71
+
72
+
73
+ @pytest.mark.parametrize("axiom, expected_result", [
74
+ ('MF', [([0.0, 0.0], [0.0, 0.0], [0.0, 10.0], (228, 26, 28))]),
75
+ ('PF', [([0.0, 0.0], [0.0, 0.0], [0.0, -10.0], (228, 26, 28))]),
76
+ ])
77
+ def test_turt_3d_rotation_2(axiom, expected_result):
78
+ syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
79
+ syst.turtle()
80
+
81
+ lxr = syst.turt[0][0]
82
+ lxe = expected_result[0][0]
83
+
84
+ assert_allclose(lxr, lxe, atol=1e-6)
85
+
86
+ lyr = syst.turt[0][1]
87
+ lye = expected_result[0][1]
88
+
89
+ assert_allclose(lyr, lye, atol=1e-6)
90
+
91
+ lzr = syst.turt[0][2]
92
+ lze = expected_result[0][2]
93
+
94
+ assert_allclose(lzr, lze, atol=1e-6)
95
+
96
+ lcr = syst.turt[0][3]
97
+ lce = expected_result[0][3]
98
+
99
+ assert lcr == lce
100
+
101
+
102
+ @pytest.mark.parametrize("axiom, expected_result", [
103
+ ('uF', [([0, 10.1], [0, 0], [0, 0], (228, 26, 28))]),
104
+ ('vF', [([0, 9.9], [0, 0], [0, 0], (228, 26, 28))]),
105
+ ])
106
+ def test_turt_delta(axiom, expected_result):
107
+ syst = ls.Lsystc(ls.Config(), axiom, [], nbiter=1)
108
+ syst.turtle()
109
+ assert syst.turt == expected_result