Harshark/GHS-Classifier
0
1import streamlit as st2import pandas as pd3import requests4import re5from duckduckgo_search import DDGS6 7st.set_page_config(page_title="GHS Pro Classifier", layout="wide")8 9# ==========================================10# 1. OFFICIAL ECHA HARMONISED CACHE11# ==========================================12ECHA_CACHE = {13 "7732-18-5": {"name": "Water", "h_statements": []},14 "111-90-0": {"name": "Diethylene glycol monoethyl ether", "h_statements": []}, 15 "68439-50-9": {"name": "Alcohols, C12-14, ethoxylated", 16 "h_statements": ["H318", "H302", "H412"]},17 "1300-72-7": {"name": "Sodium xylenesulfonate", "h_statements": ["H319"]},18 "65-85-0": {"name": "Benzoic acid", "h_statements": ["H315", "H318", "H372"]},19 "78-70-6": {"name": "Linalool", "h_statements": ["H315", "H319", "H317"]},20 "108-88-3": {"name": "Toluene", 21 "h_statements": ["H225", "H304", "H315", "H336", "H361d", "H373"]},22 "64742-89-8": {"name": "Solvent naphtha (petroleum)", "h_statements": ["H304"]},23 "68081-81-2": {"name": "Sodium alkyl benzenesulfonate", 24 "h_statements": ["H302", "H315", "H318"]},25 "67-56-1": {"name": "Methanol", 26 "h_statements": ["H225", "H301", "H311", "H331", "H370"]}, 27 "8028-48-6": {"name": "Orange Oil", 28 "h_statements": ["H226", "H304", "H315", "H317", "H410"]},29 "1310-73-2": {"name": "Sodium Hydroxide", "h_statements": ["H314"]},30 "50-00-0": {"name": "Formaldehyde", 31 "h_statements": ["H301", "H311", "H331", "H314", "H317", "H341", "H350"]}32}33 34# ==========================================35# 2. COMBINED FETCHING ENGINE36# ==========================================37@st.cache_data(show_spinner=False)38def fetch_hazard_data(cas_number):39 cas_number = str(cas_number).strip()40 if not cas_number: 41 return [], "Empty", "None"42 43 all_h_codes = set()44 sources_used = []45 name = "Unknown Substance"46 47 try:48 url_cid = (49 "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/name/" 50 + cas_number + "/cids/JSON"51 )52 res = requests.get(url_cid, timeout=5)53 if res.status_code == 200:54 cid = res.json()['IdentifierList']['CID'][0]55 url_name = (56 "https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/" 57 + str(cid) + "/property/Title/JSON"58 )59 name = requests.get(url_name).json()['PropertyTable']['Properties'][0]['Title']60 61 url_ghs = (62 "https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/compound/" 63 + str(cid) + "/JSON?heading=GHS+Classification"64 )65 ghs_res = requests.get(url_ghs, timeout=5)66 codes = set(re.findall(r'H[234]\d{2}[a-zA-Z]*', ghs_res.text))67 if codes:68 all_h_codes.update(codes)69 sources_used.append("PubChem")70 except Exception: 71 pass72 73 try:74 with DDGS() as ddgs:75 q = '"' + cas_number + '" "Safety Data Sheet" hazard statements H3'76 results = ddgs.text(q, max_results=3)77 if results:78 combined_text = " ".join([r['body'] for r in results])79 codes = set(re.findall(r'H[234]\d{2}[a-zA-Z]*', combined_text))80 if codes:81 all_h_codes.update(codes)82 sources_used.append("Web")83 if name == "Unknown Substance":84 name = "Unindexed Substance"85 except Exception: 86 pass87 88 if cas_number in ECHA_CACHE:89 all_h_codes.update(ECHA_CACHE[cas_number]['h_statements'])90 if "ECHA Cache" not in sources_used:91 sources_used.append("ECHA Cache")92 if name in ["Unknown Substance", "Unindexed Substance"]:93 name = ECHA_CACHE[cas_number]['name']94 95 if not all_h_codes:96 return [], name, "Not Found"97 98 return list(all_h_codes), name, " + ".join(sources_used)99 100# ==========================================101# 3. FULL GHS MATH ENGINE102# ==========================================103def run_classification_math(df, region, flash_point=None):104 report = {105 "Flammable Liquids": "Not Classified",106 "Acute Toxicity (Oral)": "Not Classified", 107 "Acute Toxicity (Dermal)": "Not Classified",108 "Acute Toxicity (Inhalation)": "Not Classified",109 "Serious Eye Damage/Irritation": "Not Classified", 110 "Skin Corrosion/Irritation": "Not Classified",111 "Skin Sensitization": "Not Classified", 112 "Mutagenicity": "Not Classified", 113 "Carcinogenicity": "Not Classified", 114 "Reproductive Toxicity": "Not Classified", 115 "STOT SE": "Not Classified", 116 "STOT RE": "Not Classified", 117 "Aspiration Hazard": "Not Classified",118 "Aquatic Toxicity": "Not Classified", 119 "Signal Word": "None"120 }121 122 is_us = "United States" in region123 124 def sum_h(codes_to_find):125 total = 0126 for _, row in df.iterrows():127 code_str = str(row.get('H-Codes (Comma separated)', ''))128 if any(c in code_str for c in codes_to_find):129 try: 130 total += float(row.get('Concentration (%)', 0))131 except ValueError: 132 pass133 return total134 135 # --- 0. PHYSICAL HAZARDS ---136 if flash_point is not None:137 if flash_point < 23.0: 138 report["Flammable Liquids"] = "Category 2 (Danger - H225)"139 elif 23.0 <= flash_point <= 60.0: 140 report["Flammable Liquids"] = "Category 3 (Warning - H226)"141 elif 60.0 < flash_point <= 93.0 and is_us: 142 report["Flammable Liquids"] = "Category 4 (Warning - H227)"143 else:144 if sum_h(['H224', 'H225', 'H226', 'H227']) >= 10.0:145 if sum_h(['H224']) > 0: 146 report["Flammable Liquids"] = "Category 1 (Danger - H224) *Est."147 elif sum_h(['H225']) > 0: 148 report["Flammable Liquids"] = "Category 2 (Danger - H225) *Est."149 elif sum_h(['H226']) > 0: 150 report["Flammable Liquids"] = "Category 3 (Warning - H226) *Est."151 152 # --- 1. ACUTE TOXICITY ---153 inv_o, inv_d, inv_i = 0.0, 0.0, 0.0154 for _, row in df.iterrows():155 c = float(row.get('Concentration (%)', 0))156 codes = str(row.get('H-Codes (Comma separated)', ''))157 if c > 0:158 if 'H300' in codes: inv_o += c / 0.5159 elif 'H301' in codes: inv_o += c / 100.0160 elif 'H302' in codes: inv_o += c / 500.0161 162 if 'H310' in codes: inv_d += c / 5.0163 elif 'H311' in codes: inv_d += c / 300.0164 elif 'H312' in codes: inv_d += c / 1100.0165 166 if 'H330' in codes: inv_i += c / 0.5167 elif 'H331' in codes: inv_i += c / 3.0168 elif 'H332' in codes: inv_i += c / 11.0169 170 if inv_o > 0:171 ate = 100.0 / inv_o172 if ate <= 5: report["Acute Toxicity (Oral)"] = "Cat 1 (Danger - H300)"173 elif ate <= 50: report["Acute Toxicity (Oral)"] = "Cat 2 (Danger - H300)"174 elif ate <= 300: report["Acute Toxicity (Oral)"] = "Cat 3 (Danger - H301)"175 elif ate <= 2000: report["Acute Toxicity (Oral)"] = "Cat 4 (Warning - H302)"176 177 if inv_d > 0:178 ate = 100.0 / inv_d179 if ate <= 50: report["Acute Toxicity (Dermal)"] = "Cat 1 (Danger - H310)"180 elif ate <= 200: report["Acute Toxicity (Dermal)"] = "Cat 2 (Danger - H310)"181 elif ate <= 1000: report["Acute Toxicity (Dermal)"] = "Cat 3 (Danger - H311)"182 elif ate <= 2000: report["Acute Toxicity (Dermal)"] = "Cat 4 (Warning - H312)"183 184 if inv_i > 0:185 ate = 100.0 / inv_i186 if ate <= 0.5: report["Acute Toxicity (Inhalation)"] = "Cat 1 (Danger - H330)"187 elif ate <= 2.0: report["Acute Toxicity (Inhalation)"] = "Cat 2 (Danger - H330)"188 elif ate <= 10.0: report["Acute Toxicity (Inhalation)"] = "Cat 3 (Danger - H331)"189 elif ate <= 20.0: report["Acute Toxicity (Inhalation)"] = "Cat 4 (Warning - H332)"190 191 # --- 2. ASPIRATION ---192 if sum_h(['H304']) >= 10.0:193 report["Aspiration Hazard"] = "Category 1 (Danger - H304)"194 195 # --- 3. SKIN & EYE ---196 eye_dam = sum_h(['H318', 'H314'])197 eye_irr = sum_h(['H319'])198 if eye_dam >= 3.0: 199 report["Serious Eye Damage/Irritation"] = "Category 1 (Danger - H318)"200 elif eye_dam >= 1.0 or (eye_dam * 10 + eye_irr) >= 10.0: 201 report["Serious Eye Damage/Irritation"] = "Category 2 (Warning - H319)"202 203 skin_corr = sum_h(['H314'])204 skin_irr = sum_h(['H315'])205 if skin_corr >= 5.0: 206 report["Skin Corrosion/Irritation"] = "Category 1 (Danger - H314)"207 elif skin_corr >= 1.0 or (skin_corr * 10 + skin_irr) >= 10.0: 208 report["Skin Corrosion/Irritation"] = "Category 2 (Warning - H315)"209 210 # --- 4. STOT RE & SE ---211 stot_re_1 = sum_h(['H372'])212 stot_re_2 = sum_h(['H373'])213 if stot_re_1 >= 10.0: 214 report["STOT RE"] = "Category 1 (Danger - H372)"215 elif (1.0 <= stot_re_1 < 10.0) or stot_re_2 >= 10.0: 216 report["STOT RE"] = "Category 2 (Warning - H373)"217 218 stot_se_1 = sum_h(['H370'])219 stot_se_2 = sum_h(['H371'])220 221 meth_conc = 0222 if "67-56-1" in df['CAS Number'].values:223 meth_row = df[df['CAS Number'] == "67-56-1"]224 meth_conc = float(meth_row['Concentration (%)'].iloc[0])225 226 if stot_se_1 >= 10.0: 227 report["STOT SE"] = "Category 1 (Danger - H370)"228 elif stot_se_1 > 0 and 0 < meth_conc < 3.0:229 pass 230 elif (1.0 <= stot_se_1 < 10.0) or stot_se_2 >= 10.0: 231 report["STOT SE"] = "Category 2 (Warning - H371)"232 233 # --- 5. CMR & SENSITIZATION ---234 sens_limit = 0.1 if is_us else 1.0235 if sum_h(['H317']) >= sens_limit: 236 report["Skin Sensitization"] = "Category 1 (Warning - H317)"237 238 if sum_h(['H340']) >= 0.1: 239 report["Mutagenicity"] = "Category 1 (Danger - H340)"240 elif sum_h(['H341']) >= 1.0: 241 report["Mutagenicity"] = "Category 2 (Warning - H341)"242 243 carc2_limit = 0.1 if is_us else 1.0244 if sum_h(['H350']) >= 0.1: 245 report["Carcinogenicity"] = "Category 1 (Danger - H350)"246 elif sum_h(['H351']) >= carc2_limit: 247 report["Carcinogenicity"] = "Category 2 (Warning - H351)"248 249 repr1_limit = 0.1 if is_us else 0.3250 repr2_limit = 0.1 if is_us else 3.0251 if sum_h(['H360']) >= repr1_limit: 252 report["Reproductive Toxicity"] = "Category 1 (Danger - H360)"253 elif sum_h(['H361', 'H361d', 'H361f']) >= repr2_limit: 254 report["Reproductive Toxicity"] = "Category 2 (Warning - H361)"255 256 # --- 6. ENVIRONMENTAL ---257 if is_us:258 report["Aquatic Toxicity"] = "Not Mandatory under US OSHA"259 else:260 aq_ac = sum_h(['H400'])261 aq_c1 = sum_h(['H410'])262 aq_c2 = sum_h(['H411'])263 264 if aq_ac >= 25.0: 265 report["Aquatic Toxicity"] = "Acute 1 (Warning - H400)"266 if aq_c1 >= 25.0: 267 report["Aquatic Toxicity"] = "Chronic 1 (Warning - H410)"268 elif (aq_c1 * 10 + aq_c2) >= 25.0: 269 report["Aquatic Toxicity"] = "Chronic 2 (H411)"270 271 if "Danger" in str(report): 272 report["Signal Word"] = "Danger"273 elif "Warning" in str(report): 274 report["Signal Word"] = "Warning"275 276 return report277 278# ==========================================279# 4. STREAMLIT UI 280# ==========================================281st.title("๐ Ultimate GHS Classifier Master")282 283regions = ["European Union (CLP)", "United States (OSHA HCS)", "China (GB 30000)"]284region = st.selectbox("Regulatory Region", regions)285st.divider()286 287st.subheader("Step 1: Upload or Enter Formula")288if "raw_df" not in st.session_state:289 st.session_state.raw_df = pd.DataFrame([{"CAS": "", "Concentration (%)": 0.0}])290 291input_df = st.data_editor(292 st.session_state.raw_df, 293 num_rows="dynamic", 294 use_container_width=True295)296 297if 'CAS' in input_df.columns:298 input_df = input_df.rename(columns={'CAS': 'CAS Number'})299 300if st.button("๐ Fetch Hazards"):301 with st.spinner("Scraping PubChem, Web, and ECHA Cache..."):302 fetched_data = []303 for _, row in input_df.iterrows():304 cas = str(row.get('CAS Number', '')).strip()305 conc = float(row.get('Concentration (%)', 0))306 if cas and conc > 0:307 h_codes, name, source = fetch_hazard_data(cas)308 fetched_data.append({309 "CAS Number": cas, 310 "Name": name, 311 "Concentration (%)": conc, 312 "H-Codes (Comma separated)": ", ".join(h_codes), 313 "Hazard Classes (Manual Entry)": "", # <-- NEW COLUMN ADDED HERE314 "Source": source315 })316 st.session_state.review_df = pd.DataFrame(fetched_data)317 st.success("Hazards Fetched! Proceed to Step 2.")318 319st.divider()320 321if "review_df" in st.session_state:322 st.subheader("Step 2: Review Hazards & Calculate")323 st.error("๐จ CRITICAL: Delete 'junk' H-codes from the table before calculating!")324 325 edited_df = st.data_editor(st.session_state.review_df, use_container_width=True)326 327 st.markdown("### ๐ก๏ธ Physical Hazards (Optional)")328 fp_input = st.number_input("Known Mixture Flash Point (ยฐC) (Blank = Est)", value=None)329 330 if st.button("โ๏ธ Calculate Regional Classification", type="primary"):331 report = run_classification_math(edited_df, region, flash_point=fp_input)332 st.divider()333 st.header(f"๐ก๏ธ Final Mixture Classification ({region})")334 st.markdown(f"### Signal Word: **{report['Signal Word']}**")335 336 col1, col2, col3 = st.columns(3)337 with col1:338 st.info("**Physical Hazards**")339 st.write(f"**Flammable Liquids:** {report['Flammable Liquids']}")340 341 with col2:342 st.warning("**Health Hazards**")343 hazards_list = [344 "Acute Toxicity (Oral)", "Acute Toxicity (Dermal)", 345 "Acute Toxicity (Inhalation)", "Serious Eye Damage/Irritation", 346 "Skin Corrosion/Irritation", "Skin Sensitization", 347 "Aspiration Hazard", "STOT SE", "STOT RE", 348 "Mutagenicity", "Carcinogenicity", "Reproductive Toxicity"349 ]350 for hazard in hazards_list:351 if report[hazard] != "Not Classified": 352 st.write(f"**{hazard}:** {report[hazard]}")353 354 with col3:355 st.success("**Environmental Hazards**")356 st.write(f"**Aquatic Toxicity:** {report['Aquatic Toxicity']}")