sissississi Claude Opus 4.6 commited on
Commit
236e665
·
1 Parent(s): 3e642eb

Initial deploy: OpenEnv RL environment for origami crease patterns

Browse files

FastAPI + openenv-core environment that serves as an RL training
backend for teaching LLMs to generate valid FOLD crease patterns
from natural language prompts. Includes 11 ground-truth patterns
derived from the Optigami simulator and a multi-component reward
function using graph-based structural comparison.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files changed (4) hide show
  1. Dockerfile +12 -0
  2. README.md +34 -6
  3. app.py +524 -0
  4. requirements.txt +5 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,39 @@
1
  ---
2
- title: 'Optigami '
3
- emoji: 👁
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
8
- short_description: ver2.0
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Optigami
3
+ emoji: 🦢
4
+ colorFrom: indigo
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
+ license: mit
10
+ short_description: OpenEnv RL environment for origami crease patterns
11
  ---
12
 
13
+ # Optigami - OpenEnv RL Environment for Origami Crease Patterns
14
+
15
+ An RL training environment built with [OpenEnv](https://github.com/meta-pytorch/OpenEnv) that teaches LLMs to generate valid `.fold` crease patterns from natural language prompts.
16
+
17
+ ## API Endpoints
18
+
19
+ - `POST /reset` - Get a new origami prompt
20
+ - `POST /step` - Submit a generated FOLD JSON and receive a reward
21
+
22
+ ## Reward Structure
23
+
24
+ | Condition | Reward |
25
+ |---|---|
26
+ | Invalid JSON | -2.0 |
27
+ | Missing required FOLD fields | -1.0 |
28
+ | Valid FOLD with structural comparison | -1.0 to 1.0 |
29
+
30
+ ## Usage with OpenEnv Client
31
+
32
+ ```python
33
+ from openenv.core import EnvClient
34
+
35
+ env = EnvClient("https://openenv-community-optigami-.hf.space")
36
+ obs = env.reset()
37
+ result = env.step('{"vertices_coords": [...], "edges_vertices": [...], ...}')
38
+ print(result.reward)
39
+ ```
app.py ADDED
@@ -0,0 +1,524 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from collections import Counter
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import numpy as np
6
+ import networkx as nx
7
+ from pydantic import Field
8
+
9
+ from openenv.core import (
10
+ Action,
11
+ Environment,
12
+ Observation,
13
+ State,
14
+ create_fastapi_app,
15
+ )
16
+
17
+ # ---------------------------------------------------------------------------
18
+ # Custom Action / Observation models
19
+ # ---------------------------------------------------------------------------
20
+
21
+
22
+ class FoldAction(Action):
23
+ """An action that contains a generated FOLD JSON string."""
24
+
25
+ fold_json: str = Field(description="A JSON string in the FOLD format")
26
+
27
+
28
+ class FoldObservation(Observation):
29
+ """Observation returned by the origami environment."""
30
+
31
+ prompt: str = Field(default="", description="The origami prompt to fulfill")
32
+ info: Dict[str, Any] = Field(default_factory=dict, description="Extra info")
33
+
34
+
35
+ # ---------------------------------------------------------------------------
36
+ # Dataset: ground-truth origami crease patterns converted from the Optigami
37
+ # TypeScript codebase (lib/patterns.ts). Each entry has a natural-language
38
+ # prompt and a target FOLD-format dict.
39
+ # ---------------------------------------------------------------------------
40
+
41
+ DATASET: List[Dict[str, Any]] = [
42
+ {
43
+ "prompt": "Create a simple valley fold across the center of a square sheet",
44
+ "target": {
45
+ "vertices_coords": [[-1, 1], [1, 1], [-1, 0], [1, 0], [-1, -1], [1, -1]],
46
+ "faces_vertices": [[0, 2, 3], [0, 3, 1], [2, 4, 5], [2, 5, 3]],
47
+ "edges_vertices": [
48
+ [0, 2], [2, 3], [3, 0], [0, 1], [1, 3],
49
+ [2, 4], [4, 5], [5, 3],
50
+ ],
51
+ "edges_assignment": ["B", "V", "F", "B", "B", "B", "B", "F"],
52
+ },
53
+ },
54
+ {
55
+ "prompt": "Make an accordion fold with alternating mountain and valley creases",
56
+ "target": {
57
+ "vertices_coords": [
58
+ [-1, 1], [-0.5, 1], [0, 1], [0.5, 1], [1, 1],
59
+ [-1, -1], [-0.5, -1], [0, -1], [0.5, -1], [1, -1],
60
+ ],
61
+ "faces_vertices": [
62
+ [0, 1, 6], [0, 6, 5],
63
+ [1, 2, 7], [1, 7, 6],
64
+ [2, 3, 8], [2, 8, 7],
65
+ [3, 4, 9], [3, 9, 8],
66
+ ],
67
+ "edges_vertices": [
68
+ [0, 1], [1, 6], [6, 0], [0, 5], [5, 6],
69
+ [1, 2], [2, 7], [7, 1], [6, 7],
70
+ [2, 3], [3, 8], [8, 2], [7, 8],
71
+ [3, 4], [4, 9], [9, 3], [8, 9],
72
+ ],
73
+ "edges_assignment": [
74
+ "B", "V", "F", "B", "B",
75
+ "B", "M", "F", "F",
76
+ "B", "V", "F", "F",
77
+ "B", "B", "F", "B",
78
+ ],
79
+ },
80
+ },
81
+ {
82
+ "prompt": "Create a waterbomb base with valley folds on diagonals and mountain folds on midlines",
83
+ "target": {
84
+ "vertices_coords": [
85
+ [-1, 1], [0, 1], [1, 1],
86
+ [-1, 0], [0, 0], [1, 0],
87
+ [-1, -1], [0, -1], [1, -1],
88
+ ],
89
+ "faces_vertices": [
90
+ [0, 1, 4], [1, 2, 4],
91
+ [2, 5, 4], [5, 8, 4],
92
+ [8, 7, 4], [7, 6, 4],
93
+ [6, 3, 4], [3, 0, 4],
94
+ ],
95
+ "edges_vertices": [
96
+ [0, 1], [1, 4], [4, 0],
97
+ [1, 2], [2, 4],
98
+ [2, 5], [5, 4],
99
+ [5, 8], [8, 4],
100
+ [8, 7], [7, 4],
101
+ [7, 6], [6, 4],
102
+ [6, 3], [3, 4],
103
+ [3, 0],
104
+ ],
105
+ "edges_assignment": [
106
+ "B", "M", "V",
107
+ "B", "V",
108
+ "B", "M",
109
+ "B", "V",
110
+ "B", "M",
111
+ "B", "V",
112
+ "B", "M",
113
+ "B",
114
+ ],
115
+ },
116
+ },
117
+ {
118
+ "prompt": "Make a preliminary fold (square base) with mountain diagonals and valley midlines",
119
+ "target": {
120
+ "vertices_coords": [
121
+ [-1, 1], [0, 1], [1, 1],
122
+ [-1, 0], [0, 0], [1, 0],
123
+ [-1, -1], [0, -1], [1, -1],
124
+ ],
125
+ "faces_vertices": [
126
+ [0, 1, 4], [1, 2, 4],
127
+ [2, 5, 4], [5, 8, 4],
128
+ [8, 7, 4], [7, 6, 4],
129
+ [6, 3, 4], [3, 0, 4],
130
+ ],
131
+ "edges_vertices": [
132
+ [0, 1], [1, 4], [4, 0],
133
+ [1, 2], [2, 4],
134
+ [2, 5], [5, 4],
135
+ [5, 8], [8, 4],
136
+ [8, 7], [7, 4],
137
+ [7, 6], [6, 4],
138
+ [6, 3], [3, 4],
139
+ [3, 0],
140
+ ],
141
+ "edges_assignment": [
142
+ "B", "V", "M",
143
+ "B", "M",
144
+ "B", "V",
145
+ "B", "M",
146
+ "B", "V",
147
+ "B", "M",
148
+ "B", "V",
149
+ "B",
150
+ ],
151
+ },
152
+ },
153
+ {
154
+ "prompt": "Create a Miura-ori tessellation fold pattern on a 2x2 grid",
155
+ "target": {
156
+ "vertices_coords": [
157
+ [-1, 1], [0, 1.2], [1, 1],
158
+ [-1, 0], [0, 0.2], [1, 0],
159
+ [-1, -1], [0, -0.8], [1, -1],
160
+ ],
161
+ "faces_vertices": [
162
+ [0, 1, 4], [0, 4, 3],
163
+ [1, 2, 5], [1, 5, 4],
164
+ [3, 4, 7], [3, 7, 6],
165
+ [4, 5, 8], [4, 8, 7],
166
+ ],
167
+ "edges_vertices": [
168
+ [0, 1], [1, 4], [4, 0], [0, 3],
169
+ [1, 2], [2, 5], [5, 1], [4, 5],
170
+ [3, 4], [4, 7], [7, 3], [3, 6],
171
+ [6, 7], [7, 4], [5, 8], [8, 7],
172
+ ],
173
+ "edges_assignment": [
174
+ "B", "M", "F", "B",
175
+ "B", "B", "F", "M",
176
+ "V", "M", "F", "B",
177
+ "B", "F", "B", "B",
178
+ ],
179
+ },
180
+ },
181
+ {
182
+ "prompt": "Design a bird base crease pattern with alternating mountain and valley folds radiating from center",
183
+ "target": {
184
+ "vertices_coords": [
185
+ [-1, 1], [0, 1], [1, 1],
186
+ [-1, 0], [0, 0], [1, 0],
187
+ [-1, -1], [0, -1], [1, -1],
188
+ [-0.5, 0.5], [0.5, 0.5], [-0.5, -0.5], [0.5, -0.5],
189
+ ],
190
+ "faces_vertices": [
191
+ [0, 9, 3], [0, 1, 9], [1, 4, 9], [3, 9, 4],
192
+ [1, 10, 4], [1, 2, 10], [2, 5, 10], [4, 10, 5],
193
+ [3, 4, 11], [3, 11, 6], [6, 11, 7], [4, 7, 11],
194
+ [4, 5, 12], [5, 8, 12], [8, 7, 12], [4, 12, 7],
195
+ ],
196
+ "edges_vertices": [
197
+ [0, 9], [9, 3], [0, 1], [1, 9], [1, 4], [9, 4],
198
+ [1, 10], [1, 2], [2, 10], [2, 5], [5, 10], [10, 4],
199
+ [3, 4], [3, 11], [11, 6], [6, 7], [7, 11], [11, 4],
200
+ [4, 5], [4, 12], [5, 12], [5, 8], [8, 12], [12, 7], [7, 4],
201
+ ],
202
+ "edges_assignment": [
203
+ "V", "M", "B", "M", "M", "V",
204
+ "M", "B", "V", "B", "M", "V",
205
+ "M", "M", "V", "B", "M", "V",
206
+ "M", "V", "M", "B", "V", "M", "M",
207
+ ],
208
+ },
209
+ },
210
+ {
211
+ "prompt": "Create a hyperbolic paraboloid (hypar) fold on a 4x4 grid with alternating mountain and valley creases",
212
+ "target": {
213
+ "vertices_coords": [
214
+ [-1, 1], [-0.5, 1], [0, 1], [0.5, 1], [1, 1],
215
+ [-1, 0.5], [-0.5, 0.5], [0, 0.5], [0.5, 0.5], [1, 0.5],
216
+ [-1, 0], [-0.5, 0], [0, 0], [0.5, 0], [1, 0],
217
+ [-1, -0.5], [-0.5, -0.5], [0, -0.5], [0.5, -0.5], [1, -0.5],
218
+ [-1, -1], [-0.5, -1], [0, -1], [0.5, -1], [1, -1],
219
+ ],
220
+ "faces_vertices": [
221
+ [0, 1, 6], [0, 6, 5], [1, 2, 7], [1, 7, 6],
222
+ [2, 3, 8], [2, 8, 7], [3, 4, 9], [3, 9, 8],
223
+ [5, 6, 11], [5, 11, 10], [6, 7, 12], [6, 12, 11],
224
+ [7, 8, 13], [7, 13, 12], [8, 9, 14], [8, 14, 13],
225
+ [10, 11, 16], [10, 16, 15], [11, 12, 17], [11, 17, 16],
226
+ [12, 13, 18], [12, 18, 17], [13, 14, 19], [13, 19, 18],
227
+ [15, 16, 21], [15, 21, 20], [16, 17, 22], [16, 22, 21],
228
+ [17, 18, 23], [17, 23, 22], [18, 19, 24], [18, 24, 23],
229
+ ],
230
+ "edges_vertices": [
231
+ [0, 1], [1, 6], [6, 0], [0, 5], [5, 6],
232
+ [1, 2], [2, 7], [7, 1], [6, 7],
233
+ [2, 3], [3, 8], [8, 2], [7, 8],
234
+ [3, 4], [4, 9], [9, 3], [8, 9],
235
+ [5, 11], [10, 11], [6, 11],
236
+ [6, 12], [11, 12], [7, 12],
237
+ [7, 13], [12, 13], [8, 13],
238
+ [8, 14], [13, 14], [9, 14],
239
+ [5, 10], [10, 16], [10, 15], [15, 16], [11, 16],
240
+ [11, 17], [16, 17], [12, 17],
241
+ [12, 18], [17, 18], [13, 18],
242
+ [13, 19], [18, 19], [14, 19],
243
+ [15, 21], [15, 20], [20, 21], [16, 21],
244
+ [16, 22], [21, 22], [17, 22],
245
+ [17, 23], [22, 23], [18, 23],
246
+ [18, 24], [23, 24], [19, 24],
247
+ ],
248
+ "edges_assignment": [
249
+ "B", "M", "F", "B", "V",
250
+ "B", "V", "F", "V",
251
+ "B", "M", "F", "V",
252
+ "B", "B", "F", "V",
253
+ "F", "M", "M",
254
+ "V", "M", "V",
255
+ "M", "M", "M",
256
+ "F", "M", "B",
257
+ "B", "F", "B", "V", "V",
258
+ "M", "V", "V",
259
+ "V", "V", "V",
260
+ "F", "V", "B",
261
+ "F", "B", "B", "V",
262
+ "M", "V", "V",
263
+ "V", "V", "V",
264
+ "F", "B", "B",
265
+ ],
266
+ },
267
+ },
268
+ # --- Additional prompt variations for training diversity ---
269
+ {
270
+ "prompt": "Fold a piece of paper in half horizontally",
271
+ "target": {
272
+ "vertices_coords": [[-1, 1], [1, 1], [-1, 0], [1, 0], [-1, -1], [1, -1]],
273
+ "faces_vertices": [[0, 2, 3], [0, 3, 1], [2, 4, 5], [2, 5, 3]],
274
+ "edges_vertices": [
275
+ [0, 2], [2, 3], [3, 0], [0, 1], [1, 3],
276
+ [2, 4], [4, 5], [5, 3],
277
+ ],
278
+ "edges_assignment": ["B", "V", "F", "B", "B", "B", "B", "F"],
279
+ },
280
+ },
281
+ {
282
+ "prompt": "Make a zig-zag fold like a paper fan",
283
+ "target": {
284
+ "vertices_coords": [
285
+ [-1, 1], [-0.5, 1], [0, 1], [0.5, 1], [1, 1],
286
+ [-1, -1], [-0.5, -1], [0, -1], [0.5, -1], [1, -1],
287
+ ],
288
+ "faces_vertices": [
289
+ [0, 1, 6], [0, 6, 5],
290
+ [1, 2, 7], [1, 7, 6],
291
+ [2, 3, 8], [2, 8, 7],
292
+ [3, 4, 9], [3, 9, 8],
293
+ ],
294
+ "edges_vertices": [
295
+ [0, 1], [1, 6], [6, 0], [0, 5], [5, 6],
296
+ [1, 2], [2, 7], [7, 1], [6, 7],
297
+ [2, 3], [3, 8], [8, 2], [7, 8],
298
+ [3, 4], [4, 9], [9, 3], [8, 9],
299
+ ],
300
+ "edges_assignment": [
301
+ "B", "V", "F", "B", "B",
302
+ "B", "M", "F", "F",
303
+ "B", "V", "F", "F",
304
+ "B", "B", "F", "B",
305
+ ],
306
+ },
307
+ },
308
+ {
309
+ "prompt": "Create the base for a paper crane",
310
+ "target": {
311
+ "vertices_coords": [
312
+ [-1, 1], [0, 1], [1, 1],
313
+ [-1, 0], [0, 0], [1, 0],
314
+ [-1, -1], [0, -1], [1, -1],
315
+ [-0.5, 0.5], [0.5, 0.5], [-0.5, -0.5], [0.5, -0.5],
316
+ ],
317
+ "faces_vertices": [
318
+ [0, 9, 3], [0, 1, 9], [1, 4, 9], [3, 9, 4],
319
+ [1, 10, 4], [1, 2, 10], [2, 5, 10], [4, 10, 5],
320
+ [3, 4, 11], [3, 11, 6], [6, 11, 7], [4, 7, 11],
321
+ [4, 5, 12], [5, 8, 12], [8, 7, 12], [4, 12, 7],
322
+ ],
323
+ "edges_vertices": [
324
+ [0, 9], [9, 3], [0, 1], [1, 9], [1, 4], [9, 4],
325
+ [1, 10], [1, 2], [2, 10], [2, 5], [5, 10], [10, 4],
326
+ [3, 4], [3, 11], [11, 6], [6, 7], [7, 11], [11, 4],
327
+ [4, 5], [4, 12], [5, 12], [5, 8], [8, 12], [12, 7], [7, 4],
328
+ ],
329
+ "edges_assignment": [
330
+ "V", "M", "B", "M", "M", "V",
331
+ "M", "B", "V", "B", "M", "V",
332
+ "M", "M", "V", "B", "M", "V",
333
+ "M", "V", "M", "B", "V", "M", "M",
334
+ ],
335
+ },
336
+ },
337
+ {
338
+ "prompt": "Make a simple diagonal fold from corner to corner",
339
+ "target": {
340
+ "vertices_coords": [[0, 0], [1, 0], [1, 1], [0, 1]],
341
+ "faces_vertices": [[0, 1, 2], [0, 2, 3]],
342
+ "edges_vertices": [[0, 1], [1, 2], [2, 3], [3, 0], [0, 2]],
343
+ "edges_assignment": ["B", "B", "B", "B", "V"],
344
+ },
345
+ },
346
+ ]
347
+
348
+
349
+ # ---------------------------------------------------------------------------
350
+ # Reward calculation
351
+ # ---------------------------------------------------------------------------
352
+
353
+
354
+ def _build_graph(fold_data: dict) -> nx.Graph:
355
+ """Convert FOLD data to a networkx graph for structural comparison."""
356
+ G = nx.Graph()
357
+ verts = fold_data.get("vertices_coords", [])
358
+ edges = fold_data.get("edges_vertices", [])
359
+ assignments = fold_data.get("edges_assignment", [])
360
+
361
+ for i, v in enumerate(verts):
362
+ G.add_node(i, x=float(v[0]), y=float(v[1]))
363
+
364
+ for i, e in enumerate(edges):
365
+ a = assignments[i] if i < len(assignments) else "U"
366
+ G.add_edge(int(e[0]), int(e[1]), assignment=a)
367
+
368
+ return G
369
+
370
+
371
+ def _edge_overlap_score(gen_graph: nx.Graph, target_graph: nx.Graph) -> float:
372
+ """
373
+ Compute a normalised edge-overlap score based on assignment histogram IoU.
374
+ """
375
+ gen_assign = sorted(
376
+ [d.get("assignment", "U") for _, _, d in gen_graph.edges(data=True)]
377
+ )
378
+ tgt_assign = sorted(
379
+ [d.get("assignment", "U") for _, _, d in target_graph.edges(data=True)]
380
+ )
381
+
382
+ gen_counts = Counter(gen_assign)
383
+ tgt_counts = Counter(tgt_assign)
384
+
385
+ all_keys = set(gen_counts.keys()) | set(tgt_counts.keys())
386
+ intersection = sum(min(gen_counts[k], tgt_counts[k]) for k in all_keys)
387
+ union = sum(max(gen_counts[k], tgt_counts[k]) for k in all_keys)
388
+
389
+ if union == 0:
390
+ return 0.0
391
+ return intersection / union
392
+
393
+
394
+ def calculate_fold_reward(generated_json: str, target_json: dict) -> float:
395
+ """
396
+ Multi-component reward for comparing a generated FOLD JSON to a target.
397
+
398
+ Components:
399
+ 1. JSON validity (-2.0 penalty if invalid)
400
+ 2. Required fields (-1.0 penalty if missing)
401
+ 3. Vertex count similarity (0 - 0.3)
402
+ 4. Edge count similarity (0 - 0.3)
403
+ 5. Assignment overlap IoU (0 - 0.4)
404
+
405
+ Total reward clipped to [-2.0, 1.0].
406
+ """
407
+ try:
408
+ gen_data = json.loads(generated_json)
409
+ except (json.JSONDecodeError, TypeError):
410
+ return -2.0
411
+
412
+ for field in ("vertices_coords", "edges_vertices"):
413
+ if field not in gen_data:
414
+ return -1.0
415
+
416
+ reward = 0.0
417
+
418
+ # Vertex count similarity (max 0.3)
419
+ gen_v = len(gen_data.get("vertices_coords", []))
420
+ tgt_v = len(target_json.get("vertices_coords", []))
421
+ if tgt_v > 0:
422
+ v_ratio = 1.0 - min(abs(gen_v - tgt_v) / tgt_v, 1.0)
423
+ reward += v_ratio * 0.3
424
+
425
+ # Edge count similarity (max 0.3)
426
+ gen_e = len(gen_data.get("edges_vertices", []))
427
+ tgt_e = len(target_json.get("edges_vertices", []))
428
+ if tgt_e > 0:
429
+ e_ratio = 1.0 - min(abs(gen_e - tgt_e) / tgt_e, 1.0)
430
+ reward += e_ratio * 0.3
431
+
432
+ # Assignment overlap (max 0.4)
433
+ try:
434
+ gen_graph = _build_graph(gen_data)
435
+ tgt_graph = _build_graph(target_json)
436
+ overlap = _edge_overlap_score(gen_graph, tgt_graph)
437
+ reward += overlap * 0.4
438
+ except Exception:
439
+ pass
440
+
441
+ return max(-2.0, min(1.0, reward))
442
+
443
+
444
+ # ---------------------------------------------------------------------------
445
+ # OpenEnv Environment
446
+ # ---------------------------------------------------------------------------
447
+
448
+
449
+ class OrigamiEnv(Environment[FoldAction, FoldObservation, State]):
450
+ """
451
+ Single-turn RL environment for origami crease-pattern generation.
452
+
453
+ reset() -> returns a natural-language prompt as the observation.
454
+ step() -> accepts FOLD JSON, compares to ground truth, returns reward.
455
+ """
456
+
457
+ def __init__(self) -> None:
458
+ super().__init__()
459
+ self.dataset = DATASET
460
+ self.current_idx = 0
461
+ self._step_count = 0
462
+ self._episode_id: Optional[str] = None
463
+
464
+ def reset(
465
+ self,
466
+ seed: Optional[int] = None,
467
+ episode_id: Optional[str] = None,
468
+ **kwargs: Any,
469
+ ) -> FoldObservation:
470
+ if seed is not None:
471
+ np.random.seed(seed)
472
+ self.current_idx = int(np.random.randint(len(self.dataset)))
473
+ self._step_count = 0
474
+ self._episode_id = episode_id
475
+ return FoldObservation(
476
+ prompt=self.dataset[self.current_idx]["prompt"],
477
+ done=False,
478
+ reward=None,
479
+ info={
480
+ "target_vertex_count": len(
481
+ self.dataset[self.current_idx]["target"]["vertices_coords"]
482
+ ),
483
+ "target_edge_count": len(
484
+ self.dataset[self.current_idx]["target"]["edges_vertices"]
485
+ ),
486
+ },
487
+ )
488
+
489
+ def step(
490
+ self,
491
+ action: FoldAction,
492
+ timeout_s: Optional[float] = None,
493
+ **kwargs: Any,
494
+ ) -> FoldObservation:
495
+ target = self.dataset[self.current_idx]["target"]
496
+ reward = calculate_fold_reward(action.fold_json, target)
497
+ self._step_count += 1
498
+ return FoldObservation(
499
+ prompt="Episode finished.",
500
+ done=True,
501
+ reward=reward,
502
+ info={
503
+ "prompt": self.dataset[self.current_idx]["prompt"],
504
+ "target_vertex_count": len(target["vertices_coords"]),
505
+ "target_edge_count": len(target["edges_vertices"]),
506
+ },
507
+ )
508
+
509
+ def state(self) -> State:
510
+ return State(
511
+ episode_id=self._episode_id,
512
+ step_count=self._step_count,
513
+ )
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # Create the FastAPI application
518
+ # ---------------------------------------------------------------------------
519
+
520
+ app = create_fastapi_app(
521
+ env=OrigamiEnv,
522
+ action_cls=FoldAction,
523
+ observation_cls=FoldObservation,
524
+ )
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ openenv-core>=0.2.1
2
+ fastapi
3
+ uvicorn
4
+ numpy
5
+ networkx