CoolFace
Apppublic

Harshark/GHS-Classifier

sourceHugging Facemitupdated 7mo agoView on Hugging Face
0likes
streamlit_app.py356 linesDownload Raw Back to src
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']}")