AbhijitClemson commited on
Commit
1adc2e7
·
verified ·
1 Parent(s): 3ffff6e

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +12 -0
  2. .gitattributes +19 -35
  3. .gitignore +152 -0
  4. .streamlit/config.toml +14 -0
  5. Dockerfile +24 -0
  6. README.md +23 -4
  7. Showcase.gif +3 -0
  8. app.py +15 -0
  9. data_loader.py +116 -0
  10. db.py +48 -0
  11. images/AISI 1045 and AA5052_Ram Speed.png +3 -0
  12. AA5052_Strain Hardening Exponent (n).png +0 -0
  13. AA5052_Strength Coefficient (K).png +0 -0
  14. images/ASAAAA_Coefficient 'a' for average equivalent strain (ε̄_ave).png +3 -0
  15. images/ASAAAA_Diameter.png +0 -0
  16. images/ASAAAA_Flow Stress Equation (Lubricated).png +3 -0
  17. images/ASAAAA_Flow Stress Equation (Power's Law).png +3 -0
  18. images/ASAAAA_Flow Stress Equation (Sticking).png +0 -0
  19. images/ASAAAA_Strain hardening exponent (n).png +0 -0
  20. images/ASAAAA_Strength coefficient (K).png +0 -0
  21. images/Abhijit.jpg +0 -0
  22. images/Cynate Ester + 55% Carbon Fiber_Moisture Absorption at Equilibrium.png +0 -0
  23. images/Epoxy + 44% Carbon fiber_Tensile Strength.png +3 -0
  24. images/GangLi.jpg +0 -0
  25. images/Home.png +3 -0
  26. images/Materials_bg_InDeS.png +3 -0
  27. images/Mathias.jpg +0 -0
  28. images/PTFE_Water Absorption.png +0 -0
  29. images/Pradeep.jpg +0 -0
  30. images/Stress-strain-response-of-the-T300-3k-carbon-fiber-bundle.png +0 -0
  31. images/Tejaswi.jpg +0 -0
  32. images/Tensile-stress-strain-curve-of-Pure-PEEK-fibres-for-three-different-extrusion.png +0 -0
  33. images/desktop.ini +3 -0
  34. images/iPP_Curve Error (Maximum).png +3 -0
  35. images/iPP_Feature-to-curve reconstruction normalized error.png +0 -0
  36. images/iPP_Injection Pressure (Pinject) Range.png +3 -0
  37. images/iPP_Injection Rate (Rinject) Range.png +3 -0
  38. images/iPP_Melting temperature.png +3 -0
  39. images/iPP_Normalized Root Mean Square Error (NRMSE) Distribution Center.png +3 -0
  40. images/logo.png +3 -0
  41. images/us_deptenergy.jpg +3 -0
  42. logo.png +3 -0
  43. merged_file.json +0 -0
  44. page_files/Categorized_Search.py +900 -0
  45. page_files/Contact_Team.py +40 -0
  46. page_files/Home.py +722 -0
  47. page_files/Upload_Data.py +887 -0
  48. page_files/categorized/Backend/Pdf_DataExtraction.py +295 -0
  49. page_files/categorized/Backend/Pdf_ImageExtraction.py +390 -0
  50. page_files/categorized/Backend/upload_backend.py +356 -0
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ .gitattributes
4
+ .env
5
+ __pycache__/
6
+ *.pyc
7
+ *.pyo
8
+ *.pyd
9
+ .venv/
10
+ .hf_vendor/
11
+ outputs/
12
+ Showcase.gif
.gitattributes CHANGED
@@ -1,35 +1,19 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
3
+ images/AISI[[:space:]]1045[[:space:]]and[[:space:]]AA5052_Ram[[:space:]]Speed.png filter=lfs diff=lfs merge=lfs -text
4
+ images/ASAAAA_Coefficient[[:space:]]'a'[[:space:]]for[[:space:]]average[[:space:]]equivalent[[:space:]]strain[[:space:]](ε̄_ave).png filter=lfs diff=lfs merge=lfs -text
5
+ images/ASAAAA_Flow[[:space:]]Stress[[:space:]]Equation[[:space:]](Lubricated).png filter=lfs diff=lfs merge=lfs -text
6
+ images/ASAAAA_Flow[[:space:]]Stress[[:space:]]Equation[[:space:]](Power's[[:space:]]Law).png filter=lfs diff=lfs merge=lfs -text
7
+ images/Epoxy[[:space:]]+[[:space:]]44%[[:space:]]Carbon[[:space:]]fiber_Tensile[[:space:]]Strength.png filter=lfs diff=lfs merge=lfs -text
8
+ images/Home.png filter=lfs diff=lfs merge=lfs -text
9
+ images/iPP_Curve[[:space:]]Error[[:space:]](Maximum).png filter=lfs diff=lfs merge=lfs -text
10
+ images/iPP_Injection[[:space:]]Pressure[[:space:]](Pinject)[[:space:]]Range.png filter=lfs diff=lfs merge=lfs -text
11
+ images/iPP_Injection[[:space:]]Rate[[:space:]](Rinject)[[:space:]]Range.png filter=lfs diff=lfs merge=lfs -text
12
+ images/iPP_Melting[[:space:]]temperature.png filter=lfs diff=lfs merge=lfs -text
13
+ images/iPP_Normalized[[:space:]]Root[[:space:]]Mean[[:space:]]Square[[:space:]]Error[[:space:]](NRMSE)[[:space:]]Distribution[[:space:]]Center.png filter=lfs diff=lfs merge=lfs -text
14
+ images/logo.png filter=lfs diff=lfs merge=lfs -text
15
+ images/Materials_bg_InDeS.png filter=lfs diff=lfs merge=lfs -text
16
+ images/us_deptenergy.jpg filter=lfs diff=lfs merge=lfs -text
17
+ logo.png filter=lfs diff=lfs merge=lfs -text
18
+ page_files/categorized/ESS-min.jpg filter=lfs diff=lfs merge=lfs -text
19
+ Showcase.gif filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
105
+ __pypackages__/
106
+
107
+ # Celery stuff
108
+ celerybeat-schedule
109
+ celerybeat.pid
110
+
111
+ # SageMath parsed files
112
+ *.sage.py
113
+
114
+ # Environments
115
+ .env
116
+ .venv
117
+ env/
118
+ venv/
119
+ ENV/
120
+ env.bak/
121
+ venv.bak/
122
+
123
+ # Spyder project settings
124
+ .spyderproject
125
+ .spyproject
126
+
127
+ # Rope project settings
128
+ .ropeproject
129
+
130
+ # mkdocs documentation
131
+ /site
132
+
133
+ # mypy
134
+ .mypy_cache/
135
+ .dmypy.json
136
+ dmypy.json
137
+
138
+ # Pyre type checker
139
+ .pyre/
140
+
141
+ # pytype static type analyzer
142
+ .pytype/
143
+
144
+ # Cython debug symbols
145
+ cython_debug/
146
+
147
+ # PyCharm
148
+ # JetBrains specific template is maintainted in a separate JetBrains.gitignore that can
149
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
150
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
151
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
152
+ #.idea/
.streamlit/config.toml ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ base = "light"
3
+ primaryColor = "#1d4ed8"
4
+ backgroundColor = "#ffffff"
5
+ secondaryBackgroundColor = "#ffffff"
6
+ textColor = "#111827"
7
+ font = "sans serif"
8
+ borderColor = "rgba(0, 0, 0, 0)"
9
+ showWidgetBorder = false
10
+
11
+ [theme.sidebar]
12
+ backgroundColor = "#ffffff"
13
+ secondaryBackgroundColor = "#ffffff"
14
+ textColor = "#111827"
Dockerfile ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ STREAMLIT_BROWSER_GATHER_USAGE_STATS=false
7
+
8
+ WORKDIR /app
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ libglib2.0-0 \
12
+ libgl1 \
13
+ libsm6 \
14
+ libxext6 \
15
+ && rm -rf /var/lib/apt/lists/*
16
+
17
+ COPY requirements.txt /app/requirements.txt
18
+ RUN pip install --upgrade pip && pip install -r /app/requirements.txt
19
+
20
+ COPY . /app
21
+
22
+ EXPOSE 7860
23
+
24
+ CMD ["streamlit", "run", "app.py", "--server.address=0.0.0.0", "--server.port=7860"]
README.md CHANGED
@@ -1,10 +1,29 @@
1
  ---
2
  title: Mat Database
3
- emoji: 📚
4
- colorFrom: purple
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Mat Database
3
+ emoji: "🧪"
4
+ colorFrom: blue
5
+ colorTo: indigo
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  ---
10
 
11
+ # Mat Database
12
+
13
+ Streamlit application for browsing and uploading material properties data.
14
+
15
+ ## Local Run
16
+
17
+ ```bash
18
+ pip install -r requirements.txt
19
+ streamlit run app.py
20
+ ```
21
+
22
+ ## Environment Variables
23
+
24
+ - `GEMINI_API_KEY` (optional but required for PDF extraction with Gemini)
25
+ - `DB_HOST` (optional, required for database-backed browsing/upload)
26
+ - `DB_PORT` (optional, defaults to `5432`)
27
+ - `DB_NAME` (optional, required for database-backed browsing/upload)
28
+ - `DB_USER` (optional, required for database-backed browsing/upload)
29
+ - `DB_PASSWORD` (optional, required for database-backed browsing/upload)
Showcase.gif ADDED

Git LFS Details

  • SHA256: 27c97f8c4d109ef123d59a33fdd55c5893237e3ddef17714158d5e9c36869d25
  • Pointer size: 131 Bytes
  • Size of remote file: 251 kB
app.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+
3
+ st.set_page_config(layout="wide", initial_sidebar_state="collapsed")
4
+
5
+ pages = {
6
+ "": [
7
+ st.Page("page_files/Home.py", title="Home"),
8
+ st.Page("page_files/Categorized_Search.py", title="Categorized Search"),
9
+ st.Page("page_files/Upload_Data.py", title="Upload Data"),
10
+ st.Page("page_files/Contact_Team.py", title="Contact Team"),
11
+ ]
12
+ }
13
+
14
+ pg = st.navigation(pages, position="top")
15
+ pg.run()
data_loader.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from db import execute_query, fetch_all
2
+ import pandas as pd
3
+
4
+ EMPTY_MATERIAL_COLUMNS = [
5
+ "material_name",
6
+ "material_abbreviation",
7
+ "section",
8
+ "property_name",
9
+ "value",
10
+ "unit",
11
+ "english",
12
+ "test_condition",
13
+ "comments",
14
+ ]
15
+
16
+ def load_material_data(material_type: str) -> pd.DataFrame:
17
+ table_map = {
18
+ "Polymers": "Polymers",
19
+ "Fibers": "Fibers",
20
+ "Composites": "Composites_materials",
21
+ }
22
+
23
+ table = table_map.get(material_type)
24
+ if not table:
25
+ return pd.DataFrame(columns=EMPTY_MATERIAL_COLUMNS)
26
+
27
+ query = f"""
28
+ SELECT
29
+ material_name,
30
+ material_abbreviation,
31
+ section,
32
+ property_name,
33
+ value,
34
+ unit,
35
+ english,
36
+ test_condition,
37
+ comments
38
+ FROM "{table}"
39
+ """
40
+
41
+ try:
42
+ rows = fetch_all(query)
43
+ except Exception:
44
+ return pd.DataFrame(columns=EMPTY_MATERIAL_COLUMNS)
45
+ return pd.DataFrame(rows, columns=EMPTY_MATERIAL_COLUMNS)
46
+
47
+ def get_all_sections():
48
+ all_data = pd.concat([
49
+ load_material_data("Polymers"),
50
+ load_material_data("Fibers"),
51
+ load_material_data("Composites"),
52
+ ], ignore_index=True)
53
+
54
+ if all_data.empty or "section" not in all_data.columns:
55
+ return []
56
+ return sorted(all_data["section"].dropna().unique().tolist())
57
+
58
+
59
+ def insert_material_rows(df: pd.DataFrame) -> int:
60
+ if df is None or df.empty:
61
+ return 0
62
+
63
+ table_map = {
64
+ "Polymer": "Polymers",
65
+ "Fiber": "Fibers",
66
+ "Composite": "Composites_materials",
67
+ }
68
+
69
+ insert_template = """
70
+ INSERT INTO "{table}" (
71
+ material_name,
72
+ material_abbreviation,
73
+ section,
74
+ property_name,
75
+ value,
76
+ unit,
77
+ english,
78
+ test_condition,
79
+ comments
80
+ ) VALUES (
81
+ :material_name,
82
+ :material_abbreviation,
83
+ :section,
84
+ :property_name,
85
+ :value,
86
+ :unit,
87
+ :english,
88
+ :test_condition,
89
+ :comments
90
+ )
91
+ """
92
+
93
+ inserted = 0
94
+ for _, row in df.iterrows():
95
+ table = table_map.get(row.get("material_class"))
96
+ if not table:
97
+ continue
98
+
99
+ params = {
100
+ "material_name": row.get("material_name", ""),
101
+ "material_abbreviation": row.get("material_abbreviation", ""),
102
+ "section": row.get("section", ""),
103
+ "property_name": row.get("property_name", ""),
104
+ "value": row.get("value", ""),
105
+ "unit": row.get("unit", ""),
106
+ "english": row.get("english", ""),
107
+ "test_condition": row.get("test_condition", ""),
108
+ "comments": row.get("comments", ""),
109
+ }
110
+
111
+ try:
112
+ inserted += execute_query(insert_template.format(table=table), params)
113
+ except Exception:
114
+ return inserted
115
+
116
+ return inserted
db.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # db.py
2
+ import os
3
+ from sqlalchemy import create_engine, text
4
+ from sqlalchemy.orm import sessionmaker
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
9
+ DB_HOST = os.getenv("DB_HOST")
10
+ DB_PORT = os.getenv("DB_PORT", "5432")
11
+ DB_NAME = os.getenv("DB_NAME")
12
+ DB_USER = os.getenv("DB_USER")
13
+ DB_PASSWORD = os.getenv("DB_PASSWORD")
14
+
15
+ DATABASE_URL = None
16
+ engine = None
17
+ SessionLocal = None
18
+
19
+ if all([DB_HOST, DB_NAME, DB_USER, DB_PASSWORD]):
20
+ DATABASE_URL = f"postgresql+psycopg://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
21
+ engine = create_engine(DATABASE_URL, pool_pre_ping=True)
22
+ SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
23
+
24
+
25
+ def _require_engine():
26
+ if engine is None:
27
+ raise RuntimeError(
28
+ "Database is not configured. Set DB_HOST, DB_PORT, DB_NAME, DB_USER, and DB_PASSWORD."
29
+ )
30
+
31
+ def fetch_all(query, params=None):
32
+ _require_engine()
33
+ with engine.connect() as conn:
34
+ result = conn.execute(text(query), params or {})
35
+ return [dict(row._mapping) for row in result]
36
+
37
+ def fetch_one(query, params=None):
38
+ _require_engine()
39
+ with engine.connect() as conn:
40
+ result = conn.execute(text(query), params or {})
41
+ row = result.fetchone()
42
+ return dict(row._mapping) if row else None
43
+
44
+ def execute_query(query, params=None):
45
+ _require_engine()
46
+ with engine.begin() as conn:
47
+ result = conn.execute(text(query), params or {})
48
+ return result.rowcount
images/AISI 1045 and AA5052_Ram Speed.png ADDED

Git LFS Details

  • SHA256: 0329479746a3ba20be80ab2a6324528c6e3cf1e0685eae4b24661215c26b687b
  • Pointer size: 131 Bytes
  • Size of remote file: 153 kB
AA5052_Strain Hardening Exponent (n).png RENAMED
File without changes
AA5052_Strength Coefficient (K).png RENAMED
File without changes
images/ASAAAA_Coefficient 'a' for average equivalent strain (ε̄_ave).png ADDED

Git LFS Details

  • SHA256: df86d791caec82de3f10f8c27dfb2cb34b18e97cd4ffd873e6afa8a13381f574
  • Pointer size: 131 Bytes
  • Size of remote file: 108 kB
images/ASAAAA_Diameter.png ADDED
images/ASAAAA_Flow Stress Equation (Lubricated).png ADDED

Git LFS Details

  • SHA256: b8d6abcb19f71eb2348c53d83fa84cbb46ccf3f505f2a676a358533eaa7d6f79
  • Pointer size: 131 Bytes
  • Size of remote file: 828 kB
images/ASAAAA_Flow Stress Equation (Power's Law).png ADDED

Git LFS Details

  • SHA256: b8d6abcb19f71eb2348c53d83fa84cbb46ccf3f505f2a676a358533eaa7d6f79
  • Pointer size: 131 Bytes
  • Size of remote file: 828 kB
images/ASAAAA_Flow Stress Equation (Sticking).png ADDED
images/ASAAAA_Strain hardening exponent (n).png ADDED
images/ASAAAA_Strength coefficient (K).png ADDED
images/Abhijit.jpg ADDED
images/Cynate Ester + 55% Carbon Fiber_Moisture Absorption at Equilibrium.png ADDED
images/Epoxy + 44% Carbon fiber_Tensile Strength.png ADDED

Git LFS Details

  • SHA256: 9fd330279709453afa888090f43d4a14220b1b56e869d975a4c0015da74a29db
  • Pointer size: 131 Bytes
  • Size of remote file: 262 kB
images/GangLi.jpg ADDED
images/Home.png ADDED

Git LFS Details

  • SHA256: caf4fde646a560ceb5f0991f876626f2ff10afa9013854aa2b7720192bdbab69
  • Pointer size: 132 Bytes
  • Size of remote file: 5.92 MB
images/Materials_bg_InDeS.png ADDED

Git LFS Details

  • SHA256: 864115b3941abf1be1abfebfe7a5a5e1b4e1d98e9660ad923bc439835734dd22
  • Pointer size: 132 Bytes
  • Size of remote file: 2.48 MB
images/Mathias.jpg ADDED
images/PTFE_Water Absorption.png ADDED
images/Pradeep.jpg ADDED
images/Stress-strain-response-of-the-T300-3k-carbon-fiber-bundle.png ADDED
images/Tejaswi.jpg ADDED
images/Tensile-stress-strain-curve-of-Pure-PEEK-fibres-for-three-different-extrusion.png ADDED
images/desktop.ini ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [LocalizedFileNames]
2
+ Screenshot 2026-01-20 001556.png=@Screenshot 2026-01-20 001556,0
3
+ Screenshot 2026-01-20 001941.png=@Screenshot 2026-01-20 001941,0
images/iPP_Curve Error (Maximum).png ADDED

Git LFS Details

  • SHA256: ed7e365af4f15929472742db3d350caae98b55d736efe5005d7ffff12b627205
  • Pointer size: 131 Bytes
  • Size of remote file: 631 kB
images/iPP_Feature-to-curve reconstruction normalized error.png ADDED
images/iPP_Injection Pressure (Pinject) Range.png ADDED

Git LFS Details

  • SHA256: 8c1685904ff03b79898147b5aaf8adfb125f018526c3872cee48b85070dc9bf5
  • Pointer size: 132 Bytes
  • Size of remote file: 1.01 MB
images/iPP_Injection Rate (Rinject) Range.png ADDED

Git LFS Details

  • SHA256: 1dc5611383d12d4e6d0678be2427b6ba8403d6dd5522a1480f048b7856286535
  • Pointer size: 132 Bytes
  • Size of remote file: 1.5 MB
images/iPP_Melting temperature.png ADDED

Git LFS Details

  • SHA256: 56ee0d92f2479211f6372ca6e22844328915a3186e79c888eb0717d27b47c09f
  • Pointer size: 131 Bytes
  • Size of remote file: 875 kB
images/iPP_Normalized Root Mean Square Error (NRMSE) Distribution Center.png ADDED

Git LFS Details

  • SHA256: 211f2dd7af79eaccc5fe86f6f374c3f21734557bd4a7f5b4ae5bf9bb0244a2d9
  • Pointer size: 131 Bytes
  • Size of remote file: 476 kB
images/logo.png ADDED

Git LFS Details

  • SHA256: ca199c61238861f284e76397e24ace640bada75e6753ba4737023a1d8ad84aca
  • Pointer size: 131 Bytes
  • Size of remote file: 150 kB
images/us_deptenergy.jpg ADDED

Git LFS Details

  • SHA256: 92c687ec97c2ff1fc9bdaeab7d1e2896b1cf8cac65f1ae42cf39a392c5dbc427
  • Pointer size: 131 Bytes
  • Size of remote file: 428 kB
logo.png ADDED

Git LFS Details

  • SHA256: ca199c61238861f284e76397e24ace640bada75e6753ba4737023a1d8ad84aca
  • Pointer size: 131 Bytes
  • Size of remote file: 150 kB
merged_file.json ADDED
The diff for this file is too large to render. See raw diff
 
page_files/Categorized_Search.py ADDED
@@ -0,0 +1,900 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import base64
2
+ import re
3
+ from pathlib import Path
4
+
5
+ import pandas as pd
6
+ import streamlit as st
7
+ from PIL import Image
8
+
9
+ from data_loader import get_all_sections, load_material_data
10
+
11
+ st.markdown(
12
+ """
13
+ <style>
14
+ [data-testid="stToolbar"],
15
+ [data-testid="stDecoration"],
16
+ [data-testid="stHeader"],
17
+ [data-testid="stSidebar"] {
18
+ display: none !important;
19
+ }
20
+
21
+ [data-testid="stAppViewContainer"] {
22
+ background: #eef2f7 !important;
23
+ }
24
+
25
+ .st-emotion-cache-6c7yup [data-testid="stLayoutWrapper"] {
26
+ background: #fff !important;
27
+
28
+
29
+ }
30
+
31
+ .st-emotion-cache-18kf3ut [data-testid="stLayoutWrapper"] {
32
+ background: #fff !important;
33
+
34
+
35
+ }
36
+
37
+ .block-container {
38
+ max-width: 100% !important;
39
+ padding: 0.6rem 0.75rem 0.8rem 0.75rem !important;
40
+ }
41
+
42
+ div[data-testid="stVerticalBlockBorderWrapper"] {
43
+ border-color: #e1e7ef !important;
44
+ border-radius: 10px !important;
45
+ background: #ffffff !important;
46
+ }
47
+
48
+ .aim-top-strip {
49
+ height: 54px;
50
+ border: 1px solid #e1e7ef;
51
+ border-radius: 10px;
52
+ background: #ffffff;
53
+ margin-bottom: 12px;
54
+ }
55
+
56
+ .st-key-top_search_row [data-testid="stHorizontalBlock"] {
57
+ gap: 0 !important;
58
+ align-items: stretch !important;
59
+ }
60
+
61
+ .st-key-top_search_input [data-baseweb="input"] {
62
+ height: 46px !important;
63
+ min-height: 46px !important;
64
+ border-radius: 50px 0 0 50px !important;
65
+ border-right: none !important;
66
+ border-color: #d0d8d4 !important;
67
+ background: #ffffff !important;
68
+ }
69
+
70
+ .st-key-top_search_input input {
71
+ height: 46px !important;
72
+ color: #0f1f1a !important;
73
+ font-family: 'DM Sans', sans-serif !important;
74
+ font-size: 0.92rem !important;
75
+ padding-left: 18px !important;
76
+ }
77
+
78
+ .st-key-top_search_input input::placeholder {
79
+ color: #0f1f1a !important;
80
+ opacity: 0.4 !important;
81
+ }
82
+
83
+ .st-key-top_search_btn button {
84
+ background: #8ACAFF !important;
85
+ border: 1.5px solid #8ACAFF !important;
86
+ border-left: none !important;
87
+ border-radius: 0 50px 50px 0 !important;
88
+ color: #0f1f1a !important;
89
+ font-family: 'DM Sans', sans-serif !important;
90
+ font-size: 0.88rem !important;
91
+ font-weight: 600 !important;
92
+ height: 46px !important;
93
+ padding: 0 28px !important;
94
+ box-shadow: none !important;
95
+ }
96
+
97
+ .st-key-top_search_input {
98
+ margin-right: 0 !important;
99
+ padding-right: 0 !important;
100
+ }
101
+
102
+ .st-key-top_search_btn {
103
+ margin-left: 0 !important;
104
+ padding-left: 0 !important;
105
+ }
106
+
107
+ .aim-logo {
108
+ display: flex;
109
+ align-items: center;
110
+ gap: 8px;
111
+ padding: 14px 14px 12px;
112
+ margin: -0.2rem -0.2rem 10px -0.2rem;
113
+ border-bottom: 1px solid #edf2f7;
114
+ border-top-left-radius: 10px;
115
+ border-top-right-radius: 10px;
116
+ background: #bae1fc;
117
+ color: #111827;
118
+ font-size: 0.92rem;
119
+ font-weight: 700;
120
+ }
121
+
122
+ .aim-sec {
123
+ color: #94a3b8;
124
+ font-size: 0.64rem;
125
+ font-weight: 700;
126
+ letter-spacing: 1.5px;
127
+ text-transform: uppercase;
128
+ margin: 8px 0 6px;
129
+ }
130
+
131
+ .aim-lbl {
132
+ color: #94a3b8;
133
+ font-size: 0.62rem;
134
+ font-weight: 700;
135
+ letter-spacing: 0.9px;
136
+ text-transform: uppercase;
137
+ margin: 3px 0;
138
+ }
139
+
140
+ .aim-breadcrumb {
141
+ color: #94a3b8;
142
+ font-size: 0.7rem;
143
+ letter-spacing: 0.3px;
144
+ margin-top: 4px;
145
+ }
146
+
147
+ .aim-breadcrumb span {
148
+ color: #3155d4;
149
+ font-weight: 600;
150
+ }
151
+
152
+ .aim-title {
153
+ color: #111827;
154
+ font-size: 2.95rem;
155
+ line-height: 1.03;
156
+ font-weight: 800;
157
+ margin: 4px 0 6px;
158
+ }
159
+
160
+ .aim-sub {
161
+ color: #64748b;
162
+ font-size: 0.82rem;
163
+ }
164
+
165
+ .aim-sub strong {
166
+ color: #111827;
167
+ }
168
+
169
+ .aim-selected {
170
+ color: #64748b;
171
+ font-size: 0.74rem;
172
+ margin-top: 10px;
173
+ line-height: 1.35;
174
+ }
175
+
176
+ .aim-selected b {
177
+ color: #111827;
178
+ }
179
+
180
+ .stButton > button {
181
+ min-height: 30px;
182
+ border-radius: 999px !important;
183
+ border: 1.35px solid #d8e1eb !important;
184
+ font-size: 0.78rem !important;
185
+ font-weight: 600 !important;
186
+ padding: 0.18rem 0.6rem !important;
187
+ white-space: nowrap !important;
188
+ }
189
+
190
+ .stButton > button[kind="secondary"] {
191
+ background: #ffffff !important;
192
+ color: #334155 !important;
193
+ }
194
+
195
+ .stButton > button[kind="primary"] {
196
+ background: #111827 !important;
197
+ border-color: #111827 !important;
198
+ color: #ffffff !important;
199
+ }
200
+
201
+ div[data-baseweb="select"] > div {
202
+ min-height: 34px;
203
+ border: 1.35px solid #dce4ee !important;
204
+ border-radius: 8px !important;
205
+ background: #ffffff !important;
206
+ }
207
+
208
+ div[data-baseweb="select"] * {
209
+ font-size: 0.8rem !important;
210
+ color: #334155 !important;
211
+ }
212
+
213
+ [data-testid="stCheckbox"] label p {
214
+ color: #374151 !important;
215
+ font-size: 0.78rem !important;
216
+ }
217
+
218
+ [data-testid="stTabs"] [data-baseweb="tab-list"] {
219
+ justify-content: flex-end;
220
+ gap: 10px;
221
+ border-bottom: 1px solid #edf2f7;
222
+ }
223
+
224
+ [data-testid="stTabs"] button[data-baseweb="tab"] {
225
+ color: #94a3b8;
226
+ font-size: 0.84rem;
227
+ font-weight: 600;
228
+ padding: 12px 8px 11px;
229
+ }
230
+
231
+ [data-testid="stTabs"] button[aria-selected="true"] {
232
+ color: #111827 !important;
233
+ border-bottom: 2.5px solid #111827 !important;
234
+ }
235
+
236
+ div[data-testid="stDataEditor"] {
237
+ border: none !important;
238
+ border-radius: 0 !important;
239
+ overflow: visible !important;
240
+ background: transparent !important;
241
+ box-shadow: none !important;
242
+ }
243
+
244
+ div[data-testid="stDataEditor"] [role="columnheader"] {
245
+ background: #f8fafc !important;
246
+ color: #94a3b8 !important;
247
+ text-transform: uppercase;
248
+ letter-spacing: 1px;
249
+ font-size: 0.64rem !important;
250
+ font-weight: 700 !important;
251
+ border-bottom: 1px solid #edf2f7 !important;
252
+ }
253
+
254
+ div[data-testid="stDataEditor"] [role="gridcell"] {
255
+ font-size: 0.82rem;
256
+ color: #111827 !important;
257
+ background: #ffffff !important;
258
+ border-bottom: 1px solid #f8fafc !important;
259
+ }
260
+
261
+ div[data-testid="stDataEditor"] [role="grid"] {
262
+ background: #ffffff !important;
263
+ border: none !important;
264
+ border-radius: 0 !important;
265
+ box-shadow: none !important;
266
+ }
267
+
268
+ /* Materials grid must blend with the parent card (no inner boxed layer) */
269
+ div.stElementContainer[class*="st-key-materials_editor_"] div[data-testid="stFullScreenFrame"],
270
+ div.stElementContainer[class*="st-key-materials_editor_"] div[data-testid="stDataFrame"],
271
+ div.stElementContainer[class*="st-key-materials_editor_"] div[data-testid="stDataFrameResizable"],
272
+ div.stElementContainer[class*="st-key-materials_editor_"] div[data-testid="stDataFrameResizable"][style] {
273
+ border: 0 !important;
274
+ border-radius: 0 !important;
275
+ background: transparent !important;
276
+ box-shadow: none !important;
277
+ outline: none !important;
278
+ }
279
+
280
+ div.stElementContainer[class*="st-key-materials_editor_"] div.stDataFrameGlideDataEditor,
281
+ div.stElementContainer[class*="st-key-materials_editor_"] div.stDataFrameGlideDataEditor[style] {
282
+ --gdg-bg-cell: #ffffff !important;
283
+ --gdg-bg-cell-medium: #ffffff !important;
284
+ --gdg-bg-header: #f8fafc !important;
285
+ --gdg-border-color: transparent !important;
286
+ --gdg-horizontal-border-color: transparent !important;
287
+ --gdg-drilldown-border: transparent !important;
288
+ --gdg-text-dark: #111827 !important;
289
+ --gdg-text-medium: rgba(17, 24, 39, 0.82) !important;
290
+ --gdg-text-header: rgba(148, 163, 184, 1) !important;
291
+ --gdg-cell-vertical-padding: 8px !important;
292
+ --gdg-bg-bubble: #dbeafe !important;
293
+ --gdg-text-bubble: #1d4ed8 !important;
294
+ --gdg-bubble-height: 22px !important;
295
+ --gdg-bubble-padding: 10px !important;
296
+ }
297
+
298
+ .aim-pg-info {
299
+ color: #94a3b8;
300
+ font-size: 0.8rem;
301
+ margin-top: 6px;
302
+ }
303
+
304
+ .aim-pg-info strong {
305
+ color: #111827;
306
+ }
307
+
308
+ .aim-ellipsis {
309
+ color: #94a3b8;
310
+ font-size: 0.85rem;
311
+ text-align: center;
312
+ margin-top: 4px;
313
+ }
314
+ </style>
315
+ """,
316
+ unsafe_allow_html=True,
317
+ )
318
+
319
+
320
+ @st.cache_data
321
+ def load_data(material_type: str) -> pd.DataFrame:
322
+ return load_material_data(material_type)
323
+
324
+
325
+ @st.cache_data
326
+ def load_all_data() -> pd.DataFrame:
327
+ frames = []
328
+ for material_type in ["Composites", "Polymers", "Fibers"]:
329
+ frame = load_data(material_type)
330
+ frame["_class"] = material_type
331
+ frames.append(frame)
332
+ return pd.concat(frames, ignore_index=True)
333
+
334
+
335
+ def extract_matrix_fiber(abbr: str):
336
+ if not isinstance(abbr, str):
337
+ return None, None
338
+
339
+ text = abbr.lower()
340
+
341
+ matrix_map = {
342
+ "epoxy": "Epoxy",
343
+ "cyanate ester": "Cyanate Ester",
344
+ "cynate ester": "Cyanate Ester",
345
+ "polypropylene": "Polypropylene",
346
+ "pp": "Polypropylene",
347
+ "peek": "PEEK",
348
+ "pei": "PEI",
349
+ "nylon": "Nylon",
350
+ "pa6": "PA6",
351
+ "polyester": "Polyester",
352
+ "vinyl ester": "Vinyl Ester",
353
+ "phenolic": "Phenolic",
354
+ }
355
+
356
+ fiber_map = {
357
+ "carbon": "Carbon Fiber",
358
+ "glass": "Glass Fiber",
359
+ "e-glass": "E-Glass Fiber",
360
+ "s-glass": "S-Glass Fiber",
361
+ "aramid": "Aramid Fiber",
362
+ "kevlar": "Kevlar Fiber",
363
+ "basalt": "Basalt Fiber",
364
+ "natural": "Natural Fiber",
365
+ }
366
+
367
+ matrix = next((value for key, value in matrix_map.items() if key in text), None)
368
+ fiber = next((value for key, value in fiber_map.items() if key in text), None)
369
+ return matrix, fiber
370
+
371
+
372
+ def toggle_class(material_class: str):
373
+ active = list(st.session_state.active_classes)
374
+ if material_class in active:
375
+ active.remove(material_class)
376
+ else:
377
+ active.append(material_class)
378
+
379
+ order = ["Composites", "Polymers", "Fibers"]
380
+ st.session_state.active_classes = [item for item in order if item in active]
381
+ st.session_state.current_page = 0
382
+
383
+
384
+ def visible_page_numbers(current_page: int, total_pages: int):
385
+ if total_pages <= 6:
386
+ return list(range(total_pages))
387
+
388
+ pages = {0, 1, 2, current_page, total_pages - 1}
389
+ if current_page - 1 > 2:
390
+ pages.add(current_page - 1)
391
+ if current_page + 1 < total_pages - 1:
392
+ pages.add(current_page + 1)
393
+ return sorted(pages)
394
+
395
+
396
+ defaults = {
397
+ "active_classes": [],
398
+ "selected_props": [],
399
+ "selected_matrix": "All",
400
+ "selected_fiber": "All",
401
+ "selected_row": None,
402
+ "current_page": 0,
403
+ "_search_term": None,
404
+ "top_search_input": "",
405
+ "inspect_section": None,
406
+ "inspect_property": None,
407
+ "_reset_prop_checks": False,
408
+ }
409
+
410
+ for key, value in defaults.items():
411
+ if key not in st.session_state:
412
+ st.session_state[key] = value
413
+
414
+ if "material_type" in st.session_state:
415
+ incoming_type = st.session_state.pop("material_type")
416
+ if incoming_type in ["Composites", "Polymers", "Fibers"]:
417
+ st.session_state.active_classes = [incoming_type]
418
+
419
+ if "selected_section" in st.session_state:
420
+ st.session_state.selected_props = [st.session_state.pop("selected_section")]
421
+ st.session_state._reset_prop_checks = True
422
+
423
+ if "search_term" in st.session_state:
424
+ st.session_state._search_term = st.session_state.pop("search_term")
425
+
426
+ if st.session_state._search_term and not st.session_state.top_search_input:
427
+ st.session_state.top_search_input = st.session_state._search_term
428
+
429
+ all_data = load_all_data()
430
+ if "user_uploaded_data" in st.session_state:
431
+ uploaded = st.session_state["user_uploaded_data"].copy()
432
+ uploaded["_class"] = uploaded["material_class"].map(
433
+ {"Polymer": "Polymers", "Fiber": "Fibers", "Composite": "Composites"}
434
+ )
435
+ all_data = pd.concat([all_data, uploaded], ignore_index=True)
436
+
437
+ st.session_state["base_data"] = all_data
438
+
439
+ meta = (
440
+ all_data[["material_abbreviation", "material_name", "_class"]]
441
+ .fillna("")
442
+ .drop_duplicates(subset=["material_abbreviation"])
443
+ .reset_index(drop=True)
444
+ )
445
+ meta[["Matrix", "Fiber"]] = meta["material_abbreviation"].apply(
446
+ lambda value: pd.Series(extract_matrix_fiber(value))
447
+ )
448
+
449
+ all_sections = get_all_sections()
450
+
451
+ if st.session_state._reset_prop_checks:
452
+ selected = set(st.session_state.selected_props)
453
+ for index, section in enumerate(all_sections):
454
+ st.session_state[f"prop_check_{index}"] = section in selected
455
+ st.session_state._reset_prop_checks = False
456
+
457
+ filtered_meta = meta.copy()
458
+ if st.session_state.active_classes:
459
+ filtered_meta = filtered_meta[filtered_meta["_class"].isin(st.session_state.active_classes)]
460
+ if st.session_state.selected_matrix != "All":
461
+ filtered_meta = filtered_meta[filtered_meta["Matrix"] == st.session_state.selected_matrix]
462
+ if st.session_state.selected_fiber != "All":
463
+ filtered_meta = filtered_meta[filtered_meta["Fiber"] == st.session_state.selected_fiber]
464
+
465
+ if st.session_state._search_term:
466
+ term = st.session_state._search_term
467
+ try:
468
+ pattern = re.compile(term, re.IGNORECASE)
469
+ except re.error:
470
+ pattern = re.compile(re.escape(term), re.IGNORECASE)
471
+
472
+ filtered_meta = filtered_meta[
473
+ filtered_meta["material_abbreviation"].astype(str).str.contains(pattern)
474
+ | filtered_meta["material_name"].astype(str).str.contains(pattern)
475
+ ]
476
+
477
+ if st.session_state.selected_props:
478
+ valid_abbr = all_data[
479
+ all_data["section"].isin(st.session_state.selected_props) & all_data["value"].notna()
480
+ ]["material_abbreviation"].unique()
481
+ filtered_meta = filtered_meta[filtered_meta["material_abbreviation"].isin(valid_abbr)]
482
+
483
+ filtered_meta = filtered_meta.reset_index(drop=True)
484
+
485
+ PAGE_SIZE = 5
486
+ total = len(filtered_meta)
487
+ total_pages = max(1, (total + PAGE_SIZE - 1) // PAGE_SIZE)
488
+ st.session_state.current_page = min(st.session_state.current_page, total_pages - 1)
489
+
490
+ start = st.session_state.current_page * PAGE_SIZE
491
+ end = start + PAGE_SIZE
492
+ page_meta = filtered_meta.iloc[start:end].reset_index(drop=True)
493
+
494
+ left_col, right_col = st.columns([1.03, 3.07], gap="small")
495
+
496
+ with left_col:
497
+ with st.container(border=True):
498
+ logo_html = ""
499
+ logo_path = Path("logo.png")
500
+ if logo_path.exists():
501
+ with logo_path.open("rb") as file_handle:
502
+ logo_b64 = base64.b64encode(file_handle.read()).decode()
503
+ logo_html = (
504
+ f"<img src='data:image/png;base64,{logo_b64}' "
505
+ "style='height:20px;width:20px;object-fit:contain;border-radius:4px;'/>"
506
+ )
507
+
508
+ st.markdown(
509
+ f"<div class='aim-logo'>{logo_html} AIM Composites</div>",
510
+ unsafe_allow_html=True,
511
+ )
512
+
513
+ st.markdown("<div class='aim-sec'>🧩 Material Class</div>", unsafe_allow_html=True)
514
+
515
+ cls_a, cls_b = st.columns(2)
516
+ with cls_a:
517
+ if st.button(
518
+ "Composites",
519
+ key="class_comp",
520
+ use_container_width=True,
521
+ type="primary" if "Composites" in st.session_state.active_classes else "secondary",
522
+ ):
523
+ toggle_class("Composites")
524
+ st.rerun()
525
+
526
+ with cls_b:
527
+ if st.button(
528
+ "Polymers",
529
+ key="class_poly",
530
+ use_container_width=True,
531
+ type="primary" if "Polymers" in st.session_state.active_classes else "secondary",
532
+ ):
533
+ toggle_class("Polymers")
534
+ st.rerun()
535
+
536
+ if st.button(
537
+ "Fibers",
538
+ key="class_fib",
539
+ use_container_width=True,
540
+ type="primary" if "Fibers" in st.session_state.active_classes else "secondary",
541
+ ):
542
+ toggle_class("Fibers")
543
+ st.rerun()
544
+
545
+ st.markdown("<div class='aim-sec'>🧪 Composition</div>", unsafe_allow_html=True)
546
+ composite_meta = meta[meta["_class"] == "Composites"]
547
+ matrix_options = ["All"] + sorted([item for item in composite_meta["Matrix"].dropna().unique() if item])
548
+ fiber_options = ["All"] + sorted([item for item in composite_meta["Fiber"].dropna().unique() if item])
549
+
550
+ st.markdown("<div class='aim-lbl'>🧱 Matrix</div>", unsafe_allow_html=True)
551
+ matrix_value = st.selectbox(
552
+ "Matrix",
553
+ matrix_options,
554
+ index=matrix_options.index(st.session_state.selected_matrix)
555
+ if st.session_state.selected_matrix in matrix_options
556
+ else 0,
557
+ key="matrix_select",
558
+ label_visibility="collapsed",
559
+ )
560
+
561
+ st.markdown("<div class='aim-lbl'>🧵 Fiber</div>", unsafe_allow_html=True)
562
+ fiber_value = st.selectbox(
563
+ "Fiber",
564
+ fiber_options,
565
+ index=fiber_options.index(st.session_state.selected_fiber)
566
+ if st.session_state.selected_fiber in fiber_options
567
+ else 0,
568
+ key="fiber_select",
569
+ label_visibility="collapsed",
570
+ )
571
+
572
+ if matrix_value != st.session_state.selected_matrix:
573
+ st.session_state.selected_matrix = matrix_value
574
+ st.session_state.current_page = 0
575
+ st.rerun()
576
+
577
+ if fiber_value != st.session_state.selected_fiber:
578
+ st.session_state.selected_fiber = fiber_value
579
+ st.session_state.current_page = 0
580
+ st.rerun()
581
+
582
+ st.markdown("<div class='aim-sec'>📋 Property Types</div>", unsafe_allow_html=True)
583
+ selected_props = []
584
+ with st.container(height=480):
585
+ for index, section in enumerate(all_sections):
586
+ key = f"prop_check_{index}"
587
+ if key not in st.session_state:
588
+ st.session_state[key] = section in st.session_state.selected_props
589
+
590
+ if st.checkbox(section, key=key):
591
+ selected_props.append(section)
592
+
593
+ if selected_props != st.session_state.selected_props:
594
+ st.session_state.selected_props = selected_props
595
+ st.session_state.current_page = 0
596
+ st.rerun()
597
+
598
+ if st.button(
599
+ "🔎 Inspect",
600
+ key="inspect_btn",
601
+ use_container_width=True,
602
+ type="primary",
603
+ disabled=st.session_state.selected_row is None,
604
+ ):
605
+ st.info("Open the Inspect tab on the right panel.")
606
+
607
+ if st.session_state.selected_row:
608
+ selected_abbr, selected_name = st.session_state.selected_row
609
+ st.markdown(
610
+ f"<div class='aim-selected'><b>Selected</b><br>{selected_name}<br><span style='font-family:monospace'>{selected_abbr}</span></div>",
611
+ unsafe_allow_html=True,
612
+ )
613
+
614
+ with right_col:
615
+ with st.container(border=True):
616
+ with st.container(key="top_search_row"):
617
+ input_col, btn_col = st.columns([0.82, 0.18], gap="small")
618
+ with input_col:
619
+ search_query = st.text_input(
620
+ label="Search",
621
+ placeholder="Search by material name, property, or abbreviation...",
622
+ label_visibility="collapsed",
623
+ key="top_search_input",
624
+ )
625
+ with btn_col:
626
+ search_clicked = st.button("Search", key="top_search_btn", use_container_width=True)
627
+
628
+ if search_clicked:
629
+ query = (search_query or "").strip()
630
+ st.session_state._search_term = query if query else None
631
+ st.session_state.current_page = 0
632
+ st.rerun()
633
+
634
+ with st.container(border=True):
635
+ st.markdown(
636
+ """
637
+ <div style='padding:14px 18px 0;'>
638
+ <div class='aim-breadcrumb'>INVENTORY / <span>MATERIALS DATABASE</span></div>
639
+ <div class='aim-title'>Materials Database</div>
640
+ </div>
641
+ """,
642
+ unsafe_allow_html=True,
643
+ )
644
+
645
+ tab_materials, tab_dashboard, tab_inspect = st.tabs(
646
+ ["All Materials", "Dashboard", "Inspect"]
647
+ )
648
+
649
+ with tab_materials:
650
+ filter_label = (
651
+ ", ".join(st.session_state.active_classes)
652
+ if st.session_state.active_classes
653
+ else "All Materials"
654
+ )
655
+ shown_start = start + 1 if total > 0 else 0
656
+ shown_end = min(end, total)
657
+
658
+ row_left, row_right = st.columns([2.2, 1.2])
659
+ with row_left:
660
+ st.markdown(f"<div class='aim-sub'>{filter_label}</div>", unsafe_allow_html=True)
661
+ with row_right:
662
+ st.markdown(
663
+ f"<div class='aim-sub' style='text-align:right'>Showing <strong>{shown_start}-{shown_end}</strong> of <strong>{total}</strong> materials</div>",
664
+ unsafe_allow_html=True,
665
+ )
666
+
667
+ selected_abbr = st.session_state.selected_row[0] if st.session_state.selected_row else None
668
+
669
+ class_map = {
670
+ "Composites": "🔵 COMPOSITE",
671
+ "Polymers": "🟢 POLYMER",
672
+ "Fibers": "🟠 FIBER",
673
+ }
674
+
675
+ if not page_meta.empty:
676
+ table_df = page_meta.copy()
677
+ table_df["Select"] = table_df["material_abbreviation"].eq(selected_abbr)
678
+ table_df["Class"] = table_df["_class"].map(class_map)
679
+ table_df["Actions"] = ""
680
+ table_df = table_df[
681
+ ["Select", "material_name", "material_abbreviation", "Class", "Actions"]
682
+ ].rename(
683
+ columns={
684
+ "material_name": "Material Name",
685
+ "material_abbreviation": "Abbreviation",
686
+ }
687
+ )
688
+ else:
689
+ table_df = pd.DataFrame(
690
+ columns=["Select", "Material Name", "Abbreviation", "Class", "Actions"]
691
+ )
692
+
693
+ for _ in range(2):
694
+ table_df.loc[len(table_df)] = [False, "", "", "", ""]
695
+
696
+ editor_df = st.data_editor(
697
+ table_df,
698
+ key=f"materials_editor_{st.session_state.current_page}",
699
+ use_container_width=True,
700
+ hide_index=True,
701
+ height=372,
702
+ row_height=46,
703
+ column_order=["Select", "Material Name", "Abbreviation", "Class", "Actions"],
704
+ column_config={
705
+ "Select": st.column_config.CheckboxColumn("", width="small"),
706
+ "Material Name": st.column_config.TextColumn("MATERIAL NAME", width="large"),
707
+ "Abbreviation": st.column_config.TextColumn("ABBREVIATION", width="medium"),
708
+ "Class": st.column_config.TextColumn("CLASS", width="small"),
709
+ "Actions": st.column_config.TextColumn("ACTIONS", width="small"),
710
+ },
711
+ disabled=["Material Name", "Abbreviation", "Class", "Actions"],
712
+ )
713
+
714
+ checked_rows = editor_df[
715
+ editor_df["Select"]
716
+ & editor_df["Abbreviation"].astype(str).str.strip().ne("")
717
+ ]
718
+ if not checked_rows.empty:
719
+ chosen = checked_rows.iloc[0]
720
+ abbr = chosen["Abbreviation"]
721
+ name = chosen["Material Name"]
722
+ if (
723
+ st.session_state.selected_row is None
724
+ or st.session_state.selected_row[0] != abbr
725
+ ):
726
+ st.session_state.selected_row = (abbr, name)
727
+ st.rerun()
728
+ info_col, nav_col = st.columns([2.4, 2.0])
729
+ with info_col:
730
+ st.markdown(
731
+ f"<div class='aim-pg-info'>Showing <strong>{shown_start}-{shown_end}</strong> of <strong>{total}</strong> materials | Items per page: <strong>{PAGE_SIZE}</strong></div>",
732
+ unsafe_allow_html=True,
733
+ )
734
+
735
+ with nav_col:
736
+ nav_items = []
737
+ nav_items.append(("<<", 0, st.session_state.current_page == 0, False))
738
+ nav_items.append(("<", max(0, st.session_state.current_page - 1), st.session_state.current_page == 0, False))
739
+
740
+ visible_pages = visible_page_numbers(st.session_state.current_page, total_pages)
741
+ previous = -1
742
+ for number in visible_pages:
743
+ if previous >= 0 and number - previous > 1:
744
+ nav_items.append(("...", None, True, False))
745
+ nav_items.append(
746
+ (
747
+ str(number + 1),
748
+ number,
749
+ False,
750
+ number == st.session_state.current_page,
751
+ )
752
+ )
753
+ previous = number
754
+
755
+ nav_items.append(
756
+ (
757
+ ">",
758
+ min(total_pages - 1, st.session_state.current_page + 1),
759
+ st.session_state.current_page >= total_pages - 1,
760
+ False,
761
+ )
762
+ )
763
+ nav_items.append(
764
+ (
765
+ ">>",
766
+ total_pages - 1,
767
+ st.session_state.current_page >= total_pages - 1,
768
+ False,
769
+ )
770
+ )
771
+
772
+ nav_columns = st.columns(len(nav_items))
773
+ for idx, (column, item) in enumerate(zip(nav_columns, nav_items)):
774
+ label, target_page, disabled, active = item
775
+ with column:
776
+ if label == "...":
777
+ st.markdown("<div class='aim-ellipsis'>...</div>", unsafe_allow_html=True)
778
+ else:
779
+ if st.button(
780
+ label,
781
+ key=f"page_btn_{idx}_{label}_{target_page}",
782
+ use_container_width=True,
783
+ disabled=disabled,
784
+ type="primary" if active else "secondary",
785
+ ):
786
+ st.session_state.current_page = target_page
787
+ st.rerun()
788
+
789
+ with tab_dashboard:
790
+ st.markdown(
791
+ "<div style='padding:40px 8px;color:#64748b;font-size:0.85rem'>Dashboard coming soon. Analytics and visualizations will appear here.</div>",
792
+ unsafe_allow_html=True,
793
+ )
794
+
795
+ with tab_inspect:
796
+ if not st.session_state.selected_row:
797
+ st.warning("Select a material in All Materials first.")
798
+ else:
799
+ selected_abbr, selected_name = st.session_state.selected_row
800
+ st.markdown(f"**Material:** {selected_name}")
801
+ st.caption(selected_abbr)
802
+
803
+ material_df = all_data[
804
+ (all_data["material_abbreviation"] == selected_abbr)
805
+ & (all_data["value"].notna())
806
+ & (all_data["property_name"].notna())
807
+ ]
808
+
809
+ section_options = sorted(material_df["section"].dropna().unique().tolist())
810
+ if not section_options:
811
+ st.warning("No property data found for this material.")
812
+ else:
813
+ if st.session_state.inspect_section not in section_options:
814
+ st.session_state.inspect_section = section_options[0]
815
+
816
+ section_choice = st.selectbox(
817
+ "Type of Property",
818
+ section_options,
819
+ index=section_options.index(st.session_state.inspect_section),
820
+ key="inspect_section_select",
821
+ )
822
+ st.session_state.inspect_section = section_choice
823
+
824
+ properties_df = (
825
+ material_df[material_df["section"] == section_choice][
826
+ ["property_name", "section"]
827
+ ]
828
+ .drop_duplicates()
829
+ .reset_index(drop=True)
830
+ )
831
+ st.dataframe(properties_df, use_container_width=True, hide_index=True, height=240)
832
+
833
+ property_options = properties_df["property_name"].dropna().tolist()
834
+ if property_options:
835
+ if st.session_state.inspect_property not in property_options:
836
+ st.session_state.inspect_property = property_options[0]
837
+
838
+ property_choice = st.selectbox(
839
+ "Property",
840
+ property_options,
841
+ index=property_options.index(st.session_state.inspect_property),
842
+ key="inspect_property_select",
843
+ )
844
+ st.session_state.inspect_property = property_choice
845
+
846
+ if st.button("Search", key="inspect_search", type="primary"):
847
+ result = all_data[
848
+ (all_data["material_abbreviation"] == selected_abbr)
849
+ & (all_data["property_name"] == property_choice)
850
+ & (all_data["value"].notna())
851
+ ]
852
+
853
+ if result.empty:
854
+ st.warning("No data found for this material-property combination")
855
+ else:
856
+ st.subheader("Property Data")
857
+ st.dataframe(result.T, use_container_width=True)
858
+
859
+ st.subheader("Property Graph")
860
+ image_path = Path("images") / f"{selected_abbr}_{property_choice}.png"
861
+ if image_path.exists():
862
+ image = Image.open(image_path)
863
+ st.image(image, use_container_width=True, caption="Stress strain curve")
864
+ else:
865
+ st.caption("No plot image available for this material-property pair.")
866
+
867
+
868
+
869
+
870
+
871
+
872
+
873
+
874
+
875
+
876
+
877
+
878
+
879
+
880
+
881
+
882
+
883
+
884
+
885
+
886
+
887
+
888
+
889
+
890
+
891
+
892
+
893
+
894
+
895
+
896
+
897
+
898
+
899
+
900
+
page_files/Contact_Team.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ from pathlib import Path
3
+ from PIL import Image, ImageOps
4
+
5
+ IMG_DIR = Path("images")
6
+ TARGET_SIZE = (213, 310)
7
+
8
+ def fixed_image(name):
9
+ img = Image.open(IMG_DIR / name).convert("RGB")
10
+ return ImageOps.fit(img, TARGET_SIZE, Image.LANCZOS, centering=(0.5, 0.5))
11
+
12
+ c1, c2, c3 = st.columns([1, 2, 1])
13
+ with c2:
14
+ st.subheader("Team Members")
15
+
16
+ col1, col2, col3 = st.columns(3)
17
+ with col1:
18
+ st.image(fixed_image("GangLi.jpg"))
19
+ st.markdown("**Gang Li** \nProfessor of Mechanical Engineering, Clemson University \ngli@clemson.edu")
20
+
21
+ with col2:
22
+ st.image(fixed_image("Mathias.jpg"))
23
+ st.markdown("**Heider, Mathias** \nResearch Assistant - CSE PhD Student \nUniversity of Delaware \nmheider@udel.edu")
24
+
25
+ with col3:
26
+ st.image(fixed_image("Abhijit.jpg"))
27
+ st.markdown("**Abhijit Varanasi** \nLab Specialist - Clemson University \nMFA Graduate, Clemson University \nBE - CSE \navarana@clemson.edu")
28
+
29
+ st.write("")
30
+
31
+ sp1, col4, sp2, col5, sp3 = st.columns([1, 3, 1, 3, 1])
32
+ with col4:
33
+ st.image(fixed_image("Tejaswi.jpg"))
34
+ st.markdown("**Tejaswi Gudimetla** \nLab Aide - Clemson University \nvgudime@clemson.edu")
35
+
36
+ with col5:
37
+ st.image(fixed_image("Pradeep.jpg"))
38
+ st.markdown("**Sai Aditya Pradeep** \nResearch and Development Engineer, University of Delaware \nspradeep@udel.edu")
39
+
40
+ st.sidebar.image("logo.png", caption=" ", width=150)
page_files/Home.py ADDED
@@ -0,0 +1,722 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import streamlit.components.v1 as components
3
+ from pathlib import Path
4
+ import base64
5
+ from streamlit_card import card
6
+ from data_loader import get_all_sections
7
+ import random
8
+ from data_loader import get_all_sections
9
+ import re
10
+
11
+
12
+ ALL_CARDS = [
13
+ ("Composites", "Material class", "material", "Composites"),
14
+ ("Polymers", "Material class", "material", "Polymers"),
15
+ ("Fibers", "Material class", "material", "Fibers"),
16
+ ]
17
+
18
+ sections = get_all_sections()
19
+ for section in sections:
20
+ ALL_CARDS.append((section, "Property type", "section", section))
21
+
22
+ if "visible_cards" not in st.session_state:
23
+ random.shuffle(ALL_CARDS)
24
+ st.session_state.visible_cards = ALL_CARDS[:4]
25
+
26
+ VISIBLE_CARDS = st.session_state.visible_cards
27
+ prop_count = len([c for c in ALL_CARDS if c[2] == "section"])
28
+
29
+ def get_card_icon(title: str, card_type: str) -> str:
30
+ if card_type == "material":
31
+ icons = {"composites": "🧱", "polymers": "🔬", "fibers": "🧵"}
32
+ return icons.get(title.lower(), "🧱")
33
+ t = title.lower()
34
+ if "mechanical" in t: return "⚙️"
35
+ if "thermal" in t: return "🔥"
36
+ if "electrical" in t: return "⚡"
37
+ if "physical" in t: return "⚖️"
38
+ if "processing" in t: return "🔧"
39
+ if "optical" in t: return "🔭"
40
+ if "chemical" in t: return "🧪"
41
+ if "flammab" in t: return "🔴"
42
+ if "component" in t: return "🧩"
43
+ if "descriptive" in t: return "📋"
44
+ return "📋"
45
+
46
+ def img_to_b64(path):
47
+ try:
48
+ ext = Path(path).suffix.lower().replace(".", "")
49
+ mime = "png" if ext == "png" else "jpeg"
50
+ with open(path, "rb") as f:
51
+ data = base64.b64encode(f.read()).decode()
52
+ return f"data:image/{mime};base64,{data}"
53
+ except Exception:
54
+ return ""
55
+
56
+
57
+ home_img = img_to_b64("images/Home.png")
58
+ logo_img = img_to_b64("logo.png")
59
+
60
+ st.markdown("""
61
+ <style>
62
+ section[data-testid="stMain"] {
63
+ background: #fff !important;
64
+ }
65
+ .stApp {
66
+ background: #fff !important;
67
+ }
68
+ </style>
69
+ """,
70
+ unsafe_allow_html=True
71
+ )
72
+
73
+ # Global style overrides
74
+ st.markdown("""
75
+ <style>
76
+ @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap');
77
+
78
+ /* Hide default Streamlit chrome */
79
+ .block-container { padding: 0 !important; max-width: 100% !important; background: #fff !important;}
80
+ [data-testid="stToolbar"],
81
+ [data-testid="stDecoration"],
82
+ [data-testid="stHeader"] { display: none !important; }
83
+
84
+
85
+
86
+
87
+
88
+ /* ── Search row ── */
89
+ /* Only target the search bar's horizontal block, not all columns */
90
+ .st-emotion-cache-y1l7l5 .st-emotion-cache-1permvm {
91
+ gap: 0 !important;
92
+ align-items: stretch !important;
93
+ }
94
+
95
+ .st-emotion-cache-16s2yzk button {
96
+ background: #f2f6f4 !important;
97
+ border: 1.5px solid #d0d8d4 !important;
98
+ border-right: none !important;
99
+ border-radius: 50px 0 0 50px !important;
100
+ color: #3a5248 !important;
101
+ font-family: 'DM Sans', sans-serif !important;
102
+ font-size: 0.8rem !important;
103
+ font-weight: 600 !important;
104
+ height: 46px !important;
105
+ padding: 0 16px !important;
106
+ white-space: nowrap !important;
107
+ box-shadow: none !important;
108
+ }
109
+
110
+ .st-emotion-cache-4dubyl [data-baseweb="input"] {
111
+ background: #000 !important;
112
+ height: 46px !important;
113
+ min-height: 46px !important;
114
+ border-radius: 0 !important;
115
+ border-color: #d0d8d4 !important;
116
+ border-left: none !important;
117
+ border-right: none !important;
118
+ }
119
+
120
+ .st-emotion-cache-4dubyl input {
121
+ background: #fff !important;
122
+ height: 46px !important;
123
+ padding-top: 0 !important;
124
+ padding-bottom: 0 !important;
125
+ font-family: 'DM Sans', sans-serif !important;
126
+ font-size: 0.92rem !important;
127
+ color: #000 !important;
128
+ box-shadow: none !important;
129
+ padding-left: 18px !important;
130
+ }
131
+
132
+ .st-emotion-cache-4dubyl > div {
133
+
134
+ height: 46px !important;
135
+ }
136
+ .st-emotion-cache-4dubyl input::placeholder {
137
+ color: #0f1f1a !important;
138
+ opacity: 0.4 !important;
139
+ }
140
+ .st-emotion-cache-mpgwbc button {
141
+ background: #8ACAFF !important;
142
+ border: 1.5px solid #8ACAFF !important;
143
+ border-left: none !important;
144
+ border-radius: 0 50px 50px 0 !important;
145
+ color: #0f1f1a !important;
146
+ font-family: 'DM Sans', sans-serif !important;
147
+ font-size: 0.88rem !important;
148
+ font-weight: 600 !important;
149
+ height: 46px !important;
150
+ padding: 0 28px !important;
151
+ box-shadow: none !important;
152
+ }
153
+
154
+ .st-key-view_all_btn button {
155
+ background: transparent !important;
156
+ border: none !important;
157
+ color: #8ACAFF !important;
158
+ font-family: 'DM Sans', sans-serif !important;
159
+ font-size: 0.85rem !important;
160
+ font-weight: 600 !important;
161
+ padding: 0 !important;
162
+ height: auto !important;
163
+ box-shadow: none !important;
164
+ cursor: pointer !important;
165
+ }
166
+ .st-key-view_all_btn button:hover {
167
+ color: #8ACAFF !important;
168
+ text-decoration: underline !important;
169
+ background: transparent !important;
170
+ }
171
+ .footer-nav-head {
172
+ font-size: 0.68rem;
173
+ font-weight: 700;
174
+ letter-spacing: 1.4px;
175
+ text-transform: uppercase;
176
+ color: #0f1f1a;
177
+ margin-bottom: 14px;
178
+ font-family: 'DM Sans', sans-serif;
179
+ }
180
+
181
+ </style>
182
+ """, unsafe_allow_html=True)
183
+
184
+
185
+ # Helper
186
+ def go_categorized(material=None, search=None):
187
+ if material:
188
+ st.session_state.material_type = material
189
+ if search:
190
+ st.session_state.search_term = search
191
+ st.switch_page("page_files/Categorized_Search.py")
192
+
193
+
194
+ def go_upload():
195
+ st.switch_page("page_files/Upload_Data.py")
196
+
197
+
198
+ # ══════════════════════════════════════════════════════════════════════════════
199
+ # 1. ANIMATION SECTION (pure HTML, no clickables needed)
200
+ # ══════════════════════════════════════════════════════════════════════════════
201
+ about_img_html = (
202
+ f"<div class='aim-about-img'><img src='{home_img}' alt='AIM platform diagram'/></div>"
203
+ if home_img else
204
+ "<div class='aim-about-img-placeholder'>[ Platform diagram ]</div>"
205
+ )
206
+
207
+ logo_html = (
208
+ f"<img src='{logo_img}' alt='AIM Logo' style='height:52px;width:52px;object-fit:contain;border-radius:14px;'/>"
209
+ if logo_img else ""
210
+ )
211
+
212
+ components.html(f"""
213
+ <!DOCTYPE html><html lang="en"><head>
214
+ <meta charset="UTF-8"/>
215
+ <link rel="preconnect" href="https://fonts.googleapis.com">
216
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
217
+ <style>
218
+ *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
219
+ body {{ font-family: 'DM Sans', sans-serif; background: #f7f7f5; color: #1a2e26; overflow-x: hidden; }}
220
+
221
+ /* ANIMATION SECTION */
222
+ .anim-section {{
223
+ background: #000;
224
+ padding: 80px 40px 60px;
225
+ display: flex; flex-direction: column; align-items: center;
226
+ }}
227
+ .anim-title {{
228
+ font-size: clamp(1.6rem, 3vw, 2.4rem); font-weight: 800;
229
+ color: #fff; text-align: center; letter-spacing: -1px; margin-bottom: 60px;
230
+ }}
231
+ .anim-stage {{
232
+ position: relative; width: 560px; height: 320px;
233
+ display: flex; align-items: center;
234
+ justify-content: space-between; padding: 0 40px;
235
+ }}
236
+
237
+ /* MAG GLASS */
238
+ .mag-wrap {{ position: relative; width: 100px; display: flex; flex-direction: column; align-items: center; }}
239
+ .mag-label {{ font-size: 0.65rem; font-weight: 600; letter-spacing: 1.5px; text-transform: uppercase; color: #4a7a6a; margin-bottom: 10px; }}
240
+ .mag-svg {{ width: 90px; height: 160px; overflow: visible; }}
241
+ .mag-lens-flash {{ opacity: 0; transition: opacity 0.6s ease 0.5s; }}
242
+ .mag-wrap.active .mag-lens-flash {{ opacity: 1; animation: lensFlash 1.8s ease-in-out infinite 0.5s; }}
243
+ @keyframes lensFlash {{ 0%,100% {{ opacity:.2; r:26; }} 50% {{ opacity:.7; r:28; }} }}
244
+ .mag-scan {{ opacity: 0; transition: opacity 0.3s ease 0.8s; }}
245
+ .mag-wrap.active .mag-scan {{ opacity: 1; animation: scanMove 1.2s ease-in-out infinite 0.8s; }}
246
+ @keyframes scanMove {{ 0% {{ transform:translateY(-14px);opacity:.8; }} 50% {{ transform:translateY(0);opacity:1; }} 100% {{ transform:translateY(14px);opacity:.8; }} }}
247
+
248
+ /* BUBBLES */
249
+ .bubble {{ position:absolute; border-radius:50%; background:radial-gradient(circle at 35% 35%,rgba(255,255,255,.6),rgba(80,160,255,.25)); border:1px solid rgba(100,180,255,.4); opacity:0; pointer-events:none; backdrop-filter:blur(2px); }}
250
+ .b1 {{ width:18px;height:18px;left:108px;bottom:155px; }}
251
+ .b2 {{ width:13px;height:13px;left:120px;bottom:175px; }}
252
+ .b3 {{ width:22px;height:22px;left:96px; bottom:140px; }}
253
+ .b4 {{ width:10px;height:10px;left:128px;bottom:185px; }}
254
+ .b5 {{ width:16px;height:16px;left:112px;bottom:165px; }}
255
+ .b6 {{ width:8px; height:8px; left:124px;bottom:195px; }}
256
+ .anim-stage.active .b1 {{ animation:floatBubble1 2.6s cubic-bezier(.25,.46,.45,.94) 1.2s forwards; }}
257
+ .anim-stage.active .b2 {{ animation:floatBubble2 2.2s cubic-bezier(.25,.46,.45,.94) 1.5s forwards; }}
258
+ .anim-stage.active .b3 {{ animation:floatBubble3 3.0s cubic-bezier(.25,.46,.45,.94) 1.0s forwards; }}
259
+ .anim-stage.active .b4 {{ animation:floatBubble4 2.0s cubic-bezier(.25,.46,.45,.94) 1.8s forwards; }}
260
+ .anim-stage.active .b5 {{ animation:floatBubble5 2.8s cubic-bezier(.25,.46,.45,.94) 1.3s forwards; }}
261
+ .anim-stage.active .b6 {{ animation:floatBubble6 1.8s cubic-bezier(.25,.46,.45,.94) 2.0s forwards; }}
262
+ @keyframes floatBubble1 {{ 0% {{ opacity:0;transform:translate(0,0) scale(.4); }} 15% {{ opacity:1;transform:translate(4px,-20px) scale(1); }} 60% {{ opacity:.9;transform:translate(120px,-55px) scale(.9); }} 85% {{ opacity:.5;transform:translate(280px,-30px) scale(.6); }} 100% {{ opacity:0;transform:translate(340px,-10px) scale(.2); }} }}
263
+ @keyframes floatBubble2 {{ 0% {{ opacity:0;transform:translate(0,0) scale(.3); }} 15% {{ opacity:1;transform:translate(-5px,-28px) scale(1); }} 60% {{ opacity:.9;transform:translate(110px,-70px) scale(.85); }} 85% {{ opacity:.4;transform:translate(265px,-40px) scale(.5); }} 100% {{ opacity:0;transform:translate(330px,-15px) scale(.15); }} }}
264
+ @keyframes floatBubble3 {{ 0% {{ opacity:0;transform:translate(0,0) scale(.5); }} 15% {{ opacity:1;transform:translate(6px,-15px) scale(1); }} 55% {{ opacity:.9;transform:translate(130px,-45px) scale(.95); }} 80% {{ opacity:.4;transform:translate(278px,-20px) scale(.55); }} 100% {{ opacity:0;transform:translate(345px,-5px) scale(.2); }} }}
265
+ @keyframes floatBubble4 {{ 0% {{ opacity:0;transform:translate(0,0) scale(.3); }} 20% {{ opacity:1;transform:translate(-8px,-35px) scale(1); }} 65% {{ opacity:.8;transform:translate(105px,-80px) scale(.8); }} 88% {{ opacity:.3;transform:translate(255px,-50px) scale(.4); }} 100% {{ opacity:0;transform:translate(320px,-20px) scale(.1); }} }}
266
+ @keyframes floatBubble5 {{ 0% {{ opacity:0;transform:translate(0,0) scale(.4); }} 15% {{ opacity:1;transform:translate(3px,-22px) scale(1); }} 58% {{ opacity:.9;transform:translate(115px,-60px) scale(.88); }} 83% {{ opacity:.45;transform:translate(270px,-35px) scale(.55); }} 100% {{ opacity:0;transform:translate(335px,-12px) scale(.2); }} }}
267
+ @keyframes floatBubble6 {{ 0% {{ opacity:0;transform:translate(0,0) scale(.25); }} 20% {{ opacity:1;transform:translate(-3px,-40px) scale(1); }} 62% {{ opacity:.7;transform:translate(100px,-85px) scale(.75); }} 85% {{ opacity:.25;transform:translate(250px,-55px) scale(.35); }} 100% {{ opacity:0;transform:translate(318px,-22px) scale(.1); }} }}
268
+
269
+ /* DATABASE */
270
+ .db-wrap {{ position:relative;width:130px;display:flex;flex-direction:column;align-items:center; }}
271
+ .db-label {{ font-size:.65rem;font-weight:600;letter-spacing:1.5px;text-transform:uppercase;color:#4a7a6a;margin-bottom:10px; }}
272
+ .cyl {{ opacity:.15;transition:opacity .5s ease; }}
273
+ .anim-stage.active .cyl-left {{ opacity:1;transition-delay:2.0s; }}
274
+ .anim-stage.active .cyl-right {{ opacity:1;transition-delay:2.2s; }}
275
+ .anim-stage.active .cyl-front {{ opacity:1;transition-delay:2.4s;animation:frontPulse 2.4s ease-in-out infinite 2.8s; }}
276
+ @keyframes frontPulse {{ 0%,100% {{ filter:brightness(1); }} 50% {{ filter:brightness(1.2); }} }}
277
+
278
+ /* CAPTION */
279
+ .anim-caption {{ margin-top:48px;display:flex;gap:40px;align-items:center; }}
280
+ .anim-step {{ display:flex;align-items:center;gap:10px;opacity:0;transform:translateY(10px);transition:opacity .5s ease,transform .5s ease; }}
281
+ .anim-stage.active ~ .anim-caption .anim-step:nth-child(1) {{ opacity:1;transform:none;transition-delay:.5s; }}
282
+ .anim-stage.active ~ .anim-caption .anim-step:nth-child(2) {{ opacity:1;transform:none;transition-delay:1s; }}
283
+ .anim-stage.active ~ .anim-caption .anim-step:nth-child(3) {{ opacity:1;transform:none;transition-delay:1.5s; }}
284
+ .step-text {{ font-size:.78rem;color:#5a8a7a;font-weight:500; }}
285
+ </style>
286
+ </head><body>
287
+
288
+ <section class="anim-section">
289
+ <h2 class="anim-title">From Experiment to Database</h2>
290
+ <div class="anim-stage" id="animStage">
291
+ <!-- MAG GLASS -->
292
+ <div class="mag-wrap" id="magWrap">
293
+ <div class="mag-label">Research</div>
294
+ <svg class="mag-svg" viewBox="0 0 90 160" fill="none" xmlns="http://www.w3.org/2000/svg">
295
+ <defs>
296
+ <radialGradient id="lensGrad" cx="40%" cy="38%" r="55%">
297
+ <stop offset="0%" stop-color="#4da6ff" stop-opacity="0.25"/>
298
+ <stop offset="100%" stop-color="#1a4a8a" stop-opacity="0.7"/>
299
+ </radialGradient>
300
+ <radialGradient id="flashGrad" cx="50%" cy="50%" r="50%">
301
+ <stop offset="0%" stop-color="#8ACAFF" stop-opacity="0.9"/>
302
+ <stop offset="100%" stop-color="#8ACAFF" stop-opacity="0"/>
303
+ </radialGradient>
304
+ </defs>
305
+ <circle cx="38" cy="38" r="32" fill="none" stroke="#2a5a8a" stroke-width="3"/>
306
+ <circle cx="38" cy="38" r="29" fill="url(#lensGrad)"/>
307
+ <line class="mag-scan" x1="16" y1="38" x2="60" y2="38" stroke="#8ACAFF" stroke-width="1.5" stroke-linecap="round" opacity="0.8"/>
308
+ <circle class="mag-lens-flash" cx="38" cy="38" r="26" fill="url(#flashGrad)"/>
309
+ <circle cx="24" cy="24" r="5" fill="white" opacity="0.15"/>
310
+ <circle cx="30" cy="34" r="2" fill="#8ACAFF" opacity="0.7"/>
311
+ <circle cx="42" cy="30" r="2" fill="#8ACAFF" opacity="0.5"/>
312
+ <circle cx="38" cy="44" r="2" fill="#8ACAFF" opacity="0.6"/>
313
+ <circle cx="48" cy="38" r="1.5" fill="#8ACAFF" opacity="0.4"/>
314
+ </svg>
315
+ </div>
316
+ <!-- BUBBLES -->
317
+ <div class="bubble b1"></div><div class="bubble b2"></div>
318
+ <div class="bubble b3"></div><div class="bubble b4"></div>
319
+ <div class="bubble b5"></div><div class="bubble b6"></div>
320
+ <!-- DATABASE -->
321
+ <div class="db-wrap">
322
+ <div class="db-label">Database</div>
323
+ <svg width="220" height="200" viewBox="0 0 220 200" fill="none" xmlns="http://www.w3.org/2000/svg">
324
+ <defs>
325
+ <linearGradient id="sideBlue" x1="0" y1="0" x2="1" y2="0">
326
+ <stop offset="0%" stop-color="#2a55b0"/><stop offset="50%" stop-color="#4a7fd8"/><stop offset="100%" stop-color="#2a55b0"/>
327
+ </linearGradient>
328
+ <linearGradient id="sideDark" x1="0" y1="0" x2="1" y2="0">
329
+ <stop offset="0%" stop-color="#0d1f5c"/><stop offset="50%" stop-color="#1a3a8a"/><stop offset="100%" stop-color="#0d1f5c"/>
330
+ </linearGradient>
331
+ <linearGradient id="topBlue" x1="0" y1="0" x2="0" y2="1">
332
+ <stop offset="0%" stop-color="#6a9fe0"/><stop offset="100%" stop-color="#3a6abf"/>
333
+ </linearGradient>
334
+ <linearGradient id="topDark" x1="0" y1="0" x2="0" y2="1">
335
+ <stop offset="0%" stop-color="#2a4a9a"/><stop offset="100%" stop-color="#0d1f5c"/>
336
+ </linearGradient>
337
+ </defs>
338
+ <!-- back left -->
339
+ <g class="cyl cyl-left">
340
+ <rect x="10" y="88" width="66" height="20" rx="1" fill="url(#sideBlue)"/>
341
+ <ellipse cx="43" cy="88" rx="33" ry="10" fill="url(#topBlue)"/>
342
+ <rect x="10" y="66" width="66" height="24" rx="1" fill="url(#sideBlue)"/>
343
+ <ellipse cx="43" cy="66" rx="33" ry="10" fill="url(#topBlue)"/>
344
+ <rect x="10" y="46" width="66" height="22" rx="1" fill="url(#sideBlue)"/>
345
+ <ellipse cx="43" cy="46" rx="33" ry="10" fill="url(#topBlue)"/>
346
+ <ellipse cx="43" cy="108" rx="33" ry="10" fill="#1e3a80"/>
347
+ </g>
348
+ <!-- back right -->
349
+ <g class="cyl cyl-right">
350
+ <rect x="144" y="88" width="66" height="20" rx="1" fill="url(#sideBlue)"/>
351
+ <ellipse cx="177" cy="88" rx="33" ry="10" fill="url(#topBlue)"/>
352
+ <rect x="144" y="66" width="66" height="24" rx="1" fill="url(#sideBlue)"/>
353
+ <ellipse cx="177" cy="66" rx="33" ry="10" fill="url(#topBlue)"/>
354
+ <rect x="144" y="46" width="66" height="22" rx="1" fill="url(#sideBlue)"/>
355
+ <ellipse cx="177" cy="46" rx="33" ry="10" fill="url(#topBlue)"/>
356
+ <ellipse cx="177" cy="108" rx="33" ry="10" fill="#1e3a80"/>
357
+ </g>
358
+ <!-- front center -->
359
+ <g class="cyl cyl-front">
360
+ <rect x="70" y="128" width="80" height="24" rx="1" fill="url(#sideDark)"/>
361
+ <ellipse cx="110" cy="128" rx="40" ry="13" fill="url(#topDark)"/>
362
+ <rect x="70" y="102" width="80" height="28" rx="1" fill="url(#sideDark)"/>
363
+ <ellipse cx="110" cy="102" rx="40" ry="13" fill="url(#topDark)"/>
364
+ <rect x="70" y="76" width="80" height="28" rx="1" fill="url(#sideDark)"/>
365
+ <ellipse cx="110" cy="76" rx="40" ry="13" fill="url(#topDark)"/>
366
+ <rect x="70" y="54" width="80" height="24" rx="1" fill="url(#sideDark)"/>
367
+ <ellipse cx="110" cy="54" rx="40" ry="13" fill="url(#topDark)"/>
368
+ <ellipse cx="110" cy="152" rx="40" ry="13" fill="#080f2a"/>
369
+ </g>
370
+ </svg>
371
+ </div>
372
+ </div>
373
+ <div class="anim-caption">
374
+ <div class="anim-step"><span class="step-text">Collect measurements</span></div>
375
+ <div class="anim-step"><span class="step-text">Process &amp; validate</span></div>
376
+ <div class="anim-step"><span class="step-text">Stored in AIM</span></div>
377
+ </div>
378
+ </section>
379
+
380
+ <script>
381
+ const stage = document.getElementById('animStage');
382
+ const magWrap = document.getElementById('magWrap');
383
+ const observer = new IntersectionObserver(entries => {{
384
+ entries.forEach(e => {{
385
+ if (e.isIntersecting) {{
386
+ setTimeout(() => {{ magWrap.classList.add('active'); stage.classList.add('active'); }}, 300);
387
+ }} else {{
388
+ magWrap.classList.remove('active'); stage.classList.remove('active');
389
+ }}
390
+ }});
391
+ }}, {{ threshold: 0.4 }});
392
+ observer.observe(document.querySelector('.anim-section'));
393
+ </script>
394
+ </body></html>
395
+ """, height=700, scrolling=False)
396
+
397
+ # ══════════════════════════════════════════════════════════════════════════════
398
+ # 2. HERO , heading + description (static HTML)
399
+ # ══════════════════════════════════════════════════════════════════════════════
400
+ st.markdown("""
401
+ <style>
402
+ .aim-hero-text {
403
+ background: #fff;
404
+ text-align: center;
405
+ padding: 64px 40px 24px;
406
+ border-bottom: none;
407
+ }
408
+ .aim-hero-text h1 {
409
+ font-family: 'DM Sans', sans-serif;
410
+ font-size: clamp(2rem, 4.5vw, 3.2rem);
411
+ font-weight: 800; color: #0f1f1a;
412
+ line-height: 1.1; letter-spacing: -1.5px;
413
+ margin-bottom: 18px;
414
+ }
415
+ .aim-hero-text p {
416
+ color: #5a6b65; font-size: 1rem; line-height: 1.65;
417
+ max-width: 500px; margin: 0 auto 28px;
418
+ }
419
+ </style>
420
+ <div class="aim-hero-text">
421
+ <h1>Accelerate Your Composites Research</h1>
422
+ <p>Access a centralized, open-source database for experimental composite material properties.
423
+ Polymer, fiber, and composite datasets , all in one place.</p>
424
+ </div>
425
+ """, unsafe_allow_html=True)
426
+
427
+
428
+
429
+ # ══════════════════════════════════════════════════════════════════════════════
430
+ # 4. STATS (static HTML)
431
+ # ══════════════════════════════════════════════════════════════════════════════
432
+ st.markdown(f"""
433
+ <style>
434
+ .aim-stats {{
435
+ background: #f7f7f5; border-bottom: 1px solid #e4e8e5;
436
+ display: flex; justify-content: center;
437
+ }}
438
+ .aim-stat {{
439
+ text-align: center; padding: 34px 56px;
440
+ border-right: 1px solid #e4e8e5;
441
+ }}
442
+ .aim-stat:last-child {{ border-right: none; }}
443
+ .aim-stat-num {{
444
+ font-family: 'DM Sans', sans-serif;
445
+ font-size: 2.1rem; font-weight: 800;
446
+ color: #8ACAFF; line-height: 1; margin-bottom: 6px;
447
+ }}
448
+ .aim-stat-label {{
449
+ font-size: 0.68rem; font-weight: 600;
450
+ letter-spacing: 1.4px; color: #7a8e87; text-transform: uppercase;
451
+ }}
452
+ </style>
453
+ <div class="aim-stats">
454
+ <div class="aim-stat"><div class="aim-stat-num">3</div><div class="aim-stat-label">Material Classes</div></div>
455
+ <div class="aim-stat"><div class="aim-stat-num">{prop_count}</div><div class="aim-stat-label">Properties Tracked</div></div>
456
+ <div class="aim-stat"><div class="aim-stat-num">8</div><div class="aim-stat-label">Research Teams</div></div>
457
+ <div class="aim-stat"><div class="aim-stat-num">2</div><div class="aim-stat-label">Universities</div></div>
458
+ </div>
459
+ """, unsafe_allow_html=True)
460
+
461
+
462
+ # ══════════════════════════════════════════════════════════════════════════════
463
+ # 5. MAJOR CATEGORIES , Streamlit columns + containers + buttons
464
+ # ══════════════════════════════════════════════════════════════════════════════
465
+ st.markdown("""
466
+ <style>
467
+ /* Card buttons - scoped to cards horizontal block only */
468
+ .st-emotion-cache-r3ry0f button {
469
+ background: #fff !important;
470
+ border: 1.5px solid #e4e8e5 !important;
471
+ border-radius: 12px !important;
472
+ padding: 24px 20px !important;
473
+ height: 160px !important;
474
+ width: 100% !important;
475
+ text-align: left !important;
476
+ white-space: pre-wrap !important;
477
+ line-height: 1.5 !important;
478
+ box-shadow: none !important;
479
+ transition: box-shadow 0.2s, border-color 0.2s !important;
480
+ }
481
+
482
+ .st-emotion-cache-r3ry0f button:hover {
483
+ border-color: #BAE1FC !important;
484
+ box-shadow: 0 0 0 3px rgba(186,225,252,0.25), 0 6px 22px rgba(0,0,0,0.07) !important;
485
+ background: #fff !important;
486
+ }
487
+ .st-emotion-cache-r3ry0f button p:first-child {
488
+ font-size: 1.1rem !important;
489
+ background: #f2f4f3 !important;
490
+ border-radius: 8px !important;
491
+ padding: 6px 8px !important;
492
+ display: inline-block !important;
493
+ margin-bottom: 12px !important;
494
+ line-height: 1 !important;
495
+ }
496
+ .st-emotion-cache-r3ry0f button p:nth-child(2) {
497
+ font-family: 'DM Sans', sans-serif !important;
498
+ font-size: 0.97rem !important;
499
+ font-weight: 700 !important;
500
+ color: #0f1f1a !important;
501
+ margin-bottom: 6px !important;
502
+ }
503
+ .st-emotion-cache-r3ry0f button p:last-child {
504
+ font-family: 'DM Sans', sans-serif !important;
505
+ font-size: 0.7rem !important;
506
+ font-weight: 700 !important;
507
+ letter-spacing: 1.2px !important;
508
+ color: #8ACAFF !important;
509
+ text-transform: uppercase !important;
510
+ }
511
+ .cards-section-wrap h2 {
512
+ font-family: 'DM Sans', sans-serif !important;
513
+ font-size: 1.4rem !important;
514
+ font-weight: 700 !important;
515
+ color: #0f1f1a !important;
516
+ margin-bottom: 6px !important;
517
+ }
518
+ .cards-section-wrap p {
519
+ color: #5a6b65 !important;
520
+ font-size: 0.9rem !important;
521
+ line-height: 1.6 !important;
522
+ margin-bottom: 16px !important;
523
+ }
524
+ </style>
525
+ """, unsafe_allow_html=True)
526
+
527
+
528
+
529
+ st.markdown("<div style='padding: 64px 0 0 0;'></div>", unsafe_allow_html=True)
530
+
531
+ st.markdown("""
532
+ <div class="cards-section-wrap">
533
+ <h2>Quick Links</h2>
534
+ <p>Open the pages you need most.</p>
535
+ </div>
536
+ """, unsafe_allow_html=True)
537
+
538
+ quick_cards = [
539
+ ("Extract Data", "Platform", "page_files/Upload_Data.py", "about", "📋"),
540
+ ("Contact Team", "Support", "page_files/Contact_Team.py", "contact", " 🛡️"),
541
+ ("Search", "Data Page", "page_files/Categorized_Search.py", "search", "🔎"),
542
+ ]
543
+
544
+ cols = st.columns(3, gap="large")
545
+ for col, (title, tag, target_page, key_suffix, badge) in zip(cols, quick_cards):
546
+ with col:
547
+ if st.button(
548
+ f"{badge}\n\n{title}\n\n{tag}",
549
+ key=f"quick_{key_suffix}_page",
550
+ use_container_width=True,
551
+ ):
552
+ st.switch_page(target_page)
553
+
554
+ st.markdown("<div style='background:#fff; padding: 24px 0; border-bottom: 1px solid #e8e8e4;'></div>", unsafe_allow_html=True)
555
+
556
+
557
+
558
+
559
+ # ══════════════════════════════════════════════════════════════════════════════
560
+ # 6. ABOUT + FOOTER (static HTML)
561
+ # ══════════════════════════════════════════════════════════════════════════════
562
+ components.html(f"""
563
+ <!DOCTYPE html><html lang="en"><head>
564
+ <meta charset="UTF-8"/>
565
+ <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
566
+ <style>
567
+ *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
568
+ body {{ font-family: 'DM Sans', sans-serif; color: #1a2e26; }}
569
+
570
+ .aim-about {{ background: #fff; padding: 60px; border-bottom: 1px solid #e8e8e4; }}
571
+ .aim-about h2 {{ font-size: 1.65rem; font-weight: 700; color: #0f1f1a; letter-spacing: -0.5px; margin-bottom: 8px; }}
572
+ .aim-about-sub {{ color: #6a7e77; font-size: 0.9rem; margin-bottom: 30px; }}
573
+ .aim-about-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 48px; align-items: start; }}
574
+ .aim-about-text p {{ color: #3a4e47; font-size: 0.91rem; line-height: 1.75; margin-bottom: 14px; }}
575
+ .aim-about-img {{ border-radius: 10px; overflow: hidden; border: 1px solid #e4e8e5; }}
576
+ .aim-about-img img {{ width: 100%; display: block; }}
577
+ .aim-about-img-placeholder {{
578
+ background: #f0f5f3; border-radius: 10px; height: 280px;
579
+ display: flex; align-items: center; justify-content: center;
580
+ color: #7a9e8f; font-size: 0.85rem; border: 1.5px dashed #c8d6d0;
581
+ }}
582
+
583
+ .aim-footer {{ background: #fff; border-top: 1px solid #e8e8e4; padding: 50px 60px 28px; }}
584
+ .aim-footer-brand {{ display: flex; align-items: center; gap: 8px; font-weight: 700; color: #0f1f1a; font-size: 0.95rem; margin-bottom: 10px; }}
585
+ .aim-footer-desc {{ font-size: 0.82rem; color: #7a8e87; line-height: 1.6; }}
586
+ </style>
587
+ </head><body>
588
+
589
+ <section class="aim-about">
590
+ <h2>About the Platform</h2>
591
+ <p class="aim-about-sub">Artificially Intelligent Manufacturing Paradigm (AIM) for Composites</p>
592
+ <div class="aim-about-grid">
593
+ <div class="aim-about-text">
594
+ <p>The AIM Database tool serves as a powerful, centralized hub designed to streamline
595
+ collaboration and information exchange within the composite materials research community.
596
+ The platform enables researchers to contribute to a shared knowledge base by uploading
597
+ experimental datasets through secure terminals.</p>
598
+ <p>Users can submit specific measurements regarding mechanical properties, thermal behavior,
599
+ and rheology, alongside their published journal papers , ensuring that both raw data and
600
+ peer-reviewed findings are integrated into one cohesive system.</p>
601
+ <p>All contributed information is securely aggregated within a central cloud architecture,
602
+ allowing for efficient storage, organization, and retrieval across polymer, fiber, and
603
+ composite categories.</p>
604
+ </div>
605
+ <div>{about_img_html}</div>
606
+ </div>
607
+ </section>
608
+
609
+
610
+
611
+ </body></html>
612
+ """, height=550, scrolling=False)
613
+
614
+ st.markdown("""
615
+ <style>
616
+
617
+ /* Footer nav wrapper */
618
+ div[data-testid="stHorizontalBlock"]:has(.footer-nav-head) {
619
+ background: #fff !important;
620
+ border-bottom: 1px solid #e8e8e4 !important;
621
+ padding: 0 60px 28px !important;
622
+ margin-top: -16px !important;
623
+
624
+ }
625
+ .aim-footer-brand {
626
+ display: flex;
627
+ align-items: center;
628
+ gap: 8px;
629
+ font-weight: 700;
630
+ color: #0f1f1a;
631
+ font-size: 0.95rem;
632
+ margin-bottom: 10px;
633
+ font-family: 'DM Sans', sans-serif;
634
+ }
635
+ .aim-footer-desc {
636
+ font-size: 0.82rem;
637
+ color: #7a8e87;
638
+ line-height: 1.6;
639
+ font-family: 'DM Sans', sans-serif;
640
+ }
641
+ .footer-nav-head {
642
+ font-size: 0.68rem;
643
+ font-weight: 700;
644
+ letter-spacing: 1.4px;
645
+ text-transform: uppercase;
646
+ color: #0f1f1a;
647
+ margin-bottom: 14px;
648
+ font-family: 'DM Sans', sans-serif;
649
+ }
650
+ .st-emotion-cache-1ofqig9 {
651
+ background: transparent !important;
652
+ border: none !important;
653
+ padding: 0 !important;
654
+ }
655
+ .st-emotion-cache-1qeq59m {
656
+ background: transparent !important;
657
+ border: none !important;
658
+ text-decoration: none !important;
659
+ padding: 0 0 9px 0 !important;
660
+ display: block !important;
661
+ }
662
+ .st-emotion-cache-1qeq59m:hover {
663
+ background: transparent !important;
664
+ border: none !important;
665
+ }
666
+ .st-emotion-cache-1qeq59m p {
667
+ color: #6a7e77 !important;
668
+ font-family: 'DM Sans', sans-serif !important;
669
+ font-size: 0.83rem !important;
670
+ font-weight: 400 !important;
671
+ margin: 0 !important;
672
+ }
673
+ .st-emotion-cache-1qeq59m:hover p {
674
+ color: #8ACAFF !important;
675
+ }
676
+
677
+ /* copyright bar */
678
+ .aim-footer-bottom {
679
+ background: #fff;
680
+ border-top: 1px solid #e8e8e4;
681
+ padding: 22px 60px;
682
+ font-size: 0.76rem;
683
+ color: #a0b0aa;
684
+ font-family: 'DM Sans', sans-serif;
685
+ }
686
+
687
+ </style>
688
+ """, unsafe_allow_html=True)
689
+
690
+ brand_col, db_col, sup_col, _ = st.columns([4, 2, 2, 2], vertical_alignment="top", width="stretch")
691
+
692
+ with brand_col:
693
+ st.markdown(f"""
694
+ <div class="aim-footer-brand">{logo_html} AIM Composites</div>
695
+ <p class="aim-footer-desc">Advancing composites research through open data and collaborative tools.
696
+ A joint initiative of Clemson University and University of Delaware.</p>
697
+ """, unsafe_allow_html=True)
698
+
699
+ with sup_col:
700
+ st.markdown('<p class="footer-nav-head">DATABASE</p>', unsafe_allow_html=True)
701
+ st.page_link(
702
+ "page_files/Categorized_Search.py",
703
+ label="Browse Materials",
704
+ )
705
+ st.page_link(
706
+ "page_files/Categorized_Search.py",
707
+ label="Categorized Search",
708
+ )
709
+ st.page_link(
710
+ "page_files/Upload_Data.py",
711
+ label="Upload Data",
712
+ )
713
+
714
+ with _:
715
+ st.markdown('<p class="footer-nav-head">SUPPORT</p>', unsafe_allow_html=True)
716
+ st.page_link(
717
+ "page_files/Contact_Team.py",
718
+ label="Contact Team",
719
+ )
720
+
721
+
722
+
page_files/Upload_Data.py ADDED
@@ -0,0 +1,887 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import tempfile
4
+ import base64
5
+
6
+ import fitz # PyMuPDF
7
+ import pandas as pd
8
+ import streamlit as st
9
+
10
+ from data_loader import insert_material_rows
11
+ from page_files.categorized.Backend.upload_backend import (
12
+ call_gemini_from_bytes,
13
+ convert_to_dataframe,
14
+ create_zip,
15
+ extract_images,
16
+ save_matched_images,
17
+ save_single_image_with_property,
18
+ )
19
+ def inject_upload_page_styles():
20
+ st.markdown(
21
+ """
22
+ <style>
23
+ @import url("https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700;800&display=swap");
24
+
25
+ [data-testid="stHeader"] {
26
+ display: none !important;
27
+ }
28
+
29
+ .stApp {
30
+ background: #f3f6fb !important;
31
+ }
32
+
33
+ html, body, [class*="css"] {
34
+ font-family: "DM Sans", sans-serif !important;
35
+ }
36
+
37
+ .block-container {
38
+ max-width: 980px !important;
39
+ padding-top: 1rem !important;
40
+ padding-bottom: 2rem !important;
41
+ }
42
+ .st-emotion-cache-tn0cau {
43
+ background: #ffffff !important;
44
+
45
+
46
+
47
+ }
48
+ div[class*="st-key-ud_main_card"] > div[data-testid="stVerticalBlockBorderWrapper"] > div {
49
+ background: #ffffff !important;
50
+ border: 1px solid #dbe3ee !important;
51
+ border-radius: 16px !important;
52
+ padding: 28px 32px 32px 32px !important;
53
+ box-shadow: 0 4px 24px rgba(15, 23, 42, 0.08) !important;
54
+ }
55
+
56
+
57
+ /* Card wrapper like cardref */
58
+ .upload-card {
59
+ max-width: 960px;
60
+ margin: 2.5rem auto;
61
+ padding: 2.25rem 2.5rem;
62
+ border-radius: 18px;
63
+ background: #ffffff;
64
+ box-shadow: 0 18px 45px rgba(15, 23, 42, 0.09);
65
+ border: 1px solid #e4e7f0;
66
+ }
67
+
68
+ /* Upload section layout */
69
+ .upload-section {
70
+ display: flex;
71
+ align-items: center; /* vertical alignment */
72
+ justify-content: space-between;
73
+ gap: 1.5rem;
74
+ margin-top: 1.25rem;
75
+ }
76
+
77
+ .upload-dropzone {
78
+ flex: 1;
79
+ }
80
+
81
+ .upload-button-wrap {
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ }
86
+
87
+ .upload-button-wrap button {
88
+ min-width: 160px;
89
+ }
90
+
91
+ div[class*="st-key-ud_main_card"] [data-testid="stVerticalBlockBorderWrapper"] {
92
+ background: #ffffff !important;
93
+ border: 1px solid #dbe3ee !important;
94
+ border-radius: 16px !important;
95
+ box-shadow: 0 4px 24px rgba(15, 23, 42, 0.08) !important;
96
+ }
97
+ span.st-emotion-cache-epvm6 {
98
+ display: flex !important;
99
+ justify-content: center !important;
100
+ width: 100% !important;
101
+ }
102
+ .ud-page-title {
103
+ color: #111827;
104
+ font-size: 2.2rem;
105
+ line-height: 1.08;
106
+ font-weight: 800;
107
+ margin: 0 0 8px 0;
108
+ }
109
+
110
+ .ud-page-desc {
111
+ color: #64748b;
112
+ font-size: 1rem;
113
+ margin: 0 0 16px 0;
114
+ }
115
+
116
+ .ud-topbar {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 10px;
120
+ background: #bae1fc;
121
+ border: 4px solid #d7e4f2;
122
+ border-radius: 20px;
123
+ color: #111827;
124
+ font-size: 1.05rem;
125
+ font-weight: 700;
126
+ padding: 12px 14px;
127
+ margin-bottom: 7px;
128
+ }
129
+
130
+ .ud-topbar img {
131
+ width: 20px;
132
+ height: 20px;
133
+ object-fit: contain;
134
+ border-radius: 4px;
135
+ }
136
+
137
+
138
+
139
+ div[class*="st-key-material_ident_card"] [data-testid="stVerticalBlockBorderWrapper"] {
140
+ background: transparent !important;
141
+ border: 0 !important;
142
+ border-radius: 0 !important;
143
+ padding: 0 !important;
144
+ box-shadow: none !important;
145
+ }
146
+
147
+ div[class*="st-key-material_form_card"] [data-testid="stVerticalBlockBorderWrapper"] {
148
+ background: transparent !important;
149
+ border: 0 !important;
150
+ border-radius: 0 !important;
151
+ padding: 0 !important;
152
+ box-shadow: none !important;
153
+ }
154
+
155
+ .ud-ident-title {
156
+ color: #111827;
157
+ font-size: 2rem;
158
+ font-weight: 800;
159
+ margin: 4px 0 8px 2px;
160
+ display: flex;
161
+ align-items: center;
162
+ gap: 8px;
163
+ }
164
+
165
+ .ud-sec-icon {
166
+ width: 18px;
167
+ height: 18px;
168
+ border-radius: 999px;
169
+ background: #2563eb;
170
+ color: #ffffff;
171
+ display: inline-flex;
172
+ align-items: center;
173
+ justify-content: center;
174
+ font-size: 0.72rem;
175
+ font-weight: 700;
176
+ line-height: 1;
177
+ }
178
+
179
+ .ud-upload-title {
180
+ color: #111827;
181
+ font-size: 1.9rem;
182
+ font-weight: 800;
183
+ margin: 12px 0 8px 0;
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 8px;
187
+ }
188
+
189
+ div[class*="st-key-material_ident_card"] label p {
190
+ color: #1f2937 !important;
191
+ font-size: 0.95rem !important;
192
+ font-weight: 600 !important;
193
+ }
194
+
195
+ div[class*="st-key-material_ident_card"] div[data-baseweb="select"] > div,
196
+ div[class*="st-key-material_ident_card"] div[data-baseweb="input"] > div {
197
+ min-height: 46px !important;
198
+ border-radius: 10px !important;
199
+ border: 1px solid #d6dee8 !important;
200
+ background: #f8fafc !important;
201
+ }
202
+
203
+ [data-testid="stFileUploaderDropzone"] {
204
+ background: #f8fbff !important;
205
+ border: 2px dashed #d4deea !important;
206
+ border-radius: 14px !important;
207
+ min-height: 230px !important;
208
+ padding: 1.4rem !important;
209
+ position: relative !important;
210
+ display: flex !important;
211
+ flex-direction: column !important;
212
+ align-items: center !important;
213
+ justify-content: center !important;
214
+ }
215
+
216
+ /* The inner flex column — center everything */
217
+ [data-testid="stFileUploaderDropzone"] > div {
218
+ display: flex !important;
219
+ flex-direction: column !important;
220
+ align-items: center !important;
221
+ justify-content: center !important;
222
+ text-align: center !important;
223
+ gap: 10px !important;
224
+ width: 100% !important;
225
+ }
226
+
227
+ /* Browse files button itself */
228
+ [data-testid="stFileUploaderDropzone"] button,
229
+ [data-testid="stFileUploaderDropzone"] > div button {
230
+ background: #2f6fe4 !important;
231
+ color: #ffffff !important;
232
+ border: 0 !important;
233
+ border-radius: 9px !important;
234
+ font-weight: 700 !important;
235
+ padding: 0.45rem 1.25rem !important;
236
+ display: block !important;
237
+ margin: 0 auto !important;
238
+ }
239
+
240
+ /* Streamlit wraps button in a top-level span; center that wrapper */
241
+ [data-testid="stFileUploaderDropzone"] > span {
242
+ display: flex !important;
243
+ justify-content: center !important;
244
+ width: 100% !important;
245
+ margin-top: 0.5rem !important;
246
+ }
247
+
248
+ [data-testid="stFileUploaderDropzone"] [data-testid="stFileUploaderDropzoneInstructions"] {
249
+ width: 100% !important;
250
+ display: flex !important;
251
+ flex-direction: column !important;
252
+ align-items: center !important;
253
+ justify-content: center !important;
254
+ text-align: center !important;
255
+ }
256
+
257
+ /* The "Limit 200MB" small text */
258
+ [data-testid="stFileUploaderDropzone"] small {
259
+ font-size: 0.96rem !important;
260
+ text-align: center !important;
261
+ display: block !important;
262
+ }
263
+
264
+ /* Cloud icon / drag text paragraph */
265
+ [data-testid="stFileUploaderDropzone"] p,
266
+ [data-testid="stFileUploaderDropzone"] div > p {
267
+ text-align: center !important;
268
+ width: 100% !important;
269
+ }
270
+ </style>
271
+ """,
272
+ unsafe_allow_html=True,
273
+ )
274
+
275
+
276
+ def render_top_bar():
277
+ logo_html = ""
278
+ try:
279
+ with open("logo.png", "rb") as fh:
280
+ logo_b64 = base64.b64encode(fh.read()).decode()
281
+ logo_html = f"<img src='data:image/png;base64,{logo_b64}' alt='AIM'/>"
282
+ except Exception:
283
+ logo_html = ""
284
+
285
+ st.markdown(
286
+ f"<div class='ud-topbar'>{logo_html}<span>AIM Composites</span></div>",
287
+ unsafe_allow_html=True,
288
+ )
289
+
290
+
291
+ def input_form():
292
+ property_categories = {
293
+ "Polymer": [
294
+ "Thermal",
295
+ "Mechanical",
296
+ "Processing",
297
+ "Physical",
298
+ "Descriptive",
299
+ ],
300
+ "Fiber": [
301
+ "Mechanical",
302
+ "Physical",
303
+ "Thermal",
304
+ "Descriptive",
305
+ ],
306
+ "Composite": [
307
+ "Mechanical",
308
+ "Thermal",
309
+ "Processing",
310
+ "Physical",
311
+ "Descriptive",
312
+ "Composition / Reinforcement",
313
+ "Architecture / Structure",
314
+ ],
315
+ }
316
+
317
+ property_names = {
318
+ "Polymer": {
319
+ "Thermal": [
320
+ "Glass transition temperature (Tg)",
321
+ "Melting temperature (Tm)",
322
+ "Crystallization temperature (Tc)",
323
+ "Degree of crystallinity",
324
+ "Decomposition temperature",
325
+ ],
326
+ "Mechanical": [
327
+ "Tensile modulus",
328
+ "Tensile strength",
329
+ "Elongation at break",
330
+ "Flexural modulus",
331
+ "Impact strength",
332
+ ],
333
+ "Processing": [
334
+ "Melt flow index (MFI)",
335
+ "Processing temperature",
336
+ "Cooling rate",
337
+ "Mold shrinkage",
338
+ ],
339
+ "Physical": [
340
+ "Density",
341
+ "Specific gravity",
342
+ ],
343
+ "Descriptive": [
344
+ "Material grade",
345
+ "Manufacturer",
346
+ ],
347
+ },
348
+ "Fiber": {
349
+ "Mechanical": [
350
+ "Tensile modulus",
351
+ "Tensile strength",
352
+ "Strain to failure",
353
+ ],
354
+ "Physical": [
355
+ "Density",
356
+ "Fiber diameter",
357
+ ],
358
+ "Thermal": [
359
+ "Decomposition temperature",
360
+ ],
361
+ "Descriptive": [
362
+ "Fiber type",
363
+ "Surface treatment",
364
+ ],
365
+ },
366
+ "Composite": {
367
+ "Mechanical": [
368
+ "Longitudinal modulus (E1)",
369
+ "Transverse modulus (E2)",
370
+ "Shear modulus (G12)",
371
+ "Poissons ratio (V12)",
372
+ "Tensile strength (fiber direction)",
373
+ "Interlaminar shear strength",
374
+ ],
375
+ "Thermal": [
376
+ "Glass transition temperature (matrix)",
377
+ "Coefficient of thermal expansion (CTE)",
378
+ ],
379
+ "Processing": [
380
+ "Curing temperature",
381
+ "Curing pressure",
382
+ ],
383
+ "Physical": [
384
+ "Density",
385
+ ],
386
+ "Descriptive": [
387
+ "Laminate type",
388
+ ],
389
+ "Composition / Reinforcement": [
390
+ "Fiber volume fraction",
391
+ "Fiber weight fraction",
392
+ "Fiber type",
393
+ "Matrix type",
394
+ ],
395
+ "Architecture / Structure": [
396
+ "Weave type",
397
+ "Ply orientation",
398
+ "Number of plies",
399
+ "Stacking sequence",
400
+ ],
401
+ },
402
+ }
403
+
404
+ with st.container(border=False, key="material_ident_card"):
405
+ st.markdown("<div class='ud-ident-title'><span class='ud-sec-icon'>i</span>Material Identification</div>", unsafe_allow_html=True)
406
+
407
+ col_a, col_b = st.columns(2)
408
+ with col_a:
409
+ material_class = st.selectbox(
410
+ "Material Class",
411
+ ("Polymer", "Fiber", "Composite"),
412
+ index=None,
413
+ placeholder="Choose material class",
414
+ key="manual_material_class",
415
+ )
416
+ with col_b:
417
+ if material_class:
418
+ property_category = st.selectbox(
419
+ "Property Type",
420
+ property_categories[material_class],
421
+ index=None,
422
+ placeholder="Choose property type",
423
+ key="manual_property_category",
424
+ )
425
+ else:
426
+ property_category = None
427
+ st.selectbox(
428
+ "Property Type",
429
+ ["Choose material class first"],
430
+ index=0,
431
+ disabled=True,
432
+ key="manual_property_category_disabled",
433
+ )
434
+
435
+ if material_class and property_category:
436
+ property_options = property_names[material_class][property_category] + ["Something else"]
437
+ property_name = st.selectbox(
438
+ "Property Name",
439
+ property_options,
440
+ index=None,
441
+ placeholder="Choose property",
442
+ key="manual_property_name",
443
+ )
444
+ else:
445
+ property_name = None
446
+
447
+ custom_property_name = ""
448
+ if property_name == "Something else":
449
+ custom_property_name = st.text_input(
450
+ "Custom Property Name",
451
+ placeholder="Type property name",
452
+ key="manual_custom_property_name",
453
+ ).strip()
454
+
455
+ selected_property_name = (
456
+ custom_property_name if property_name == "Something else" else property_name
457
+ )
458
+
459
+ if material_class and property_category and selected_property_name:
460
+ with st.container(border=False, key="material_form_card"):
461
+ with st.form("user_input"):
462
+ st.subheader("Enter Data")
463
+
464
+ material_name = st.text_input("Material Name")
465
+ material_abbr = st.text_input("Material Abbreviation")
466
+
467
+ value = st.text_input("Value")
468
+ unit = st.text_input("Unit (SI)")
469
+ english = st.text_input("English Units")
470
+ test_condition = st.text_input("Test Condition")
471
+ comments = st.text_area("Comments")
472
+
473
+ submitted = st.form_submit_button("Submit")
474
+
475
+ if submitted:
476
+ if not (material_name and value):
477
+ st.error("Material name and value are required.")
478
+ return False
479
+ else:
480
+ input_db = pd.DataFrame(
481
+ [
482
+ {
483
+ "material_class": material_class,
484
+ "material_name": material_name,
485
+ "material_abbreviation": material_abbr,
486
+ "section": property_category,
487
+ "property_name": selected_property_name,
488
+ "value": value,
489
+ "unit": unit,
490
+ "english": english,
491
+ "test_condition": test_condition,
492
+ "comments": comments,
493
+ }
494
+ ]
495
+ )
496
+
497
+ try:
498
+ inserted = insert_material_rows(input_db)
499
+ except Exception as exc:
500
+ st.error(f"Failed to save to PostgreSQL: {exc}")
501
+ return False
502
+
503
+ if inserted <= 0:
504
+ st.error("No rows were inserted into PostgreSQL.")
505
+ return False
506
+
507
+ st.cache_data.clear()
508
+ st.success("Property added successfully to PostgreSQL.")
509
+ st.dataframe(input_db)
510
+ return True
511
+
512
+ return False
513
+
514
+ return False
515
+
516
+
517
+ def main():
518
+ inject_upload_page_styles()
519
+ render_top_bar()
520
+
521
+
522
+ st.subheader("Submit Scientific Material")
523
+ st.caption("Provide technical data and research documentation for the central repository.")
524
+
525
+
526
+ if "image_results" not in st.session_state:
527
+ st.session_state.image_results = []
528
+ if "pdf_processed" not in st.session_state:
529
+ st.session_state.pdf_processed = False
530
+ if "current_pdf_name" not in st.session_state:
531
+ st.session_state.current_pdf_name = None
532
+ if "form_submitted" not in st.session_state:
533
+ st.session_state.form_submitted = False
534
+ if "pdf_data_extracted" not in st.session_state:
535
+ st.session_state.pdf_data_extracted = False
536
+ if "pdf_extracted_df" not in st.session_state:
537
+ st.session_state.pdf_extracted_df = pd.DataFrame()
538
+ if "saved_image_mapping" not in st.session_state:
539
+ st.session_state.saved_image_mapping = {}
540
+
541
+
542
+ with st.container(border=True, key="ud_main_card"):
543
+ if input_form():
544
+ st.session_state.form_submitted = True
545
+
546
+
547
+ st.markdown("<div class='ud-upload-title'><span class='ud-sec-icon'>i</span>Research Documentation</div>", unsafe_allow_html=True)
548
+
549
+ uploaded_file = st.file_uploader(
550
+ "Upload PDF (Material Datasheet or Research Paper)", type=["pdf"]
551
+ )
552
+
553
+
554
+ if not uploaded_file:
555
+ st.info("Upload a PDF to extract material data and plots")
556
+
557
+ if not uploaded_file:
558
+ st.session_state.pdf_processed = False
559
+ st.session_state.current_pdf_name = None
560
+ st.session_state.image_results = []
561
+ st.session_state.form_submitted = False
562
+ st.session_state.pdf_data_extracted = False
563
+ st.session_state.pdf_extracted_df = pd.DataFrame()
564
+ st.session_state.saved_image_mapping = {}
565
+ return
566
+
567
+ paper_id = os.path.splitext(uploaded_file.name)[0].replace(" ", "_")
568
+
569
+ if st.session_state.current_pdf_name != uploaded_file.name:
570
+ st.session_state.pdf_processed = False
571
+ st.session_state.current_pdf_name = uploaded_file.name
572
+ st.session_state.image_results = []
573
+ st.session_state.form_submitted = False
574
+ st.session_state.saved_image_mapping = {}
575
+
576
+ if st.session_state.form_submitted:
577
+ st.session_state.form_submitted = False
578
+ st.info(
579
+ "A Form was submitted. But your previous extracted data has been added already. "
580
+ "If you want to extract more data/plots upload again"
581
+ )
582
+ tab1, tab2 = st.tabs(["Material Data", "Extracted Plots"])
583
+ with tab1:
584
+ st.info("Material data from form has been added to database.")
585
+ with tab2:
586
+ st.info("Plots already extracted")
587
+ return
588
+
589
+ tab1, tab2 = st.tabs([" Material Data", " Extracted Plots"])
590
+
591
+ with tempfile.TemporaryDirectory() as tmpdir:
592
+ pdf_path = os.path.join(tmpdir, uploaded_file.name)
593
+ with open(pdf_path, "wb") as f:
594
+ f.write(uploaded_file.getbuffer())
595
+
596
+ with tab1:
597
+ st.subheader("Material Properties Data")
598
+
599
+ if not st.session_state.pdf_data_extracted:
600
+ with st.spinner(" Extracting material data..."):
601
+ with open(pdf_path, "rb") as f:
602
+ pdf_bytes = f.read()
603
+
604
+ data = call_gemini_from_bytes(pdf_bytes, uploaded_file.name)
605
+
606
+ if data:
607
+ df = convert_to_dataframe(data)
608
+ if not df.empty:
609
+ st.session_state.pdf_extracted_df = df
610
+ st.session_state.pdf_data_extracted = True
611
+ st.session_state.pdf_extracted_meta = data
612
+ else:
613
+ st.warning("No data extracted")
614
+ else:
615
+ st.error("Failed to extract data from PDF")
616
+
617
+ df = st.session_state.pdf_extracted_df
618
+
619
+ if not df.empty:
620
+ data = st.session_state.get("pdf_extracted_meta", {})
621
+ st.success(f"Extracted {len(df)} properties")
622
+
623
+ col1, col2 = st.columns(2)
624
+ with col1:
625
+ st.metric("Material", data.get("material_name", "N/A"))
626
+ with col2:
627
+ st.metric("Abbreviation", data.get("material_abbreviation", "N/A"))
628
+
629
+ st.dataframe(df, use_container_width=True, height=400)
630
+ st.subheader("Assign Material Category")
631
+
632
+ extracted_material_class = st.selectbox(
633
+ "Select category for this material",
634
+ ["Polymer", "Fiber", "Composite"],
635
+ index=None,
636
+ placeholder="Required before adding to database",
637
+ )
638
+
639
+ if st.button("+Add to Database"):
640
+ if not extracted_material_class:
641
+ st.error("Please select a material category before adding.")
642
+ else:
643
+ df["material_class"] = extracted_material_class
644
+ df["material_type"] = extracted_material_class
645
+
646
+ if st.session_state.image_results:
647
+ with st.spinner("Saving matched plot images..."):
648
+ saved_images = save_matched_images(
649
+ df,
650
+ st.session_state.image_results,
651
+ save_dir="images",
652
+ )
653
+
654
+ if saved_images:
655
+ st.success(f" Saved {len(saved_images)} plot image(s)")
656
+ with st.expander("View saved images"):
657
+ for img_info in saved_images:
658
+ st.write(
659
+ f"? **{img_info['property']}** ? {img_info['caption']}"
660
+ )
661
+ st.write(f" Saved to: `{img_info['path']}`")
662
+ else:
663
+ st.info("? No plots matched the extracted properties")
664
+
665
+ if "user_uploaded_data" not in st.session_state:
666
+ st.session_state["user_uploaded_data"] = df
667
+ else:
668
+ st.session_state["user_uploaded_data"] = pd.concat(
669
+ [st.session_state["user_uploaded_data"], df],
670
+ ignore_index=True,
671
+ )
672
+
673
+ st.success(f"Added to {extracted_material_class} database!")
674
+
675
+ with tab2:
676
+ st.subheader("Extracted Plot Images")
677
+
678
+ if not st.session_state.pdf_processed:
679
+ with st.spinner(" Extracting plots from PDF..."):
680
+ doc = fitz.open(pdf_path)
681
+ st.session_state.image_results = extract_images(doc)
682
+ doc.close()
683
+ st.session_state.pdf_processed = True
684
+
685
+ if st.session_state.image_results:
686
+ has_extracted_data = not st.session_state.pdf_extracted_df.empty
687
+
688
+ if has_extracted_data:
689
+ mat_abbr = st.session_state.pdf_extracted_df.iloc[0][
690
+ "material_abbreviation"
691
+ ]
692
+ property_list = (
693
+ st.session_state.pdf_extracted_df["property_name"].unique().tolist()
694
+ )
695
+
696
+ st.info(
697
+ f" Material: **{mat_abbr}** | {len(property_list)} properties available for mapping"
698
+ )
699
+ else:
700
+ st.warning(
701
+ " No extracted material data found. Please extract material data first (Tab 1) to enable property mapping."
702
+ )
703
+
704
+ subtab1, subtab2 = st.tabs([" Images", "JSON Preview"])
705
+
706
+ with subtab1:
707
+ st.success(
708
+ f"Extracted {len(st.session_state.image_results)} plots"
709
+ )
710
+
711
+ col_img, col_json, col_all = st.columns(3)
712
+
713
+ with col_img:
714
+ img_zip = create_zip(st.session_state.image_results, include_json=False)
715
+ st.download_button(
716
+ " Download Images Only",
717
+ data=img_zip,
718
+ file_name=f"{paper_id}_images.zip",
719
+ mime="application/zip",
720
+ use_container_width=True,
721
+ key="download_images",
722
+ )
723
+
724
+ with col_json:
725
+ json_data = [
726
+ {
727
+ "caption": r["caption"],
728
+ "page": r["page"],
729
+ "image_count": len(r["image_data"]),
730
+ }
731
+ for r in st.session_state.image_results
732
+ ]
733
+ st.download_button(
734
+ " Download JSON",
735
+ data=json.dumps(json_data, indent=4),
736
+ file_name=f"{paper_id}_metadata.json",
737
+ mime="application/json",
738
+ use_container_width=True,
739
+ key="download_json_top",
740
+ )
741
+
742
+ with col_all:
743
+ full_zip = create_zip(st.session_state.image_results, include_json=True)
744
+ st.download_button(
745
+ " Download All",
746
+ data=full_zip,
747
+ file_name=f"{paper_id}_complete.zip",
748
+ mime="application/zip",
749
+ use_container_width=True,
750
+ key="download_all",
751
+ )
752
+
753
+ st.divider()
754
+
755
+ if st.session_state.saved_image_mapping:
756
+ with st.expander(" Saved Image Mappings", expanded=False):
757
+ for img_key, mapping_info in st.session_state.saved_image_mapping.items():
758
+ st.write(
759
+ f" **{mapping_info['caption']}** ? `{mapping_info['property']}`"
760
+ )
761
+ st.write(
762
+ f" Saved as: `{mapping_info['filename']}`"
763
+ )
764
+ st.divider()
765
+
766
+ results_copy = st.session_state.image_results.copy()
767
+
768
+ for idx in range(len(results_copy)):
769
+ if idx >= len(st.session_state.image_results):
770
+ break
771
+
772
+ result = st.session_state.image_results[idx]
773
+
774
+ with st.container(border=True):
775
+ col_cap, col_btn = st.columns([0.85, 0.15])
776
+ col_cap.markdown(
777
+ f"**Page {result['page']}** - {result['caption']}"
778
+ )
779
+
780
+ if col_btn.button("Delete", key=f"del_g_{idx}_{result['page']}"):
781
+ del st.session_state.image_results[idx]
782
+ st.rerun()
783
+
784
+ image_data_list = result["image_data"]
785
+ if image_data_list and len(image_data_list) > 0:
786
+ for p_idx in range(len(image_data_list)):
787
+ if p_idx >= len(st.session_state.image_results[idx]["image_data"]):
788
+ break
789
+
790
+ img_data = st.session_state.image_results[idx]["image_data"][p_idx]
791
+ img_unique_key = f"{idx}_{p_idx}_{result['page']}"
792
+
793
+ st.image(img_data["array"], width=300, channels="BGR")
794
+
795
+ if has_extracted_data:
796
+ col_dropdown, col_add_btn, col_remove = st.columns(
797
+ [0.6, 0.2, 0.2]
798
+ )
799
+
800
+ with col_dropdown:
801
+ selected_property = st.selectbox(
802
+ "Select Property",
803
+ options=["-- Select --"] + property_list,
804
+ key=f"prop_select_{img_unique_key}",
805
+ label_visibility="collapsed",
806
+ )
807
+
808
+ with col_add_btn:
809
+ if st.button(" Add", key=f"add_btn_{img_unique_key}"):
810
+ if selected_property and selected_property != "-- Select --":
811
+ filepath = save_single_image_with_property(
812
+ img_data["array"],
813
+ mat_abbr,
814
+ selected_property,
815
+ save_dir="images",
816
+ )
817
+
818
+ st.session_state.saved_image_mapping[
819
+ img_unique_key
820
+ ] = {
821
+ "property": selected_property,
822
+ "caption": result["caption"],
823
+ "filename": os.path.basename(filepath),
824
+ "path": filepath,
825
+ }
826
+
827
+ st.success(
828
+ f" Saved as `{mat_abbr}_{selected_property}.png`"
829
+ )
830
+ st.rerun()
831
+ else:
832
+ st.warning("Please select a property first")
833
+
834
+ with col_remove:
835
+ if st.button("Remove", key=f"del_s_{img_unique_key}"):
836
+ if img_unique_key in st.session_state.saved_image_mapping:
837
+ del st.session_state.saved_image_mapping[img_unique_key]
838
+
839
+ del st.session_state.image_results[idx]["image_data"][p_idx]
840
+ if len(st.session_state.image_results[idx]["image_data"]) == 0:
841
+ del st.session_state.image_results[idx]
842
+ st.rerun()
843
+
844
+ if img_unique_key in st.session_state.saved_image_mapping:
845
+ mapping = st.session_state.saved_image_mapping[img_unique_key]
846
+ st.info(f"Mapped to: **{mapping['property']}**")
847
+ else:
848
+ col_info, col_remove = st.columns([0.8, 0.2])
849
+ with col_info:
850
+ st.caption(
851
+ "Extract material data first to enable property mapping"
852
+ )
853
+ with col_remove:
854
+ if st.button("Remove", key=f"del_s_{img_unique_key}"):
855
+ del st.session_state.image_results[idx]["image_data"][p_idx]
856
+ if len(st.session_state.image_results[idx]["image_data"]) == 0:
857
+ del st.session_state.image_results[idx]
858
+ st.rerun()
859
+
860
+ st.divider()
861
+
862
+ with subtab2:
863
+ st.subheader("Metadata Preview")
864
+ json_data = [
865
+ {
866
+ "caption": r["caption"],
867
+ "page": r["page"],
868
+ "image_count": len(r["image_data"]),
869
+ "images": [img["filename"] for img in r["image_data"]],
870
+ }
871
+ for r in st.session_state.image_results
872
+ ]
873
+
874
+ st.download_button(
875
+ " Download JSON",
876
+ data=json.dumps(json_data, indent=4),
877
+ file_name=f"{paper_id}_metadata.json",
878
+ mime="application/json",
879
+ key="download_json_bottom",
880
+ )
881
+
882
+ st.json(json_data)
883
+ else:
884
+ st.warning("No plots found in PDF")
885
+
886
+
887
+ main()
page_files/categorized/Backend/Pdf_DataExtraction.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ from PIL import Image
4
+ import requests
5
+ import base64
6
+ import json
7
+ import os
8
+ from typing import Dict, Any, Optional
9
+
10
+
11
+
12
+
13
+ # Backend PDF extraction Logic
14
+ API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
15
+ API_URL = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key={API_KEY}"
16
+
17
+ SCHEMA = {
18
+ "type": "OBJECT",
19
+ "properties": {
20
+ "material_name": {"type": "STRING"},
21
+ "material_abbreviation": {"type": "STRING"},
22
+ "mechanical_properties": {
23
+ "type": "ARRAY",
24
+ "items": {
25
+ "type": "OBJECT",
26
+ "properties": {
27
+ "section": {"type": "STRING"},
28
+ "property_name": {"type": "STRING"},
29
+ "value": {"type": "STRING"},
30
+ "unit": {"type": "STRING"},
31
+ "english": {"type": "STRING"},
32
+ "test_condition": {"type": "STRING"},
33
+ "comments": {"type": "STRING"}
34
+ },
35
+ "required": ["section", "property_name", "value", "english", "comments"]
36
+ }
37
+ }
38
+ }
39
+ }
40
+
41
+ # === GEMINI CALL FUNCTION ===
42
+ def call_gemini_from_bytes(pdf_bytes: bytes, filename: str) -> Optional[Dict[str, Any]]:
43
+ """Calls Gemini API with PDF bytes"""
44
+ try:
45
+ encoded_file = base64.b64encode(pdf_bytes).decode("utf-8")
46
+ mime_type = "application/pdf"
47
+ except Exception as e:
48
+ st.error(f"Error encoding PDF: {e}")
49
+ return None
50
+
51
+ prompt = (
52
+ "Extract all experimental data from this research paper. "
53
+ "For each measurement, extract: "
54
+ "- experiment_name, measured_value, unit, uncertainty, method, conditions. "
55
+ "Return as JSON."
56
+ # "You are an expert materials scientist. From the attached PDF, extract the material name, "
57
+ # "abbreviation, and ALL properties across categories (Mechanical, Thermal, Electrical, Physical, "
58
+ # "Optical, Rheological, etc.). Return them as 'mechanical_properties' (a single list). "
59
+ # "For each property, you MUST extract:\n"
60
+ # "- property_name\n- value (or range)\n- unit\n"
61
+ # "- english (converted or alternate units, e.g., psi, °F, inches; write '' if not provided)\n"
62
+ # "- test_condition\n- comments (include any notes, footnotes, standards, remarks; write '' if none)\n"
63
+ # "All fields including english and comments are REQUIRED. Respond ONLY with valid JSON following the schema."
64
+ )
65
+
66
+ payload = {
67
+ "contents": [
68
+ {
69
+ "parts": [
70
+ {"text": prompt},
71
+ {"inlineData": {"mimeType": mime_type, "data": encoded_file}}
72
+ ]
73
+ }
74
+ ],
75
+ "generationConfig": {
76
+ "temperature": 0,
77
+ "responseMimeType": "application/json",
78
+ "responseSchema": SCHEMA
79
+ }
80
+ }
81
+
82
+ try:
83
+ r = requests.post(API_URL, json=payload, timeout=300)
84
+ r.raise_for_status()
85
+ data = r.json()
86
+
87
+ candidates = data.get("candidates", [])
88
+ if not candidates:
89
+ return None
90
+
91
+ parts = candidates[0].get("content", {}).get("parts", [])
92
+ json_text = None
93
+ for p in parts:
94
+ t = p.get("text", "")
95
+ if t.strip().startswith("{"):
96
+ json_text = t
97
+ break
98
+
99
+ return json.loads(json_text) if json_text else None
100
+ except Exception as e:
101
+ st.error(f"Gemini API Error: {e}")
102
+ return None
103
+
104
+
105
+ def convert_to_dataframe(data: Dict[str, Any]) -> pd.DataFrame:
106
+ """Convert extracted JSON to DataFrame"""
107
+ rows = []
108
+ for item in data.get("mechanical_properties", []):
109
+ rows.append({
110
+ "material_name": data.get("material_name", ""),
111
+ "material_abbreviation": data.get("material_abbreviation", ""),
112
+ "section": item.get("section", ""),
113
+ "property_name": item.get("property_name", ""),
114
+ "value": item.get("value", ""),
115
+ "unit": item.get("unit", ""),
116
+ "english": item.get("english", ""),
117
+ "test_condition": item.get("test_condition", ""),
118
+ "comments": item.get("comments", "")
119
+ })
120
+ return pd.DataFrame(rows)
121
+
122
+
123
+
124
+ #using sentence transformers and semantic search techniques
125
+ import sqlite3
126
+ import pandas as pd
127
+ import os
128
+ import requests
129
+ from sentence_transformers import SentenceTransformer
130
+ from sklearn.metrics.pairwise import cosine_similarity
131
+
132
+ # ==========================
133
+ # CONFIGURATION
134
+ # ==========================
135
+ DB_PATH = "output_materials.db"
136
+ EXCEL_PATH = "5.1__actual.xlsx"
137
+ OUTPUT_EXCEL = "5.1__filled.xlsx"
138
+ GEMINI_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
139
+
140
+ GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"
141
+
142
+
143
+ # ==========================
144
+ # GEMINI YES/NO MATCH CHECK
145
+ # ==========================
146
+ def gemini_same_property(excel_prop, db_prop):
147
+ prompt = f"""
148
+ You are an expert materials scientist. Determine if BOTH property names refer
149
+ to the SAME mechanical property.
150
+
151
+ Excel property: "{excel_prop}"
152
+ Database property: "{db_prop}"
153
+
154
+ Rules:
155
+ - Compare meaning, not formatting.
156
+ - Ignore units, values, and numbers.
157
+ - If either refers to conditions, test setup, or non-property info, return NO.
158
+ - Return ONLY YES or NO.
159
+ """
160
+
161
+ payload = {
162
+ "contents": [{"parts": [{"text": prompt}]}]
163
+ }
164
+
165
+ response = requests.post(
166
+ GEMINI_URL,
167
+ params={"key": GEMINI_KEY},
168
+ json=payload,
169
+ timeout=60
170
+ ).json()
171
+
172
+ try:
173
+ ans = response["candidates"][0]["content"]["parts"][0]["text"].strip().upper()
174
+ except:
175
+ return False
176
+
177
+ return ans == "YES"
178
+
179
+
180
+ # ==========================
181
+ # SEMANTIC MATCHER (fallback)
182
+ # ==========================
183
+ embed_model = SentenceTransformer("all-MiniLM-L6-v2")
184
+
185
+ def semantic_match(excel_prop, df_section):
186
+ if df_section.empty:
187
+ return None
188
+
189
+ # compute embeddings
190
+ db_props = df_section["property_name"].tolist()
191
+ db_vecs = embed_model.encode(db_props, convert_to_numpy=True)
192
+ q_vec = embed_model.encode([excel_prop], convert_to_numpy=True)
193
+
194
+ sims = cosine_similarity(q_vec, db_vecs)[0]
195
+
196
+ df_section = df_section.copy()
197
+ df_section["sim"] = sims
198
+ df_section = df_section.sort_values("sim", ascending=False)
199
+
200
+ # Take top-5 candidates for Gemini check
201
+ top5 = df_section.head(5)
202
+
203
+ for _, row in top5.iterrows():
204
+ cand = row["property_name"]
205
+ if gemini_same_property(excel_prop, cand):
206
+ return row
207
+
208
+ return None
209
+
210
+
211
+ # ==========================
212
+ # MAIN PIPELINE
213
+ # ==========================
214
+ conn = sqlite3.connect(DB_PATH)
215
+
216
+ # Get material tables
217
+ tables = pd.read_sql_query(
218
+ "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';",
219
+ conn
220
+ )["name"].tolist()
221
+
222
+ print(f"Detected tables: {tables}")
223
+
224
+ # Load Excel template once
225
+ df_excel_template = pd.read_excel(EXCEL_PATH)
226
+ cols = df_excel_template.columns.tolist()
227
+
228
+ section_col = next((c for c in cols if "section" in c.lower()), None)
229
+ prop_col = next((c for c in cols if "property" in c.lower()), cols[0])
230
+
231
+ print(f"Detected section column: {section_col}")
232
+ print(f"Detected property column: {prop_col}")
233
+
234
+ with pd.ExcelWriter(OUTPUT_EXCEL, engine="openpyxl") as writer:
235
+
236
+ for table in tables:
237
+ print(f"\nProcessing table: {table}")
238
+
239
+ # Load DB table
240
+ df_db = pd.read_sql_query(f"""
241
+ SELECT section, property_name, value, unit, english, comments
242
+ FROM '{table}'
243
+ """, conn)
244
+
245
+ df_excel = df_excel_template.copy()
246
+ df_excel["Matched Property"] = ""
247
+ df_excel["Value"] = ""
248
+ df_excel["Unit"] = ""
249
+ df_excel["English"] = ""
250
+ df_excel["Comments"] = ""
251
+
252
+ # Process each Excel property
253
+ for i, row in df_excel.iterrows():
254
+ excel_prop = str(row[prop_col]).strip()
255
+ excel_section = str(row.get(section_col, "")).strip().lower()
256
+
257
+
258
+ if section_col:
259
+ df_sec = df_db[df_db["section"].str.lower() == excel_section]
260
+ else:
261
+ df_sec = df_db
262
+
263
+ # ==========================
264
+ # 1️ EXACT MATCH
265
+ # ==========================
266
+ exact = df_sec[df_sec["property_name"].str.lower() == excel_prop.lower()]
267
+
268
+ if not exact.empty:
269
+ r = exact.iloc[0]
270
+ df_excel.at[i, "Matched Property"] = r["property_name"]
271
+ df_excel.at[i, "Value"] = r["value"]
272
+ df_excel.at[i, "Unit"] = r["unit"]
273
+ df_excel.at[i, "English"] = r["english"]
274
+ df_excel.at[i, "Comments"] = r["comments"]
275
+ continue # done
276
+
277
+ # ==========================
278
+ # 2️ SEMANTIC + GEMINI MATCH
279
+ # ==========================
280
+ best = semantic_match(excel_prop, df_sec)
281
+
282
+ if best is not None:
283
+ df_excel.at[i, "Matched Property"] = best["property_name"]
284
+ df_excel.at[i, "Value"] = best["value"]
285
+ df_excel.at[i, "Unit"] = best["unit"]
286
+ df_excel.at[i, "English"] = best["english"]
287
+ df_excel.at[i, "Comments"] = best["comments"]
288
+ else:
289
+ df_excel.at[i, "Matched Property"] = ""
290
+
291
+ # Write one sheet per material
292
+ df_excel.to_excel(writer, sheet_name=table[:31], index=False)
293
+
294
+ print(f"\nDONE → Final filled Excel: {OUTPUT_EXCEL}")
295
+ conn.close()
page_files/categorized/Backend/Pdf_ImageExtraction.py ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import math
5
+ import tempfile
6
+ import fitz # PyMuPDF
7
+ import cv2
8
+ import numpy as np
9
+ from PIL import Image
10
+ import streamlit as st
11
+
12
+ # -------------------
13
+ # Config
14
+ # -------------------
15
+ DPI = 300
16
+ OUT_DIR = "outputs"
17
+
18
+ KEEP_ONLY_STRESS_STRAIN = False
19
+
20
+ CAP_RE = re.compile(r"^(Fig\.?\s*\d+|Figure\s*\d+)\b", re.IGNORECASE)
21
+ SS_KW = re.compile(
22
+ r"(stress\s*[-–]?\s*strain|stress|strain|tensile|MPa|GPa|kN|yield|elongation)",
23
+ re.IGNORECASE
24
+ )
25
+
26
+ # -------------------
27
+ # Render helpers
28
+ # -------------------
29
+ def render_page(page, dpi=DPI):
30
+ mat = fitz.Matrix(dpi/72, dpi/72)
31
+ pix = page.get_pixmap(matrix=mat, alpha=False)
32
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
33
+ return img, mat
34
+
35
+ def pdf_to_px_bbox(bbox_pdf, mat):
36
+ x0, y0, x1, y1 = bbox_pdf
37
+ sx, sy = mat.a, mat.d
38
+ return (int(float(x0) * sx), int(float(y0) * sy), int(float(x1) * sx), int(float(y1) * sy))
39
+
40
+ def safe_crop_px(pil_img, box):
41
+ if not isinstance(box, (tuple, list)):
42
+ return None
43
+ if len(box) == 1 and isinstance(box[0], (tuple, list)) and len(box[0]) == 4:
44
+ box = box[0]
45
+ if len(box) != 4:
46
+ return None
47
+
48
+ x0, y0, x1, y1 = box
49
+ if any(isinstance(v, (tuple, list)) for v in (x0, y0, x1, y1)):
50
+ return None
51
+
52
+ try:
53
+ x0 = int(x0)
54
+ y0 = int(y0)
55
+ x1 = int(x1)
56
+ y1 = int(y1)
57
+ except (TypeError, ValueError):
58
+ return None
59
+
60
+ if x1 < x0:
61
+ x0, x1 = x1, x0
62
+ if y1 < y0:
63
+ y0, y1 = y1, y0
64
+
65
+ W, H = pil_img.size
66
+ x0 = max(0, min(W, x0))
67
+ x1 = max(0, min(W, x1))
68
+ y0 = max(0, min(H, y0))
69
+ y1 = max(0, min(H, y1))
70
+ if x1 <= x0 or y1 <= y0:
71
+ return None
72
+ return pil_img.crop((x0, y0, x1, y1))
73
+
74
+ # -------------------
75
+ # Captions
76
+ # -------------------
77
+ def find_caption_blocks(page):
78
+ caps = []
79
+ blocks = page.get_text("blocks")
80
+ for b in blocks:
81
+ x0, y0, x1, y1, text = b[0], b[1], b[2], b[3], b[4]
82
+ t = " ".join(str(text).strip().split())
83
+ if CAP_RE.match(t):
84
+ caps.append({"bbox": (x0, y0, x1, y1), "text": t})
85
+ return caps
86
+
87
+ # -------------------
88
+ # Dedupe: dHash
89
+ # -------------------
90
+ def dhash64(pil_img):
91
+ gray = pil_img.convert("L").resize((9, 8), Image.LANCZOS)
92
+ pixels = list(gray.getdata())
93
+ bits = 0
94
+ for r in range(8):
95
+ for c in range(8):
96
+ left = pixels[r * 9 + c]
97
+ right = pixels[r * 9 + c + 1]
98
+ bits = (bits << 1) | (1 if left > right else 0)
99
+ return bits
100
+
101
+ # -------------------
102
+ # Rejectors
103
+ # -------------------
104
+ def has_colorbar_like_strip(pil_img):
105
+ img = np.array(pil_img)
106
+ if img.ndim != 3:
107
+ return False
108
+ H, W, _ = img.shape
109
+ if W < 250 or H < 150:
110
+ return False
111
+ strip_w = max(18, int(0.07 * W))
112
+ strip = img[:, W-strip_w:W, :]
113
+ q = (strip // 24).reshape(-1, 3)
114
+ uniq = np.unique(q, axis=0)
115
+ return len(uniq) > 70
116
+
117
+ def texture_score(pil_img):
118
+ gray = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2GRAY)
119
+ lap = cv2.Laplacian(gray, cv2.CV_64F)
120
+ return float(lap.var())
121
+
122
+ def is_mostly_legend(pil_img):
123
+ gray = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2GRAY)
124
+ bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
125
+ bw = cv2.medianBlur(bw, 3)
126
+ H, W = bw.shape
127
+ fill = float(np.count_nonzero(bw)) / float(H * W)
128
+ return (0.03 < fill < 0.18) and (min(H, W) < 260)
129
+
130
+ # -------------------
131
+ # Plot detection
132
+ # -------------------
133
+ def detect_axes_lines(pil_img):
134
+ gray = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2GRAY)
135
+ edges = cv2.Canny(gray, 50, 150)
136
+ H, W = gray.shape
137
+ min_len = int(0.28 * min(H, W))
138
+
139
+ lines = cv2.HoughLinesP(
140
+ edges, 1, np.pi/180,
141
+ threshold=90,
142
+ minLineLength=min_len,
143
+ maxLineGap=14
144
+ )
145
+ if lines is None:
146
+ return None, None
147
+
148
+ horizontals, verticals = [], []
149
+ for x1, y1, x2, y2 in lines[:, 0]:
150
+ dx, dy = abs(x2-x1), abs(y2-y1)
151
+ length = math.hypot(dx, dy)
152
+ if dy < 18 and dx > 0.35 * W:
153
+ horizontals.append((length, (x1, y1, x2, y2)))
154
+ if dx < 18 and dy > 0.35 * H:
155
+ verticals.append((length, (x1, y1, x2, y2)))
156
+
157
+ if not horizontals or not verticals:
158
+ return None, None
159
+
160
+ horizontals.sort(key=lambda t: t[0], reverse=True)
161
+ verticals.sort(key=lambda t: t[0], reverse=True)
162
+ return horizontals[0][1], verticals[0][1]
163
+
164
+ def axis_intersection_ok(x_axis, y_axis, W, H):
165
+ xa_y = int(round((x_axis[1] + x_axis[3]) / 2))
166
+ ya_x = int(round((y_axis[0] + y_axis[2]) / 2))
167
+ if not (0 <= xa_y < H and 0 <= ya_x < W):
168
+ return False
169
+ if ya_x > int(0.95 * W) or xa_y < int(0.05 * H):
170
+ return False
171
+ return True
172
+
173
+ def tick_text_presence_score(pil_img, x_axis, y_axis):
174
+ img = np.array(pil_img)
175
+ gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
176
+ bw = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
177
+ bw = cv2.medianBlur(bw, 3)
178
+
179
+ H, W = gray.shape
180
+ xa_y = int(round((x_axis[1] + x_axis[3]) / 2))
181
+ ya_x = int(round((y_axis[0] + y_axis[2]) / 2))
182
+
183
+ y0a = max(0, xa_y - 40)
184
+ y1a = min(H, xa_y + 110)
185
+ x_roi = bw[y0a:y1a, 0:W]
186
+
187
+ x0b = max(0, ya_x - 180)
188
+ x1b = min(W, ya_x + 50)
189
+ y_roi = bw[0:H, x0b:x1b]
190
+
191
+ def count_small_components(mask):
192
+ num, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
193
+ cnt = 0
194
+ for i in range(1, num):
195
+ x, y, w, h, area = stats[i]
196
+ if 4 <= w <= 150 and 4 <= h <= 150 and 20 <= area <= 5000:
197
+ cnt += 1
198
+ return cnt
199
+
200
+ return count_small_components(x_roi) + count_small_components(y_roi)
201
+
202
+ def is_real_plot(pil_img):
203
+ if has_colorbar_like_strip(pil_img):
204
+ return False
205
+ if is_mostly_legend(pil_img):
206
+ return False
207
+
208
+ x_axis, y_axis = detect_axes_lines(pil_img)
209
+ if x_axis is None or y_axis is None:
210
+ return False
211
+
212
+ arr = np.array(pil_img)
213
+ H, W = arr.shape[0], arr.shape[1]
214
+ if not axis_intersection_ok(x_axis, y_axis, W, H):
215
+ return False
216
+
217
+ if texture_score(pil_img) > 2200:
218
+ return False
219
+
220
+ score = tick_text_presence_score(pil_img, x_axis, y_axis)
221
+ return score >= 18
222
+
223
+ # -------------------
224
+ # Candidate boxes in a region
225
+ # -------------------
226
+ def connected_components_boxes(pil_img):
227
+ img_bgr = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
228
+ gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
229
+ mask = (gray < 245).astype(np.uint8) * 255
230
+ mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((7, 7), np.uint8), iterations=2)
231
+ num, _, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
232
+
233
+ boxes = []
234
+ for i in range(1, num):
235
+ x, y, w, h, area = stats[i]
236
+ boxes.append((int(area), (int(x), int(y), int(x + w), int(y + h))))
237
+ boxes.sort(key=lambda t: t[0], reverse=True)
238
+ return boxes
239
+
240
+ def expand_box(box, W, H, left=0.10, right=0.06, top=0.06, bottom=0.18):
241
+ x0, y0, x1, y1 = box
242
+ bw = x1 - x0
243
+ bh = y1 - y0
244
+ ex0 = max(0, int(x0 - left * bw))
245
+ ex1 = min(W, int(x1 + right * bw))
246
+ ey0 = max(0, int(y0 - top * bh))
247
+ ey1 = min(H, int(y1 + bottom * bh))
248
+ return (ex0, ey0, ex1, ey1)
249
+
250
+ # -------------------
251
+ # Crop plot from caption
252
+ # -------------------
253
+ def crop_plot_from_caption(page_img, cap_bbox_pdf, mat):
254
+ cap_px = pdf_to_px_bbox(cap_bbox_pdf, mat)
255
+ cap_y0 = cap_px[1]
256
+ cap_y1 = cap_px[3]
257
+
258
+ W, H = page_img.size
259
+ search_top = max(0, cap_y0 - int(0.95 * H))
260
+ search_bot = min(H, cap_y1 + int(0.20 * H))
261
+ region = safe_crop_px(page_img, (0, search_top, W, search_bot))
262
+ if region is None:
263
+ return None
264
+
265
+ comps = connected_components_boxes(region)
266
+ best = None
267
+ best_area = -1
268
+
269
+ for area, box in comps[:35]:
270
+ x0, y0, x1, y1 = box
271
+ bw = x1 - x0
272
+ bh = y1 - y0
273
+ if bw < 220 or bh < 180:
274
+ continue
275
+
276
+ exp = expand_box(box, region.size[0], region.size[1])
277
+ cand = safe_crop_px(region, exp)
278
+ if cand is None:
279
+ continue
280
+
281
+ if not is_real_plot(cand):
282
+ continue
283
+
284
+ if area > best_area:
285
+ best_area = area
286
+ best = cand
287
+
288
+ return best
289
+
290
+ # -------------------
291
+ # Streamlit UI
292
+ # -------------------
293
+ def run_extraction(pdf_path, paper_id="uploaded_paper"):
294
+ out_paper = os.path.join(OUT_DIR, paper_id)
295
+ out_imgs = os.path.join(out_paper, "plots_with_axes")
296
+ os.makedirs(out_imgs, exist_ok=True)
297
+
298
+ doc = fitz.open(pdf_path)
299
+ results = []
300
+ seen = set()
301
+ saved = 0
302
+
303
+ for p in range(len(doc)):
304
+ page = doc[p]
305
+ caps = find_caption_blocks(page)
306
+ if not caps:
307
+ continue
308
+
309
+ page_img, mat = render_page(page, dpi=DPI)
310
+
311
+ for cap in caps:
312
+ cap_text = cap["text"]
313
+
314
+ if KEEP_ONLY_STRESS_STRAIN and not SS_KW.search(cap_text):
315
+ continue
316
+
317
+ fig = crop_plot_from_caption(page_img, cap["bbox"], mat)
318
+ if fig is None:
319
+ continue
320
+
321
+ if fig.size[0] > 8 and fig.size[1] > 8:
322
+ fig = fig.crop((2, 2, fig.size[0]-2, fig.size[1]-2))
323
+
324
+ try:
325
+ h = dhash64(fig)
326
+ except Exception:
327
+ continue
328
+
329
+ if h in seen:
330
+ continue
331
+ seen.add(h)
332
+
333
+ img_name = f"p{p+1:02d}_{saved:04d}.png"
334
+ img_path = os.path.join(out_imgs, img_name)
335
+ fig.save(img_path)
336
+
337
+ results.append({
338
+ "page": p + 1,
339
+ "caption": cap_text,
340
+ "image": img_path
341
+ })
342
+ saved += 1
343
+
344
+ out_json = os.path.join(out_paper, "plots_with_axes.json")
345
+ with open(out_json, "w", encoding="utf-8") as f:
346
+ json.dump(results, f, indent=2, ensure_ascii=False)
347
+
348
+ return results, out_json
349
+
350
+ def main():
351
+ st.set_page_config(page_title="Research Paper Plot Extractor", layout="wide")
352
+ st.title(" Plot Extractor (Upload PDF)")
353
+
354
+ uploaded = st.file_uploader("Upload a research paper PDF", type=["pdf"])
355
+ if not uploaded:
356
+ st.info("Upload a PDF to extract plots.")
357
+ return
358
+
359
+ paper_id = os.path.splitext(uploaded.name)[0].replace(" ", "_")
360
+
361
+ with tempfile.TemporaryDirectory() as tmpdir:
362
+ pdf_path = os.path.join(tmpdir, uploaded.name)
363
+ with open(pdf_path, "wb") as f:
364
+ f.write(uploaded.read())
365
+
366
+ with st.spinner("Extracting plots..."):
367
+ results, out_json = run_extraction(pdf_path, paper_id=paper_id)
368
+
369
+ st.success(f"Extracted {len(results)} plots.")
370
+
371
+ # Show images + captions
372
+ for r in results:
373
+ st.markdown(f"**Page {r['page']}** — {r['caption']}")
374
+ st.image(r["image"], use_container_width=True)
375
+ st.divider()
376
+
377
+ # JSON viewer + download
378
+ st.subheader("JSON Output")
379
+ st.json(results)
380
+
381
+ with open(out_json, "rb") as f:
382
+ st.download_button(
383
+ "Download JSON",
384
+ data=f,
385
+ file_name=os.path.basename(out_json),
386
+ mime="application/json"
387
+ )
388
+
389
+ if __name__ == "__main__":
390
+ main()
page_files/categorized/Backend/upload_backend.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import re
3
+ import json
4
+ import zipfile
5
+ from io import BytesIO
6
+ from typing import Dict, Any, Optional
7
+ from collections import defaultdict
8
+
9
+ import cv2
10
+ import fitz # PyMuPDF
11
+ import numpy as np
12
+ import pandas as pd
13
+ import requests
14
+ import streamlit as st
15
+ import base64
16
+
17
+ API_KEY = os.getenv("GEMINI_API_KEY") or os.getenv("GOOGLE_API_KEY")
18
+ API_URL = (
19
+ "https://generativelanguage.googleapis.com/v1beta/"
20
+ "models/gemini-2.5-flash-preview-09-2025:generateContent?key="
21
+ f"{API_KEY}"
22
+ if API_KEY
23
+ else None
24
+ )
25
+
26
+ SCHEMA = {
27
+ "type": "OBJECT",
28
+ "properties": {
29
+ "material_name": {"type": "STRING"},
30
+ "material_abbreviation": {"type": "STRING"},
31
+ "trade_grade": {
32
+ "type": "STRING",
33
+ "description": "Commercial or trade grade name of the material; '' if not provided",
34
+ },
35
+ "manufacturer": {
36
+ "type": "STRING",
37
+ "description": "Company or organization producing the material; '' if not provided",
38
+ },
39
+ "mechanical_properties": {
40
+ "type": "ARRAY",
41
+ "items": {
42
+ "type": "OBJECT",
43
+ "properties": {
44
+ "section": {"type": "STRING"},
45
+ "property_name": {"type": "STRING"},
46
+ "value": {"type": "STRING"},
47
+ "unit": {"type": "STRING"},
48
+ "english": {"type": "STRING"},
49
+ "test_condition": {"type": "STRING"},
50
+ "comments": {"type": "STRING"},
51
+ },
52
+ "required": [
53
+ "section",
54
+ "property_name",
55
+ "value",
56
+ "english",
57
+ "comments",
58
+ ],
59
+ },
60
+ },
61
+ },
62
+ }
63
+
64
+ DPI = 300
65
+ CAP_RE = re.compile(r"^(Fig\.?\s*\d+|Figure\s*\d+)\b", re.IGNORECASE)
66
+
67
+
68
+ def make_abbreviation(name: str) -> str:
69
+ if not name:
70
+ return "UNKNOWN"
71
+ words = name.split()
72
+ abbr = "".join(w[0] for w in words if w and w[0].isalpha()).upper()
73
+ return abbr or name[:6].upper()
74
+
75
+
76
+ def call_gemini_from_bytes(pdf_bytes: bytes, filename: str) -> Optional[Dict[str, Any]]:
77
+ if not API_KEY or not API_URL:
78
+ st.error("Missing Gemini API key. Set GEMINI_API_KEY in environment variables.")
79
+ return None
80
+
81
+ try:
82
+ encoded_file = base64.b64encode(pdf_bytes).decode("utf-8")
83
+ mime_type = "application/pdf"
84
+ except Exception as exc:
85
+ st.error(f"Error encoding PDF: {exc}")
86
+ return None
87
+
88
+ prompt = (
89
+ "You are an expert materials scientist. From the attached PDF, extract:\n"
90
+ "- material_name (generic material, e.g., isotactic polypropylene)\n"
91
+ "- material_abbreviation\n"
92
+ "- trade_grade (commercial or trade name; write '' if not provided)\n"
93
+ "- manufacturer (company or organization producing the material; write '' if not provided)\n\n"
94
+ "Extract ALL properties across categories (Mechanical, Thermal, Electrical, Physical, "
95
+ "Optical, Rheological, etc.) and return them as 'mechanical_properties' (a single list).\n\n"
96
+ "For each property, you MUST extract:\n"
97
+ "- property_name\n"
98
+ "- value (or range)\n"
99
+ "- unit\n"
100
+ "- english (converted or alternate units, e.g., psi, °F, inches; write '' if not provided)\n"
101
+ "- test_condition\n"
102
+ "- comments (include any notes, footnotes, standards, remarks; write '' if none)\n\n"
103
+ "All fields including english and comments are REQUIRED.\n"
104
+ "Respond ONLY with valid JSON following the schema."
105
+ )
106
+
107
+ payload = {
108
+ "contents": [
109
+ {
110
+ "parts": [
111
+ {"text": prompt},
112
+ {"inlineData": {"mimeType": mime_type, "data": encoded_file}},
113
+ ]
114
+ }
115
+ ],
116
+ "generationConfig": {
117
+ "temperature": 0,
118
+ "responseMimeType": "application/json",
119
+ "responseSchema": SCHEMA,
120
+ },
121
+ }
122
+
123
+ try:
124
+ response = requests.post(API_URL, json=payload, timeout=300)
125
+ response.raise_for_status()
126
+ data = response.json()
127
+
128
+ candidates = data.get("candidates", [])
129
+ if not candidates:
130
+ return None
131
+
132
+ parts = candidates[0].get("content", {}).get("parts", [])
133
+ json_text = None
134
+ for part in parts:
135
+ text = part.get("text", "")
136
+ if text.strip().startswith("{"):
137
+ json_text = text
138
+ break
139
+
140
+ return json.loads(json_text) if json_text else None
141
+ except Exception as exc:
142
+ st.error(f"Gemini API Error: {exc}")
143
+ return None
144
+
145
+
146
+ def convert_to_dataframe(data: Dict[str, Any]) -> pd.DataFrame:
147
+ mat_name = data.get("material_name", "") or ""
148
+ mat_abbr = data.get("material_abbreviation", "") or ""
149
+ trade_grade = data.get("trade_grade", "") or ""
150
+ manufacturer = data.get("manufacturer", "") or ""
151
+
152
+ if not mat_abbr:
153
+ mat_abbr = make_abbreviation(mat_name)
154
+
155
+ rows = []
156
+ for item in data.get("mechanical_properties", []):
157
+ rows.append(
158
+ {
159
+ "material_name": mat_name,
160
+ "material_abbreviation": mat_abbr,
161
+ "trade_grade": trade_grade,
162
+ "manufacturer": manufacturer,
163
+ "section": item.get("section", "") or "Mechanical",
164
+ "property_name": item.get("property_name", "") or "Unknown property",
165
+ "value": item.get("value", "") or "N/A",
166
+ "unit": item.get("unit", "") or "",
167
+ "english": item.get("english", "") or "",
168
+ "test_condition": item.get("test_condition", "") or "",
169
+ "comments": item.get("comments", "") or "",
170
+ }
171
+ )
172
+ return pd.DataFrame(rows)
173
+
174
+
175
+ def get_page_image(page):
176
+ pix = page.get_pixmap(matrix=fitz.Matrix(DPI / 72, DPI / 72))
177
+ img = np.frombuffer(pix.samples, dtype=np.uint8).reshape(pix.h, pix.w, 3)
178
+ return cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
179
+
180
+
181
+ def is_valid_plot_geometry(binary_crop):
182
+ height, width = binary_crop.shape
183
+ if height < 100 or width < 100:
184
+ return False
185
+ ink_density = cv2.countNonZero(binary_crop) / (width * height)
186
+ if ink_density > 0.35:
187
+ return False
188
+ h_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (width // 4, 1))
189
+ v_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, height // 4))
190
+ has_h = cv2.countNonZero(cv2.erode(binary_crop, h_kernel, iterations=1)) > 0
191
+ has_v = cv2.countNonZero(cv2.erode(binary_crop, v_kernel, iterations=1)) > 0
192
+ return has_h or has_v
193
+
194
+
195
+ def merge_boxes(rects):
196
+ if not rects:
197
+ return []
198
+ rects = sorted(rects, key=lambda r: r[2] * r[3], reverse=True)
199
+ merged = []
200
+ for rect in rects:
201
+ rx, ry, rw, rh = rect
202
+ if not any(
203
+ rx >= m[0] - 15
204
+ and ry >= m[1] - 15
205
+ and rx + rw <= m[0] + m[2] + 15
206
+ and ry + rh <= m[1] + m[3] + 15
207
+ for m in merged
208
+ ):
209
+ merged.append(rect)
210
+ return merged
211
+
212
+
213
+ def extract_images(pdf_doc):
214
+ grouped_data = defaultdict(lambda: {"page": 0, "image_data": []})
215
+ padding = 30
216
+
217
+ for page_num, page in enumerate(pdf_doc, start=1):
218
+ img_bgr = get_page_image(page)
219
+ gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
220
+ _, binary = cv2.threshold(gray, 225, 255, cv2.THRESH_BINARY_INV)
221
+ kernel = np.ones((10, 10), np.uint8)
222
+ dilated = cv2.dilate(binary, kernel, iterations=1)
223
+ contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
224
+
225
+ candidates = []
226
+ page_h, page_w = gray.shape
227
+ for cnt in contours:
228
+ x, y, w, h = cv2.boundingRect(cnt)
229
+ if 0.03 < (w * h) / (page_w * page_h) < 0.8:
230
+ if is_valid_plot_geometry(binary[y : y + h, x : x + w]):
231
+ candidates.append((x, y, w, h))
232
+
233
+ final_rects = merge_boxes(candidates)
234
+ blocks = page.get_text("blocks")
235
+
236
+ for (cx, cy, cw, ch) in final_rects:
237
+ best_caption = f"Figure on Page {page_num} (Unlabeled)"
238
+ min_dist = float("inf")
239
+ for block in blocks:
240
+ text = block[4].strip()
241
+ if CAP_RE.match(text):
242
+ cap_y = block[1] * (DPI / 72)
243
+ dist = cap_y - (cy + ch)
244
+ if 0 < dist < (page_h * 0.3) and dist < min_dist:
245
+ best_caption = text.replace("\n", " ")
246
+ min_dist = dist
247
+
248
+ x1, y1 = max(0, cx - padding), max(0, cy - padding)
249
+ x2, y2 = min(page_w, cx + cw + padding), min(page_h, cy + ch + padding)
250
+ crop = img_bgr[int(y1) : int(y2), int(x1) : int(x2)]
251
+
252
+ _, buffer = cv2.imencode(".png", crop)
253
+ img_bytes = buffer.tobytes()
254
+ fname = f"pg{page_num}_{cx}_{cy}.png"
255
+
256
+ grouped_data[best_caption]["page"] = page_num
257
+ grouped_data[best_caption]["image_data"].append(
258
+ {"filename": fname, "bytes": img_bytes, "array": crop}
259
+ )
260
+
261
+ return [
262
+ {"caption": key, "page": value["page"], "image_data": value["image_data"]}
263
+ for key, value in grouped_data.items()
264
+ ]
265
+
266
+
267
+ def create_zip(results, include_json=True):
268
+ buf = BytesIO()
269
+ with zipfile.ZipFile(buf, "w") as zf:
270
+ if include_json:
271
+ json_data = [
272
+ {"caption": item["caption"], "page": item["page"], "image_count": len(item["image_data"])}
273
+ for item in results
274
+ ]
275
+ zf.writestr("plot_data.json", json.dumps(json_data, indent=4))
276
+
277
+ for item in results:
278
+ for img_data in item["image_data"]:
279
+ zf.writestr(img_data["filename"], img_data["bytes"])
280
+
281
+ buf.seek(0)
282
+ return buf.getvalue()
283
+
284
+
285
+ def match_caption_to_property(caption: str, property_name: str) -> bool:
286
+ caption_lower = caption.lower()
287
+ prop_lower = property_name.lower()
288
+
289
+ if prop_lower in caption_lower:
290
+ return True
291
+
292
+ keyword_map = {
293
+ "tensile modulus": ["tensile", "modulus", "young", "elastic"],
294
+ "tensile strength": ["tensile", "strength", "ultimate"],
295
+ "elongation at break": ["elongation", "strain", "break"],
296
+ "glass transition temperature": ["glass transition", "tg", "transition"],
297
+ "melting temperature": ["melting", "tm", "melt"],
298
+ "density": ["density", "specific gravity"],
299
+ "impact strength": ["impact", "izod", "charpy"],
300
+ "flexural modulus": ["flexural", "bending", "flex"],
301
+ "stress": ["stress", "strain"],
302
+ "thermal": ["thermal", "temperature", "heat"],
303
+ "crystallinity": ["crystallinity", "crystalline", "xrd"],
304
+ }
305
+
306
+ for prop_key, keywords in keyword_map.items():
307
+ if prop_key in prop_lower and any(kw in caption_lower for kw in keywords):
308
+ return True
309
+
310
+ prop_words = set(prop_lower.replace("(", "").replace(")", "").split())
311
+ caption_words = set(caption_lower.replace("(", "").replace(")", "").split())
312
+
313
+ common_words = prop_words & caption_words
314
+ significant_words = common_words - {"the", "of", "at", "in", "a", "an"}
315
+
316
+ return len(significant_words) >= 2
317
+
318
+
319
+ def save_matched_images(df: pd.DataFrame, image_results: list, save_dir: str = "images"):
320
+ os.makedirs(save_dir, exist_ok=True)
321
+ saved_images = []
322
+
323
+ if df.empty:
324
+ return saved_images
325
+
326
+ mat_abbr = df.iloc[0]["material_abbreviation"]
327
+ properties = df["property_name"].unique()
328
+ matched_properties = set()
329
+
330
+ for img_result in image_results:
331
+ caption = img_result["caption"]
332
+
333
+ for prop in properties:
334
+ if prop in matched_properties:
335
+ continue
336
+ if match_caption_to_property(caption, prop):
337
+ if img_result["image_data"]:
338
+ first_img = img_result["image_data"][0]
339
+ filename = f"{mat_abbr}_{prop}.png"
340
+ filepath = os.path.join(save_dir, filename)
341
+ cv2.imwrite(filepath, first_img["array"])
342
+ saved_images.append({"property": prop, "caption": caption, "path": filepath})
343
+ matched_properties.add(prop)
344
+ break
345
+
346
+ return saved_images
347
+
348
+
349
+ def save_single_image_with_property(
350
+ img_array, mat_abbr: str, property_name: str, save_dir: str = "images"
351
+ ) -> str:
352
+ os.makedirs(save_dir, exist_ok=True)
353
+ filename = f"{mat_abbr}_{property_name}.png"
354
+ filepath = os.path.join(save_dir, filename)
355
+ cv2.imwrite(filepath, img_array)
356
+ return filepath