feiyang-cai commited on
Commit
de8cb80
·
1 Parent(s): f6ee655

Fix caching: avoid rerunning inverse design during thermoforming

Browse files
Files changed (2) hide show
  1. .gitignore +15 -0
  2. inverse_design_demo/app.py +176 -52
.gitignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ .Python
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .mypy_cache/
10
+ .ipynb_checkpoints/
11
+
12
+ # OS/editor
13
+ .DS_Store
14
+ .vscode/
15
+ .idea/
inverse_design_demo/app.py CHANGED
@@ -200,6 +200,8 @@ if 'input_curve_button_clicked' not in st.session_state:
200
  st.session_state.input_curve_button_clicked= False
201
  def input_curve_click():
202
  st.session_state.input_curve_button_clicked = True
 
 
203
 
204
  if 'material_design_button_clicked' not in st.session_state:
205
  st.session_state.material_design_button_clicked= False
@@ -217,6 +219,8 @@ if 'forming_design_button_clicked' not in st.session_state:
217
  st.session_state.forming_design_button_clicked= False
218
  def forming_design_click():
219
  st.session_state.forming_design_button_clicked = True
 
 
220
 
221
 
222
 
@@ -236,6 +240,36 @@ angle=30
236
  #######################
237
  # Backend helpers (no UI changes)
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  DEFAULT_CHECKPOINT_DIR = os.environ.get(
240
  "MG_CHECKPOINT_DIR",
241
  os.path.join(
@@ -246,9 +280,14 @@ DEFAULT_CHECKPOINT_DIR = os.environ.get(
246
  "exp_20260114_164540",
247
  ),
248
  )
 
 
 
 
 
249
  DEFAULT_DATA_DIR = os.environ.get(
250
  "MG_DATA_DIR",
251
- os.path.join(ROOT_DIR, "data_generation", "processed_dataset", "config_1"),
252
  )
253
  DEFAULT_CURVE_DIR = os.environ.get(
254
  "MG_CURVE_DIR",
@@ -463,6 +502,18 @@ def _load_poly_metadata_small(data_dir: str) -> dict:
463
  meta_path = os.path.join(str(data_dir), "metadata.json")
464
  if not os.path.exists(meta_path):
465
  raise FileNotFoundError(f"metadata.json not found in {data_dir}")
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  # These keys live very early in the file (per grep): keep this small for Streamlit responsiveness.
468
  keys = [
@@ -475,16 +526,39 @@ def _load_poly_metadata_small(data_dir: str) -> dict:
475
  "angle_max",
476
  ]
477
 
 
 
478
  prefix_bytes = 256_000
 
479
  with open(meta_path, "r") as f:
480
- prefix = f.read(prefix_bytes)
481
-
482
- out = {}
483
- for k in keys:
484
- raw = _extract_json_value_prefix(prefix, k)
485
- # json.loads handles arrays and numeric scalars (including scientific notation)
486
- out[k] = json.loads(raw)
487
- return out
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
488
 
489
 
490
  @st.cache_resource(show_spinner=False)
@@ -819,43 +893,72 @@ if st.session_state.input_changed == True:
819
  del st.session_state["inverse_design_result"]
820
  if "material_design_last_run_id" in st.session_state:
821
  del st.session_state["material_design_last_run_id"]
 
 
 
 
 
 
822
  st.session_state.input_changed = False
823
 
824
  st.button("Generate required stress-strain curves", use_container_width=True, on_click=input_curve_click)
825
 
826
  if st.session_state.input_curve_button_clicked == True:
827
- #st.write(E1aV)
828
- #st.write(E1bV)
829
- n_pts = int(max(4, N_INPUT_POINTS))
830
- x = np.linspace(0, 0.1, n_pts)
831
- A = np.array([[0.2, 0.03], [0.01, 0.001]])
832
- b = np.array([E1bV-E1aV, S1bV-E1aV*0.1])
833
- a = np.linalg.solve(A, b)
834
- y1= E1aV*x + a[0]*x**2 + a[1]*x**3
835
- b = np.array([E2bV-E2aV, S2bV-E2aV*0.1])
836
- a = np.linalg.solve(A, b)
837
- y2= E2aV*x + a[0]*x**2 + a[1]*x**3
838
- b = np.array([G12bV-G12aV, S12bV-G12aV*0.1])
839
- a = np.linalg.solve(A, b)
840
- y3= G12aV*x + a[0]*x**2 + a[1]*x**3
841
-
842
- y4 = v12aV*x + (v12bV - v12aV)/0.2*x*x
843
- y5 = v21aV*x + (v21bV - v21aV)/0.2*x*x
844
-
845
- # Cache required curves for inverse design (model conditioning)
846
- st.session_state.required_curves = {
847
- "eps11": x.astype(np.float32),
848
- "sig11_mpa": y1.astype(np.float32),
849
- "eps22": x.astype(np.float32),
850
- "sig22_mpa": y2.astype(np.float32),
851
- "eps12": x.astype(np.float32),
852
- "sig12_mpa": y3.astype(np.float32),
853
- "eps22_from_eps11": (-y4).astype(np.float32),
854
- "eps11_from_eps22": (-y5).astype(np.float32),
855
- }
856
- # New curves -> invalidate any previous inverse-design result
857
- if "inverse_design_result" in st.session_state:
858
- del st.session_state["inverse_design_result"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
 
860
 
861
  #ylimit=np.max([np.max(y1),np.max(y2), np.max(y3)])
@@ -1102,19 +1205,27 @@ if st.session_state.input_curve_button_clicked == True:
1102
  st.write("")
1103
  st.button("Thermoforming Requirements", use_container_width=True, on_click=forming_input_click)
1104
  if st.session_state.forming_input_button_clicked == True:
 
 
 
 
1105
  #st.write("")
1106
  # 4th row with 3 columns
1107
  col1_row4, col2_row4, col3_row4, col4_row4, col5_row4 = st.columns([0.16,0.16,0.2,0.24,0.24])
1108
  with col1_row4:
1109
  with st.container(border=False): # Container with a border
1110
- st.write("Matrix material", "PEEK")
1111
- st.write("Fiber material=", "Carbon")
1112
- st.write("Number of layers=", nlayers)
1113
- st.write("Volume fraction=", vf)
 
 
 
1114
  with col2_row4:
1115
  with st.container(border=False): # Container with a border
1116
  df = pd.DataFrame({'Ply': [], 'Orientation': []})
1117
- plies = np.array([[1,90], [2,45], [3,-45], [4,-90]])
 
1118
  plies_df=pd.DataFrame(plies, columns=df.columns)
1119
  df = pd.concat([df, plies_df], ignore_index=True)
1120
  st.dataframe(df, hide_index=True)
@@ -1139,15 +1250,28 @@ if st.session_state.input_curve_button_clicked == True:
1139
 
1140
  st.write("")
1141
  if st.session_state.forming_input_changed == True:
1142
- st.session_state.forming_design_button_clicked = False
 
 
 
 
1143
  st.session_state.forming_input_changed = False
1144
  st.button("Thermoforming process design", use_container_width=True, on_click=forming_design_click)
1145
  if st.session_state.forming_design_button_clicked == True:
1146
- best = inverse_design(ply_number=nlayers,
1147
- fiber_vf=vf,
1148
- y_target=[angleA, angleB, angleC, max_stress],
1149
- n_restarts=5,
1150
- epochs=100)
 
 
 
 
 
 
 
 
 
1151
  # 5th row with 3 columns
1152
  col1_row5, col2_row5,col3_row5 = st.columns([0.25,0.25,0.25])
1153
  with col1_row5:
 
200
  st.session_state.input_curve_button_clicked= False
201
  def input_curve_click():
202
  st.session_state.input_curve_button_clicked = True
203
+ # allow repeated attempts: each click increments run id
204
+ st.session_state.input_curve_run_id = int(st.session_state.get("input_curve_run_id", 0)) + 1
205
 
206
  if 'material_design_button_clicked' not in st.session_state:
207
  st.session_state.material_design_button_clicked= False
 
219
  st.session_state.forming_design_button_clicked= False
220
  def forming_design_click():
221
  st.session_state.forming_design_button_clicked = True
222
+ # allow repeated attempts: each click increments run id
223
+ st.session_state.forming_design_run_id = int(st.session_state.get("forming_design_run_id", 0)) + 1
224
 
225
 
226
 
 
240
  #######################
241
  # Backend helpers (no UI changes)
242
 
243
+ def _is_git_lfs_pointer_file(path: str) -> bool:
244
+ """
245
+ Hugging Face / git checkouts sometimes contain Git-LFS pointer files instead of real data.
246
+ Detect that case so we can fall back to a real dataset path (or raise a clearer error).
247
+ """
248
+ try:
249
+ if not os.path.isfile(path):
250
+ return False
251
+ # Pointer files are tiny (a few hundred bytes).
252
+ if os.path.getsize(path) > 2048:
253
+ return False
254
+ with open(path, "r") as f:
255
+ head = f.read(256)
256
+ return "git-lfs.github.com/spec/v1" in head
257
+ except Exception:
258
+ return False
259
+
260
+
261
+ def _choose_first_real_data_dir(candidates: list[str]) -> str:
262
+ """
263
+ Pick the first candidate that contains a non-LFS metadata.json.
264
+ Returns the first candidate if none match (caller may still error later with a clear message).
265
+ """
266
+ for d in candidates:
267
+ meta = os.path.join(str(d), "metadata.json")
268
+ if os.path.exists(meta) and (not _is_git_lfs_pointer_file(meta)):
269
+ return str(d)
270
+ return str(candidates[0]) if candidates else ""
271
+
272
+
273
  DEFAULT_CHECKPOINT_DIR = os.environ.get(
274
  "MG_CHECKPOINT_DIR",
275
  os.path.join(
 
280
  "exp_20260114_164540",
281
  ),
282
  )
283
+
284
+ # NOTE: The Space repo can contain LFS pointers for large datasets. Prefer a "real" dataset
285
+ # if available (commonly checked out at the MaterialGeneration root alongside this repo).
286
+ _default_data_dir_space = os.path.join(ROOT_DIR, "data_generation", "processed_dataset", "config_1")
287
+ _default_data_dir_root = os.path.abspath(os.path.join(ROOT_DIR, "..", "data_generation", "processed_dataset", "config_1"))
288
  DEFAULT_DATA_DIR = os.environ.get(
289
  "MG_DATA_DIR",
290
+ _choose_first_real_data_dir([_default_data_dir_space, _default_data_dir_root]),
291
  )
292
  DEFAULT_CURVE_DIR = os.environ.get(
293
  "MG_CURVE_DIR",
 
502
  meta_path = os.path.join(str(data_dir), "metadata.json")
503
  if not os.path.exists(meta_path):
504
  raise FileNotFoundError(f"metadata.json not found in {data_dir}")
505
+ if _is_git_lfs_pointer_file(meta_path):
506
+ raise RuntimeError(
507
+ "\n".join(
508
+ [
509
+ "metadata.json looks like a Git-LFS pointer file, not the actual dataset.",
510
+ f"path: {meta_path}",
511
+ "Fix options:",
512
+ " - Point MG_DATA_DIR to a real dataset folder that contains the full metadata.json, or",
513
+ " - If this repo was cloned with git-lfs available, run: git lfs pull",
514
+ ]
515
+ )
516
+ )
517
 
518
  # These keys live very early in the file (per grep): keep this small for Streamlit responsiveness.
519
  keys = [
 
526
  "angle_max",
527
  ]
528
 
529
+ # Start small for responsiveness, but grow if needed (some metadata.json variants have these keys later).
530
+ max_prefix_bytes = 8_000_000
531
  prefix_bytes = 256_000
532
+ prefix = ""
533
  with open(meta_path, "r") as f:
534
+ while True:
535
+ prefix = f.read(prefix_bytes)
536
+ try:
537
+ out = {}
538
+ for k in keys:
539
+ raw = _extract_json_value_prefix(prefix, k)
540
+ out[k] = json.loads(raw)
541
+ return out
542
+ except KeyError:
543
+ # Not all keys in the current prefix; try a larger read.
544
+ if prefix_bytes >= max_prefix_bytes:
545
+ break
546
+ prefix_bytes = min(int(prefix_bytes * 2), max_prefix_bytes)
547
+ f.seek(0)
548
+
549
+ # Last resort: if we still couldn't find keys in a large prefix, try loading full JSON only
550
+ # when it won't explode memory.
551
+ try:
552
+ size = os.path.getsize(meta_path)
553
+ except Exception:
554
+ size = None
555
+ if size is not None and size <= 64_000_000:
556
+ with open(meta_path, "r") as f:
557
+ full = json.load(f)
558
+ return {k: full[k] for k in keys}
559
+ raise KeyError(
560
+ f"Could not locate required keys {keys} in the first {prefix_bytes} bytes of metadata.json at {meta_path}"
561
+ )
562
 
563
 
564
  @st.cache_resource(show_spinner=False)
 
893
  del st.session_state["inverse_design_result"]
894
  if "material_design_last_run_id" in st.session_state:
895
  del st.session_state["material_design_last_run_id"]
896
+ if "input_curve_last_run_id" in st.session_state:
897
+ del st.session_state["input_curve_last_run_id"]
898
+ if "forming_design_result" in st.session_state:
899
+ del st.session_state["forming_design_result"]
900
+ if "forming_design_last_run_id" in st.session_state:
901
+ del st.session_state["forming_design_last_run_id"]
902
  st.session_state.input_changed = False
903
 
904
  st.button("Generate required stress-strain curves", use_container_width=True, on_click=input_curve_click)
905
 
906
  if st.session_state.input_curve_button_clicked == True:
907
+ # Only (re)generate curves when the user explicitly clicks the button again,
908
+ # or when curves don't exist yet. Otherwise keep cached curves to avoid
909
+ # invalidating material inverse design on unrelated reruns (e.g. thermoforming clicks).
910
+ current_curve_run_id = int(st.session_state.get("input_curve_run_id", 0))
911
+ last_curve_run_id = int(st.session_state.get("input_curve_last_run_id", -1))
912
+ if ("required_curves" not in st.session_state) or (current_curve_run_id != last_curve_run_id):
913
+ #st.write(E1aV)
914
+ #st.write(E1bV)
915
+ n_pts = int(max(4, N_INPUT_POINTS))
916
+ x = np.linspace(0, 0.1, n_pts)
917
+ A = np.array([[0.2, 0.03], [0.01, 0.001]])
918
+ b = np.array([E1bV-E1aV, S1bV-E1aV*0.1])
919
+ a = np.linalg.solve(A, b)
920
+ y1= E1aV*x + a[0]*x**2 + a[1]*x**3
921
+ b = np.array([E2bV-E2aV, S2bV-E2aV*0.1])
922
+ a = np.linalg.solve(A, b)
923
+ y2= E2aV*x + a[0]*x**2 + a[1]*x**3
924
+ b = np.array([G12bV-G12aV, S12bV-G12aV*0.1])
925
+ a = np.linalg.solve(A, b)
926
+ y3= G12aV*x + a[0]*x**2 + a[1]*x**3
927
+
928
+ y4 = v12aV*x + (v12bV - v12aV)/0.2*x*x
929
+ y5 = v21aV*x + (v21bV - v21aV)/0.2*x*x
930
+
931
+ # Cache required curves for inverse design (model conditioning)
932
+ st.session_state.required_curves = {
933
+ "eps11": x.astype(np.float32),
934
+ "sig11_mpa": y1.astype(np.float32),
935
+ "eps22": x.astype(np.float32),
936
+ "sig22_mpa": y2.astype(np.float32),
937
+ "eps12": x.astype(np.float32),
938
+ "sig12_mpa": y3.astype(np.float32),
939
+ "eps22_from_eps11": (-y4).astype(np.float32),
940
+ "eps11_from_eps22": (-y5).astype(np.float32),
941
+ }
942
+ st.session_state.input_curve_last_run_id = current_curve_run_id
943
+
944
+ # New curves -> invalidate any previous inverse-design result
945
+ if "inverse_design_result" in st.session_state:
946
+ del st.session_state["inverse_design_result"]
947
+ if "material_design_last_run_id" in st.session_state:
948
+ del st.session_state["material_design_last_run_id"]
949
+ # ...and any downstream thermoforming process result (it depends on material design)
950
+ if "forming_design_result" in st.session_state:
951
+ del st.session_state["forming_design_result"]
952
+ if "forming_design_last_run_id" in st.session_state:
953
+ del st.session_state["forming_design_last_run_id"]
954
+ else:
955
+ curves = st.session_state.required_curves
956
+ x = np.asarray(curves["eps11"], dtype=np.float32)
957
+ y1 = np.asarray(curves["sig11_mpa"], dtype=np.float32)
958
+ y2 = np.asarray(curves["sig22_mpa"], dtype=np.float32)
959
+ y3 = np.asarray(curves["sig12_mpa"], dtype=np.float32)
960
+ y4 = -np.asarray(curves["eps22_from_eps11"], dtype=np.float32)
961
+ y5 = -np.asarray(curves["eps11_from_eps22"], dtype=np.float32)
962
 
963
 
964
  #ylimit=np.max([np.max(y1),np.max(y2), np.max(y3)])
 
1205
  st.write("")
1206
  st.button("Thermoforming Requirements", use_container_width=True, on_click=forming_input_click)
1207
  if st.session_state.forming_input_button_clicked == True:
1208
+ # Safety: thermoforming steps require a material inverse design result.
1209
+ if "inverse_design_result" not in st.session_state:
1210
+ st.warning("Please run 'Material Inverse Design' first. Thermoforming steps reuse that result and will not recompute it.")
1211
+ st.stop()
1212
  #st.write("")
1213
  # 4th row with 3 columns
1214
  col1_row4, col2_row4, col3_row4, col4_row4, col5_row4 = st.columns([0.16,0.16,0.2,0.24,0.24])
1215
  with col1_row4:
1216
  with st.container(border=False): # Container with a border
1217
+ # Reuse the cached material inverse design result (do not recompute)
1218
+ nlayers = int(len(res["full_angles"]))
1219
+ vf = float(res["vf"])
1220
+ st.write("Matrix material = ", res["matrix"])
1221
+ st.write("Fiber material = ", res["fiber"])
1222
+ st.write("Number of layers = ", nlayers)
1223
+ st.write("Volume fraction = ", f"{vf:.3f}")
1224
  with col2_row4:
1225
  with st.container(border=False): # Container with a border
1226
  df = pd.DataFrame({'Ply': [], 'Orientation': []})
1227
+ # Use the designed stacking sequence
1228
+ plies = np.array([[i + 1, float(a)] for i, a in enumerate(res["full_angles"])], dtype=object)
1229
  plies_df=pd.DataFrame(plies, columns=df.columns)
1230
  df = pd.concat([df, plies_df], ignore_index=True)
1231
  st.dataframe(df, hide_index=True)
 
1250
 
1251
  st.write("")
1252
  if st.session_state.forming_input_changed == True:
1253
+ # Any change to thermoforming requirements invalidates cached process design result
1254
+ if "forming_design_result" in st.session_state:
1255
+ del st.session_state["forming_design_result"]
1256
+ if "forming_design_last_run_id" in st.session_state:
1257
+ del st.session_state["forming_design_last_run_id"]
1258
  st.session_state.forming_input_changed = False
1259
  st.button("Thermoforming process design", use_container_width=True, on_click=forming_design_click)
1260
  if st.session_state.forming_design_button_clicked == True:
1261
+ # Cache thermoforming process design result so it doesn't rerun on every Streamlit rerender.
1262
+ current_run_id = int(st.session_state.get("forming_design_run_id", 0))
1263
+ last_run_id = int(st.session_state.get("forming_design_last_run_id", -1))
1264
+ if ("forming_design_result" not in st.session_state) or (current_run_id != last_run_id):
1265
+ with st.spinner("Optimizing thermoforming process parameters..."):
1266
+ st.session_state.forming_design_result = inverse_design(
1267
+ ply_number=nlayers,
1268
+ fiber_vf=vf,
1269
+ y_target=[angleA, angleB, angleC, max_stress],
1270
+ n_restarts=5,
1271
+ epochs=100,
1272
+ )
1273
+ st.session_state.forming_design_last_run_id = current_run_id
1274
+ best = st.session_state.forming_design_result
1275
  # 5th row with 3 columns
1276
  col1_row5, col2_row5,col3_row5 = st.columns([0.25,0.25,0.25])
1277
  with col1_row5: