Maria Castellanos
		
	commited on
		
		
					Commit 
							
							·
						
						20ed309
	
1
								Parent(s):
							
							9638dbd
								
validations and improvements
Browse files- _static/challenge_logo.png +0 -0
- about.py +6 -2
- app.py +91 -32
- evaluate.py +158 -60
- utils.py +15 -0
    	
        _static/challenge_logo.png
    CHANGED
    
    |   | 
|   | 
    	
        about.py
    CHANGED
    
    | @@ -15,13 +15,17 @@ ENDPOINTS = ["LogD", | |
| 15 | 
             
            LB_COLS0 = ["endpoint", 
         | 
| 16 | 
             
                        "user", 
         | 
| 17 | 
             
                        "MAE", 
         | 
|  | |
| 18 | 
             
                        "R2", 
         | 
| 19 | 
             
                        "Spearman R",
         | 
| 20 | 
             
                        "Kendall's Tau", 
         | 
|  | |
| 21 | 
             
                        "submission_time",
         | 
| 22 | 
             
                        "model_report"]
         | 
| 23 | 
            -
            LB_COLS = ["user", "MAE", "R2", "Spearman R", "Kendall's Tau", "submission time", "model details" | 
| 24 | 
            -
             | 
|  | |
|  | |
| 25 |  | 
| 26 | 
             
            TOKEN = os.environ.get("HF_TOKEN")
         | 
| 27 | 
             
            CACHE_PATH=os.getenv("HF_HOME", ".")
         | 
|  | |
| 15 | 
             
            LB_COLS0 = ["endpoint", 
         | 
| 16 | 
             
                        "user", 
         | 
| 17 | 
             
                        "MAE", 
         | 
| 18 | 
            +
                        "RAE",
         | 
| 19 | 
             
                        "R2", 
         | 
| 20 | 
             
                        "Spearman R",
         | 
| 21 | 
             
                        "Kendall's Tau", 
         | 
| 22 | 
            +
                        "data coverage (%)",
         | 
| 23 | 
             
                        "submission_time",
         | 
| 24 | 
             
                        "model_report"]
         | 
| 25 | 
            +
            LB_COLS = ["user", "MAE", "R2", "Spearman R", "Kendall's Tau", "submission time", "model details",
         | 
| 26 | 
            +
                       "data coverage (%)"]
         | 
| 27 | 
            +
            LB_AVG = ["user", "MA-RAE", "R2", "Spearman R", "Kendall's Tau", "submission time", "model details"] # Delete some columns for overall LB?
         | 
| 28 | 
            +
            LB_DTYPES = ['markdown', 'number', 'number', 'number', 'number', 'str', 'markdown', 'number']
         | 
| 29 |  | 
| 30 | 
             
            TOKEN = os.environ.get("HF_TOKEN")
         | 
| 31 | 
             
            CACHE_PATH=os.getenv("HF_HOME", ".")
         | 
    	
        app.py
    CHANGED
    
    | @@ -6,8 +6,7 @@ import pandas as pd | |
| 6 | 
             
            from evaluate import submit_data, evaluate_data
         | 
| 7 | 
             
            from utils import make_tag_clickable, make_user_clickable, fetch_dataset_df
         | 
| 8 |  | 
| 9 | 
            -
            from  | 
| 10 | 
            -
            from about import ENDPOINTS, LB_COLS, LB_DTYPES
         | 
| 11 |  | 
| 12 |  | 
| 13 | 
             
            ALL_EPS = ['Average'] + ENDPOINTS
         | 
| @@ -22,11 +21,19 @@ def build_leaderboard(df_results): | |
| 22 | 
             
                        per_ep[ep] = pd.DataFrame(columns=LB_COLS) # Empty df
         | 
| 23 | 
             
                        continue
         | 
| 24 |  | 
| 25 | 
            -
                    # Make user and model details clickable
         | 
| 26 | 
            -
                    df['user'] = df | 
|  | |
|  | |
| 27 | 
             
                    df['model details'] = df['model_report'].apply(lambda x: make_tag_clickable(x)).astype(str)
         | 
| 28 |  | 
| 29 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 30 |  | 
| 31 | 
             
                return per_ep
         | 
| 32 |  | 
| @@ -49,38 +56,36 @@ def gradio_interface(): | |
| 49 |  | 
| 50 | 
             
                    ### Header
         | 
| 51 | 
             
                    with gr.Row():
         | 
| 52 | 
            -
                        with gr.Column(scale= | 
| 53 | 
             
                            gr.Markdown("""
         | 
| 54 | 
            -
                                ## Welcome to the OpenADMET +  | 
| 55 | 
             
                                Your task is to develop and submit predictive models for key ADMET properties on a blinded test set of real world drug discovery data 🧑🔬
         | 
| 56 |  | 
| 57 | 
             
                                Go to the **Leaderboard** to check out how the challenge is going. 
         | 
| 58 | 
             
                                To participate, head out to the **Submit** tab and upload your results as a `CSV` file.
         | 
| 59 | 
             
                                """
         | 
| 60 | 
             
                                )
         | 
| 61 | 
            -
                        with gr.Column(scale= | 
| 62 | 
             
                            gr.Image(
         | 
| 63 | 
             
                                value="./_static/challenge_logo.png",
         | 
| 64 | 
             
                                show_label=False,
         | 
| 65 | 
             
                                show_download_button=False,
         | 
| 66 | 
            -
                                width=" | 
| 67 | 
             
                            )
         | 
| 68 |  | 
| 69 | 
             
                    # --- Welcome markdown message ---
         | 
| 70 | 
             
                    welcome_md = """
         | 
| 71 | 
            -
                    # 💊 OpenADMET +  | 
| 72 | 
             
                    ## Computational Blind Challenge in ADMET
         | 
| 73 |  | 
| 74 | 
             
                    This challenge is a community-driven initiative to benchmark predictive models for ADMET properties in drug discovery,
         | 
| 75 | 
            -
                    hosted by **OpenADMET** in collaboration with ** | 
| 76 |  | 
| 77 |  | 
| 78 | 
             
                    ## Why are ADMET properties important in drug discovery?
         | 
| 79 | 
             
                    Small molecules continue to be the bricks and mortar of drug discovery globally, accounting for ~75% of FDA approvals over the last decade. 
         | 
| 80 | 
            -
                    Oral bioavailability, easily tunable properties, modulation of a wide range of mechanisms, 
         | 
| 81 | 
            -
                     | 
| 82 | 
            -
                    despite increased interest in biologics.  Indeed, newer small molecule modalities such as degraders, molecular glues, and antibody-drug conjugates 
         | 
| 83 | 
            -
                    (to name a few) make understanding small molecule properties more important than ever.
         | 
| 84 |  | 
| 85 | 
             
                    It is fairly difficult to predict the lifetime and distribution of small molecules within the body. Additionally, 
         | 
| 86 | 
             
                    interaction with off-targets can cause safety issues and toxicity. Collectively these *Absorption*, *Distribution*, *Metabolism*, *Excretion*, *Toxicology*--or **ADMET**--properties 
         | 
| @@ -90,7 +95,17 @@ def gradio_interface(): | |
| 90 | 
             
                    that give rise to these properties through integrated structural biology, high throughput experimentation and integrative computational models. 
         | 
| 91 | 
             
                    Read more about our strategy to transform drug discovery on our [website](https://openadmet.org/community/blogs/whatisopenadmet/). 
         | 
| 92 |  | 
| 93 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 94 | 
             
                    - LogD
         | 
| 95 | 
             
                    - Kinetic Solubility **KSOL**: uM
         | 
| 96 | 
             
                    - Mouse Liver Microsomal (**MLM**) *CLint*: mL/min/kg
         | 
| @@ -102,16 +117,18 @@ def gradio_interface(): | |
| 102 | 
             
                    - Rat Liver Microsomal (**RLM**) *Clint*: mL/min/kg
         | 
| 103 | 
             
                    - Mouse Gastrocnemius Muscle Binding (**MGMB**): % Unbound
         | 
| 104 |  | 
|  | |
|  | |
| 105 | 
             
                    ## ✅ How to Participate
         | 
| 106 | 
             
                    1. **Register**: Create an account with Hugging Face.
         | 
| 107 | 
            -
                    2. **Download the Public Dataset**: Clone the  | 
| 108 | 
             
                    3. **Train Your Model**: Use the provided training data for each ADMET property of your choice.
         | 
| 109 | 
             
                    4. **Submit Predictions**: Follow the instructions in the *Submit* tab to upload your predictions.
         | 
| 110 | 
            -
                    5. Join the discussion on the [Challenge Discord]( | 
| 111 |  | 
| 112 | 
             
                    ## 📊 Data:
         | 
| 113 |  | 
| 114 | 
            -
                    The training set  | 
| 115 |  | 
| 116 | 
             
                    | Column                       |    Unit   | data type |  Description |
         | 
| 117 | 
             
                    |:-----------------------------|-----------|-----------|:-------------|
         | 
| @@ -128,15 +145,28 @@ def gradio_interface(): | |
| 128 | 
             
                    | RLM CLint                    | mL/min/kg |   float   | Rat Liver Microsomal Stability |
         | 
| 129 | 
             
                    | MGMB.                        | % Unbound |   float   | Mouse Gastrocnemius Muscle Binding |
         | 
| 130 |  | 
| 131 | 
            -
                     | 
|  | |
| 132 |  | 
| 133 | 
             
                    ## 📝 Evaluation
         | 
| 134 | 
            -
                    The challenge will be judged based on the  | 
| 135 | 
            -
             | 
| 136 | 
            -
                     | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 137 |  | 
| 138 | 
             
                    📅 **Timeline**:  
         | 
| 139 | 
            -
                    -  | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 140 |  | 
| 141 | 
             
                    ---
         | 
| 142 |  | 
| @@ -166,7 +196,7 @@ def gradio_interface(): | |
| 166 | 
             
                                lboard_dict['Average'] = Leaderboard(
         | 
| 167 | 
             
                                    value=build_leaderboard(current_df)['Average'],
         | 
| 168 | 
             
                                    datatype=LB_DTYPES,
         | 
| 169 | 
            -
                                    select_columns= | 
| 170 | 
             
                                    search_columns=["user"],
         | 
| 171 | 
             
                                    render=True,
         | 
| 172 | 
             
                                    every=15,
         | 
| @@ -207,10 +237,11 @@ def gradio_interface(): | |
| 207 | 
             
                                    gr.Markdown(
         | 
| 208 | 
             
                                        """
         | 
| 209 | 
             
                                        ## Participant Information
         | 
| 210 | 
            -
                                        To participate, we  | 
| 211 | 
            -
                                         | 
|  | |
| 212 | 
             
                                        If you wish to be included in Challenge discussions, please provide your Discord username and email. 
         | 
| 213 | 
            -
                                        If you wish to be included in a future publication with the Challenge results, please provide your name and affiliation.
         | 
| 214 |  | 
| 215 | 
             
                                        We also ask you to provide a link to a report decribing your method. While not mandatory at the time of participation, 
         | 
| 216 | 
             
                                        you need to submit the link before the challenge deadline in order to be considered for the final leaderboard.
         | 
| @@ -221,7 +252,17 @@ def gradio_interface(): | |
| 221 | 
             
                                    username_input = gr.Textbox(
         | 
| 222 | 
             
                                        label="Username", 
         | 
| 223 | 
             
                                        placeholder="Enter your Hugging Face username",
         | 
| 224 | 
            -
                                        info="This will be displayed on the leaderboard."
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 225 | 
             
                                    )
         | 
| 226 | 
             
                                with gr.Column():
         | 
| 227 | 
             
                                    # Info to track participant, that will not be displayed publicly
         | 
| @@ -247,6 +288,10 @@ def gradio_interface(): | |
| 247 | 
             
                                        label="Model Report",
         | 
| 248 | 
             
                                        placeholder="Link to a report describing your method (optional)",
         | 
| 249 | 
             
                                    )
         | 
|  | |
|  | |
|  | |
|  | |
| 250 |  | 
| 251 | 
             
                            with gr.Row():
         | 
| 252 | 
             
                                with gr.Column():
         | 
| @@ -256,9 +301,14 @@ def gradio_interface(): | |
| 256 | 
             
                                        Upload a single CSV file containing your predictions for all ligands in the test set.
         | 
| 257 | 
             
                                        Only your latest submission will be considered.
         | 
| 258 |  | 
| 259 | 
            -
                                        You can download the  | 
| 260 | 
             
                                        """
         | 
| 261 | 
             
                                    )
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 262 | 
             
                                with gr.Column():
         | 
| 263 | 
             
                                    predictions_file = gr.File(label="Single file with ADMET predictions (.csv)",
         | 
| 264 | 
             
                                                            file_types=[".csv"],
         | 
| @@ -270,12 +320,21 @@ def gradio_interface(): | |
| 270 | 
             
                                outputs=user_state
         | 
| 271 | 
             
                            )  
         | 
| 272 |  | 
| 273 | 
            -
                            submit_btn = gr.Button("Submit Predictions")
         | 
| 274 | 
             
                            message = gr.Textbox(label="Status", lines=1, visible=False)
         | 
| 275 |  | 
| 276 | 
             
                            submit_btn.click(
         | 
| 277 | 
             
                                submit_data,
         | 
| 278 | 
            -
                                inputs=[predictions_file,  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 279 | 
             
                                outputs=[message, filename],
         | 
| 280 | 
             
                            ).success(
         | 
| 281 | 
             
                                fn=lambda m: gr.update(value=m, visible=True),
         | 
|  | |
| 6 | 
             
            from evaluate import submit_data, evaluate_data
         | 
| 7 | 
             
            from utils import make_tag_clickable, make_user_clickable, fetch_dataset_df
         | 
| 8 |  | 
| 9 | 
            +
            from about import ENDPOINTS, LB_COLS, LB_AVG, LB_DTYPES
         | 
|  | |
| 10 |  | 
| 11 |  | 
| 12 | 
             
            ALL_EPS = ['Average'] + ENDPOINTS
         | 
|  | |
| 21 | 
             
                        per_ep[ep] = pd.DataFrame(columns=LB_COLS) # Empty df
         | 
| 22 | 
             
                        continue
         | 
| 23 |  | 
| 24 | 
            +
                    # Make user and model details clickable if it's a huggingface user
         | 
| 25 | 
            +
                    df['user'] = df.apply(
         | 
| 26 | 
            +
                        lambda row: make_user_clickable(row['user']) if not row['anonymous'] else row['user'], 
         | 
| 27 | 
            +
                        axis=1).astype(str)
         | 
| 28 | 
             
                    df['model details'] = df['model_report'].apply(lambda x: make_tag_clickable(x)).astype(str)
         | 
| 29 |  | 
| 30 | 
            +
                    if ep == "Average":
         | 
| 31 | 
            +
                        df["MA-RAE"] = df["RAE"]  # The average of the RAE per endpoint
         | 
| 32 | 
            +
                        sorted_df = df.sort_values(by='MA-RAE', ascending=True, kind="stable")
         | 
| 33 | 
            +
                        per_ep[ep] = sorted_df[LB_AVG]
         | 
| 34 | 
            +
                    else:
         | 
| 35 | 
            +
                        sorted_df = df.sort_values(by="MAE", ascending=True, kind="stable")
         | 
| 36 | 
            +
                        per_ep[ep] = sorted_df[LB_COLS]
         | 
| 37 |  | 
| 38 | 
             
                return per_ep
         | 
| 39 |  | 
|  | |
| 56 |  | 
| 57 | 
             
                    ### Header
         | 
| 58 | 
             
                    with gr.Row():
         | 
| 59 | 
            +
                        with gr.Column(scale=7):  # bigger text area
         | 
| 60 | 
             
                            gr.Markdown("""
         | 
| 61 | 
            +
                                ## Welcome to the OpenADMET + ExpansionRx Blind Challenge!
         | 
| 62 | 
             
                                Your task is to develop and submit predictive models for key ADMET properties on a blinded test set of real world drug discovery data 🧑🔬
         | 
| 63 |  | 
| 64 | 
             
                                Go to the **Leaderboard** to check out how the challenge is going. 
         | 
| 65 | 
             
                                To participate, head out to the **Submit** tab and upload your results as a `CSV` file.
         | 
| 66 | 
             
                                """
         | 
| 67 | 
             
                                )
         | 
| 68 | 
            +
                        with gr.Column(scale=2):  # smaller side column for logo
         | 
| 69 | 
             
                            gr.Image(
         | 
| 70 | 
             
                                value="./_static/challenge_logo.png",
         | 
| 71 | 
             
                                show_label=False,
         | 
| 72 | 
             
                                show_download_button=False,
         | 
| 73 | 
            +
                                width="5vw",  # Take up the width of the column (2/8 = 1/4)
         | 
| 74 | 
             
                            )
         | 
| 75 |  | 
| 76 | 
             
                    # --- Welcome markdown message ---
         | 
| 77 | 
             
                    welcome_md = """
         | 
| 78 | 
            +
                    # 💊 OpenADMET + ExpansionRx  
         | 
| 79 | 
             
                    ## Computational Blind Challenge in ADMET
         | 
| 80 |  | 
| 81 | 
             
                    This challenge is a community-driven initiative to benchmark predictive models for ADMET properties in drug discovery,
         | 
| 82 | 
            +
                    hosted by **OpenADMET** in collaboration with **ExpansionRx**. 
         | 
| 83 |  | 
| 84 |  | 
| 85 | 
             
                    ## Why are ADMET properties important in drug discovery?
         | 
| 86 | 
             
                    Small molecules continue to be the bricks and mortar of drug discovery globally, accounting for ~75% of FDA approvals over the last decade. 
         | 
| 87 | 
            +
                    Oral bioavailability, easily tunable properties, modulation of a wide range of mechanisms, and ease of manufacturing make small molecules highly attractive as therapeutic agents. 
         | 
| 88 | 
            +
                    Moreover, emerging small molecule modalities such as degraders, expression modulators, molecular glues, and antibody-drug conjugates (to name a few) have vastly expanded what we thought small molecules were capable of. 
         | 
|  | |
|  | |
| 89 |  | 
| 90 | 
             
                    It is fairly difficult to predict the lifetime and distribution of small molecules within the body. Additionally, 
         | 
| 91 | 
             
                    interaction with off-targets can cause safety issues and toxicity. Collectively these *Absorption*, *Distribution*, *Metabolism*, *Excretion*, *Toxicology*--or **ADMET**--properties 
         | 
|  | |
| 95 | 
             
                    that give rise to these properties through integrated structural biology, high throughput experimentation and integrative computational models. 
         | 
| 96 | 
             
                    Read more about our strategy to transform drug discovery on our [website](https://openadmet.org/community/blogs/whatisopenadmet/). 
         | 
| 97 |  | 
| 98 | 
            +
                    Critical to our mission is developing open datasets and running community blind challenges to assess the current state of the art in ADMET modeling. 
         | 
| 99 | 
            +
                    Building on the sucess of the recent [ASAP-Polaris-OpenADMET blind challenge](https://chemrxiv.org/engage/chemrxiv/article-details/68ac00d1728bf9025e22fe45) in computational methods for drug discovery,
         | 
| 100 | 
            +
                    we bring you a brand new challenge in collaboration with **ExpansionRx**. During a recent series of drug discovery campaigns for RNA mediated diseases, 
         | 
| 101 | 
            +
                    ExpansionRX collected a variety of ADMET data for off-targets and properties of interest, which they are generously sharing with the community for this challenge.
         | 
| 102 | 
            +
             | 
| 103 | 
            +
                    ## 🧪 The Challenge
         | 
| 104 | 
            +
                    
         | 
| 105 | 
            +
                    Participants will be tasked with solving real-world ADMET prediction problems ExpansionRx faced during lead optimization. 
         | 
| 106 | 
            +
                    Specifically, you will be asked to predict the ADMET properties of late-stage molecules based on earlier-stage data from the same campaigns.
         | 
| 107 | 
            +
                    For this challenge we selected ten (10) crucial endpoints for the community to predict:
         | 
| 108 | 
            +
             | 
| 109 | 
             
                    - LogD
         | 
| 110 | 
             
                    - Kinetic Solubility **KSOL**: uM
         | 
| 111 | 
             
                    - Mouse Liver Microsomal (**MLM**) *CLint*: mL/min/kg
         | 
|  | |
| 117 | 
             
                    - Rat Liver Microsomal (**RLM**) *Clint*: mL/min/kg
         | 
| 118 | 
             
                    - Mouse Gastrocnemius Muscle Binding (**MGMB**): % Unbound
         | 
| 119 |  | 
| 120 | 
            +
                    Find more information about these endpoints on our [blog](https://openadmet.org/community/blogs/challenge_announcement2/).
         | 
| 121 | 
            +
             | 
| 122 | 
             
                    ## ✅ How to Participate
         | 
| 123 | 
             
                    1. **Register**: Create an account with Hugging Face.
         | 
| 124 | 
            +
                    2. **Download the Public Dataset**: Clone the ExpansionRx dataset [link]
         | 
| 125 | 
             
                    3. **Train Your Model**: Use the provided training data for each ADMET property of your choice.
         | 
| 126 | 
             
                    4. **Submit Predictions**: Follow the instructions in the *Submit* tab to upload your predictions.
         | 
| 127 | 
            +
                    5. Join the discussion on the [Challenge Discord](https://discord.gg/MY5cEFHH3D)!
         | 
| 128 |  | 
| 129 | 
             
                    ## 📊 Data:
         | 
| 130 |  | 
| 131 | 
            +
                    The training set contains the following parameters:
         | 
| 132 |  | 
| 133 | 
             
                    | Column                       |    Unit   | data type |  Description |
         | 
| 134 | 
             
                    |:-----------------------------|-----------|-----------|:-------------|
         | 
|  | |
| 145 | 
             
                    | RLM CLint                    | mL/min/kg |   float   | Rat Liver Microsomal Stability |
         | 
| 146 | 
             
                    | MGMB.                        | % Unbound |   float   | Mouse Gastrocnemius Muscle Binding |
         | 
| 147 |  | 
| 148 | 
            +
                    You can download the training data from the [Hugging Face dataset](https://huggingface.co/datasets/OpenADMET/openadmet-challenge-training-set).
         | 
| 149 | 
            +
                    The test set will remained blinded until the challenge submission deadline. You will be tasked with predicting the same set of ADMET endpoints for the test set molecules.
         | 
| 150 |  | 
| 151 | 
             
                    ## 📝 Evaluation
         | 
| 152 | 
            +
                    The challenge will be judged based on the following criteria:
         | 
| 153 | 
            +
                    - We welcome submissions of any kind, including machine learning and physics-based approaches. You can also employ pre-training approaches as you see fit, 
         | 
| 154 | 
            +
                    as well as incorporate data from external sources into your models and submissions. 
         | 
| 155 | 
            +
                    - In the spirit of open science and open source we would love to see code showing how you created your submission if possible, in the form of a Github Repository. 
         | 
| 156 | 
            +
                    If not possible due to IP or other constraints you must at a minimum provide a short report written methodology based on the template [here](link to google doc).
         | 
| 157 | 
            +
                    **Make sure your lat submission before the deadline includes a link to a report or to a Github repository.**
         | 
| 158 | 
            +
                    - Each participant can submit as many times as they like, up to a limit of 5 times/day. **Only your latest submission will be considered for the final leaderboard.**
         | 
| 159 | 
            +
                    - The endpoints will be judged individually by mean absolute error (**MAE**), while an overall leaderboard will be judged by the macro-averaged relative absolute error (**MA-RAE**). 
         | 
| 160 | 
            +
                    - For endpoints that are not already on a log scale (e.g LogD) they will be transformed to log scale to minimize the impact of outliers on evaluation.
         | 
| 161 | 
            +
                    - We will estimate errors on the metrics using bootstrapping and use the statistical testing workflow outlined in [this paper](https://chemrxiv.org/engage/chemrxiv/article-details/672a91bd7be152b1d01a926b) to determine if model performance is statistically distinct.
         | 
| 162 |  | 
| 163 | 
             
                    📅 **Timeline**:  
         | 
| 164 | 
            +
                    - **September 12:** Challenge announcement
         | 
| 165 | 
            +
                    - **September XX:** Sample data release
         | 
| 166 | 
            +
                    - **October 27:** Challenge starts
         | 
| 167 | 
            +
                    - **October-November:** Online Q&A sessions and support via the Discord channel
         | 
| 168 | 
            +
                    - **January 19, 2026:** Submission closes
         | 
| 169 | 
            +
                    - **January 26, 2026:** Winners announced
         | 
| 170 |  | 
| 171 | 
             
                    ---
         | 
| 172 |  | 
|  | |
| 196 | 
             
                                lboard_dict['Average'] = Leaderboard(
         | 
| 197 | 
             
                                    value=build_leaderboard(current_df)['Average'],
         | 
| 198 | 
             
                                    datatype=LB_DTYPES,
         | 
| 199 | 
            +
                                    select_columns=LB_AVG,
         | 
| 200 | 
             
                                    search_columns=["user"],
         | 
| 201 | 
             
                                    render=True,
         | 
| 202 | 
             
                                    every=15,
         | 
|  | |
| 237 | 
             
                                    gr.Markdown(
         | 
| 238 | 
             
                                        """
         | 
| 239 | 
             
                                        ## Participant Information
         | 
| 240 | 
            +
                                        To participate, **we require a Hugging Face username**, which will be used to track multiple submissions.
         | 
| 241 | 
            +
                                        Your username will be displayed on the leaderboard, unless you check the *anonymous* box. If you want to remain anonymous, please provide an alias to be used for the leaderboard (we'll keep the username hidden).
         | 
| 242 | 
            +
             | 
| 243 | 
             
                                        If you wish to be included in Challenge discussions, please provide your Discord username and email. 
         | 
| 244 | 
            +
                                        If you wish to be included in a future publication with the Challenge results, please provide your name and affiliation (and check the box below).
         | 
| 245 |  | 
| 246 | 
             
                                        We also ask you to provide a link to a report decribing your method. While not mandatory at the time of participation, 
         | 
| 247 | 
             
                                        you need to submit the link before the challenge deadline in order to be considered for the final leaderboard.
         | 
|  | |
| 252 | 
             
                                    username_input = gr.Textbox(
         | 
| 253 | 
             
                                        label="Username", 
         | 
| 254 | 
             
                                        placeholder="Enter your Hugging Face username",
         | 
| 255 | 
            +
                                        # info="This will be displayed on the leaderboard."
         | 
| 256 | 
            +
                                    )
         | 
| 257 | 
            +
                                    user_alias = gr.Textbox(
         | 
| 258 | 
            +
                                        label="Optional Alias", 
         | 
| 259 | 
            +
                                        placeholder="Enter an identifying alias for the leaderboard if you wish to remain anonymous",
         | 
| 260 | 
            +
                                        # info="This will be displayed on the leaderboard."
         | 
| 261 | 
            +
                                    )
         | 
| 262 | 
            +
                                    anon_checkbox = gr.Checkbox(
         | 
| 263 | 
            +
                                        label="I want to submit anonymously",
         | 
| 264 | 
            +
                                        info="If checked, your username will be replaced with 'anonymous' on the leaderboard.",
         | 
| 265 | 
            +
                                        value=False,
         | 
| 266 | 
             
                                    )
         | 
| 267 | 
             
                                with gr.Column():
         | 
| 268 | 
             
                                    # Info to track participant, that will not be displayed publicly
         | 
|  | |
| 288 | 
             
                                        label="Model Report",
         | 
| 289 | 
             
                                        placeholder="Link to a report describing your method (optional)",
         | 
| 290 | 
             
                                    )
         | 
| 291 | 
            +
                                    paper_checkbox = gr.Checkbox(
         | 
| 292 | 
            +
                                        label="I want to be included in a future publication detailing the Challenge results",
         | 
| 293 | 
            +
                                        value=False,
         | 
| 294 | 
            +
                                    )
         | 
| 295 |  | 
| 296 | 
             
                            with gr.Row():
         | 
| 297 | 
             
                                with gr.Column():
         | 
|  | |
| 301 | 
             
                                        Upload a single CSV file containing your predictions for all ligands in the test set.
         | 
| 302 | 
             
                                        Only your latest submission will be considered.
         | 
| 303 |  | 
| 304 | 
            +
                                        You can download a CSV template with the ligands in the test set here.
         | 
| 305 | 
             
                                        """
         | 
| 306 | 
             
                                    )
         | 
| 307 | 
            +
                                    download_btn = gr.DownloadButton(
         | 
| 308 | 
            +
                                        label="📥 Download Test Set Template",
         | 
| 309 | 
            +
                                        value="data/test_set-example.csv",
         | 
| 310 | 
            +
                                        variant="secondary",
         | 
| 311 | 
            +
                                        )
         | 
| 312 | 
             
                                with gr.Column():
         | 
| 313 | 
             
                                    predictions_file = gr.File(label="Single file with ADMET predictions (.csv)",
         | 
| 314 | 
             
                                                            file_types=[".csv"],
         | 
|  | |
| 320 | 
             
                                outputs=user_state
         | 
| 321 | 
             
                            )  
         | 
| 322 |  | 
| 323 | 
            +
                            submit_btn = gr.Button("📤 Submit Predictions")
         | 
| 324 | 
             
                            message = gr.Textbox(label="Status", lines=1, visible=False)
         | 
| 325 |  | 
| 326 | 
             
                            submit_btn.click(
         | 
| 327 | 
             
                                submit_data,
         | 
| 328 | 
            +
                                inputs=[predictions_file, 
         | 
| 329 | 
            +
                                        user_state, 
         | 
| 330 | 
            +
                                        participant_name, 
         | 
| 331 | 
            +
                                        discord_username, 
         | 
| 332 | 
            +
                                        email, 
         | 
| 333 | 
            +
                                        affiliation, 
         | 
| 334 | 
            +
                                        model_tag, 
         | 
| 335 | 
            +
                                        user_alias,
         | 
| 336 | 
            +
                                        anon_checkbox,
         | 
| 337 | 
            +
                                        paper_checkbox],
         | 
| 338 | 
             
                                outputs=[message, filename],
         | 
| 339 | 
             
                            ).success(
         | 
| 340 | 
             
                                fn=lambda m: gr.update(value=m, visible=True),
         | 
    	
        evaluate.py
    CHANGED
    
    | @@ -1,27 +1,84 @@ | |
| 1 | 
             
            import gradio as gr
         | 
| 2 | 
             
            import pandas as pd
         | 
| 3 | 
             
            from pathlib import Path
         | 
| 4 | 
            -
            from scipy.stats import spearmanr, kendalltau
         | 
| 5 | 
            -
            from sklearn.metrics import mean_absolute_error, r2_score
         | 
| 6 | 
             
            from typing import Optional
         | 
| 7 | 
             
            from about import ENDPOINTS, API, submissions_repo, results_repo, test_repo
         | 
|  | |
| 8 | 
             
            from huggingface_hub import hf_hub_download
         | 
| 9 | 
             
            import datetime
         | 
| 10 | 
             
            import io
         | 
| 11 | 
             
            import json, tempfile
         | 
| 12 | 
            -
            import  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 13 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 14 |  | 
| 15 | 
            -
            class ParticipantRecord( | 
| 16 | 
            -
                hf_username:  | 
| 17 | 
            -
                 | 
| 18 | 
            -
                 | 
| 19 | 
            -
                 | 
| 20 | 
            -
                 | 
| 21 | 
            -
                 | 
|  | |
|  | |
|  | |
| 22 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 23 |  | 
| 24 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 25 | 
             
                submission_time_utc: str
         | 
| 26 | 
             
                user: str
         | 
| 27 | 
             
                original_filename: str
         | 
| @@ -29,12 +86,6 @@ class SubmissionMetadata(pydantic.BaseModel): | |
| 29 | 
             
                participant: ParticipantRecord
         | 
| 30 |  | 
| 31 |  | 
| 32 | 
            -
            def _safeify_username(username: str) -> str:
         | 
| 33 | 
            -
                return str(username.strip()).replace("/", "_").replace(" ", "_")
         | 
| 34 | 
            -
             | 
| 35 | 
            -
            def _unsafify_username(username: str) -> str:
         | 
| 36 | 
            -
                return str(username.strip()).replace("/", "_").replace(" ", "_")
         | 
| 37 | 
            -
             | 
| 38 | 
             
            def submit_data(predictions_file: str, 
         | 
| 39 | 
             
                            user_state,
         | 
| 40 | 
             
                            participant_name: str = "",
         | 
| @@ -42,13 +93,15 @@ def submit_data(predictions_file: str, | |
| 42 | 
             
                            email: str = "",
         | 
| 43 | 
             
                            affiliation: str = "",
         | 
| 44 | 
             
                            model_tag: str = "",
         | 
|  | |
|  | |
|  | |
| 45 | 
             
            ):
         | 
| 46 |  | 
| 47 | 
             
                if user_state is None:
         | 
| 48 | 
             
                    raise gr.Error("Username or alias is required for submission.")
         | 
| 49 |  | 
| 50 | 
             
                file_path = Path(predictions_file).resolve()
         | 
| 51 | 
            -
             | 
| 52 | 
             
                if not file_path.exists():
         | 
| 53 | 
             
                    raise gr.Error("Uploaded file object does not have a valid file path.")
         | 
| 54 |  | 
| @@ -60,29 +113,13 @@ def submit_data(predictions_file: str, | |
| 60 |  | 
| 61 | 
             
                if results_df.empty:
         | 
| 62 | 
             
                    return gr.Error("The uploaded file is empty.")
         | 
| 63 | 
            -
                if not set(ENDPOINTS).issubset(set(results_df.columns)):
         | 
| 64 | 
            -
                    return gr.Error(f"The uploaded file must contain all endpoint predictions {ENDPOINTS} as columns.")
         | 
| 65 |  | 
| 66 | 
            -
                 | 
| 67 | 
            -
             | 
| 68 | 
            -
             | 
| 69 | 
            -
                ts = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds") # should keep default time so can be deserialized correctly
         | 
| 70 | 
            -
                safe_user = _safeify_username(user_state)
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                destination_csv = f"submissions/{safe_user}_{ts}.csv"
         | 
| 73 | 
            -
                destination_json = destination_csv.replace(".csv", ".json")
         | 
| 74 | 
            -
                # Upload the CSV file
         | 
| 75 | 
            -
                API.upload_file(
         | 
| 76 | 
            -
                    path_or_fileobj=str(file_path),
         | 
| 77 | 
            -
                    path_in_repo=destination_csv,
         | 
| 78 | 
            -
                    repo_id=submissions_repo,
         | 
| 79 | 
            -
                    repo_type="dataset",
         | 
| 80 | 
            -
                    commit_message=f"Add submission for {safe_user} at {ts}"
         | 
| 81 | 
            -
                )
         | 
| 82 |  | 
| 83 | 
            -
                #  | 
| 84 | 
             
                try:
         | 
| 85 | 
            -
             | 
| 86 | 
             
                    participant_record = ParticipantRecord(
         | 
| 87 | 
             
                        hf_username=user_state,
         | 
| 88 | 
             
                        participant_name=participant_name,
         | 
| @@ -90,11 +127,15 @@ def submit_data(predictions_file: str, | |
| 90 | 
             
                        email=email,
         | 
| 91 | 
             
                        affiliation=affiliation,
         | 
| 92 | 
             
                        model_tag=model_tag,
         | 
|  | |
|  | |
|  | |
| 93 | 
             
                    )
         | 
| 94 | 
            -
                except  | 
| 95 | 
             
                    return f"❌ Error in participant information: {str(e)}"
         | 
| 96 | 
            -
             | 
| 97 | 
            -
             | 
|  | |
| 98 | 
             
                try:
         | 
| 99 | 
             
                    meta = SubmissionMetadata(
         | 
| 100 | 
             
                        submission_time_utc=ts,
         | 
| @@ -103,11 +144,23 @@ def submit_data(predictions_file: str, | |
| 103 | 
             
                        evaluated=False,
         | 
| 104 | 
             
                        participant=participant_record
         | 
| 105 | 
             
                    )
         | 
| 106 | 
            -
                except  | 
| 107 | 
            -
                    return f"❌ Error in metadata information: {str(e)}"
         | 
| 108 |  | 
| 109 | 
            -
                 | 
|  | |
|  | |
| 110 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 111 | 
             
                API.upload_file(
         | 
| 112 | 
             
                    path_or_fileobj=meta_bytes,
         | 
| 113 | 
             
                    path_in_repo=destination_json,
         | 
| @@ -135,7 +188,7 @@ def evaluate_data(filename: str) -> None: | |
| 135 | 
             
                    test_path = hf_hub_download(
         | 
| 136 | 
             
                        repo_id=test_repo,
         | 
| 137 | 
             
                        repo_type="dataset",
         | 
| 138 | 
            -
                        filename="data/challenge_mock_test_set.csv", #Replace later with "test_dataset.csv" | 
| 139 | 
             
                    )
         | 
| 140 | 
             
                except Exception as e:
         | 
| 141 | 
             
                    raise gr.Error(f"Failed to download test file: {e}")
         | 
| @@ -163,13 +216,18 @@ def evaluate_data(filename: str) -> None: | |
| 163 | 
             
                    username = meta.participant.hf_username
         | 
| 164 | 
             
                    timestamp = meta.submission_time_utc
         | 
| 165 | 
             
                    report = meta.participant.model_tag
         | 
|  | |
|  | |
|  | |
|  | |
| 166 | 
             
                except Exception as e:
         | 
| 167 | 
             
                    raise gr.Error(f"Failed to load metadata file: {e}. No results written to results dataset.")
         | 
| 168 |  | 
| 169 | 
             
                # Write results to results dataset
         | 
| 170 | 
            -
                results_df['user'] =  | 
| 171 | 
             
                results_df['submission_time'] = timestamp
         | 
| 172 | 
             
                results_df['model_report'] = report
         | 
|  | |
| 173 | 
             
                safe_user = _unsafify_username(username)
         | 
| 174 | 
             
                destination_path = f"results/{safe_user}_{timestamp}_results.csv"
         | 
| 175 | 
             
                tmp_name = None
         | 
| @@ -192,29 +250,69 @@ def calculate_metrics( | |
| 192 | 
             
                    results_dataframe: pd.DataFrame,
         | 
| 193 | 
             
                    test_dataframe: pd.DataFrame
         | 
| 194 | 
             
                ):
         | 
|  | |
|  | |
|  | |
| 195 |  | 
| 196 | 
            -
                 | 
| 197 | 
            -
             | 
| 198 | 
            -
             | 
| 199 | 
            -
             | 
| 200 | 
            -
             | 
| 201 | 
            -
             | 
|  | |
|  | |
| 202 |  | 
| 203 | 
            -
                df_results = pd.DataFrame(columns=["endpoint", "MAE", "R2", "Spearman R", "Kendall's Tau"])
         | 
| 204 | 
             
                for i, measurement in enumerate(ENDPOINTS):  
         | 
| 205 | 
            -
                    df_pred = results_dataframe[['Molecule Name', measurement]]. | 
| 206 | 
            -
                    df_true = test_dataframe[['Molecule Name', measurement]]. | 
| 207 | 
            -
                    #  | 
| 208 | 
            -
                     | 
| 209 | 
            -
                     | 
| 210 | 
            -
             | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 211 | 
             
                    df_results.loc[i, 'endpoint'] = measurement
         | 
| 212 | 
             
                    df_results.loc[i, 'MAE'] = mae
         | 
|  | |
| 213 | 
             
                    df_results.loc[i, 'R2'] = r2
         | 
| 214 | 
             
                    df_results.loc[i, 'Spearman R'] = spearman
         | 
| 215 | 
             
                    df_results.loc[i, "Kendall's Tau"] = ktau
         | 
|  | |
| 216 |  | 
| 217 | 
            -
                 | 
|  | |
| 218 | 
             
                df_results[num_cols] = df_results[num_cols].apply(pd.to_numeric, errors="coerce")
         | 
| 219 | 
             
                means = df_results[num_cols].mean()
         | 
| 220 | 
             
                avg_row = {"endpoint": "Average", **means.to_dict()}
         | 
|  | |
| 1 | 
             
            import gradio as gr
         | 
| 2 | 
             
            import pandas as pd
         | 
| 3 | 
             
            from pathlib import Path
         | 
|  | |
|  | |
| 4 | 
             
            from typing import Optional
         | 
| 5 | 
             
            from about import ENDPOINTS, API, submissions_repo, results_repo, test_repo
         | 
| 6 | 
            +
            from utils import metrics_per_ep
         | 
| 7 | 
             
            from huggingface_hub import hf_hub_download
         | 
| 8 | 
             
            import datetime
         | 
| 9 | 
             
            import io
         | 
| 10 | 
             
            import json, tempfile
         | 
| 11 | 
            +
            import re
         | 
| 12 | 
            +
            from pydantic import (
         | 
| 13 | 
            +
                BaseModel, 
         | 
| 14 | 
            +
                Field, 
         | 
| 15 | 
            +
                model_validator, 
         | 
| 16 | 
            +
                field_validator, 
         | 
| 17 | 
            +
                ValidationError
         | 
| 18 | 
            +
            )
         | 
| 19 |  | 
| 20 | 
            +
            HF_USERNAME_RE = re.compile(r"^[A-Za-z0-9](?:[A-Za-z0-9-_]{1,38})$")
         | 
| 21 | 
            +
            def _safeify_username(username: str) -> str:
         | 
| 22 | 
            +
                return str(username.strip()).replace("/", "_").replace(" ", "_")
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            def _unsafify_username(username: str) -> str:
         | 
| 25 | 
            +
                return str(username.strip()).replace("/", "_").replace(" ", "_")
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            def _check_required_columns(df: pd.DataFrame, name: str, cols: list[str]):
         | 
| 28 | 
            +
                missing = [c for c in cols if c not in df.columns]
         | 
| 29 | 
            +
                if missing:
         | 
| 30 | 
            +
                    raise ValueError(f"{name} is missing required columns: {missing}")
         | 
| 31 |  | 
| 32 | 
            +
            class ParticipantRecord(BaseModel):
         | 
| 33 | 
            +
                hf_username: str = Field(description="Hugging Face username")
         | 
| 34 | 
            +
                display_name: Optional[str] = Field(description="Name to display on leaderboard")
         | 
| 35 | 
            +
                participant_name: Optional[str] = Field(default=None, description="Participant's real name")
         | 
| 36 | 
            +
                discord_username: Optional[str] = Field(default=None, description="Discord username")
         | 
| 37 | 
            +
                email: Optional[str] = Field(default=None, description="Email address")
         | 
| 38 | 
            +
                affiliation: Optional[str] = Field(default=None, description="Affiliation")
         | 
| 39 | 
            +
                model_tag: Optional[str] = Field(default=None, description="Link to model description")
         | 
| 40 | 
            +
                anonymous: bool = Field(default=False, description="Whether to display username as 'anonymous'")
         | 
| 41 | 
            +
                consent_publication: bool = Field(default=False, description="Consent to be included in publications")
         | 
| 42 |  | 
| 43 | 
            +
                @field_validator("hf_username")
         | 
| 44 | 
            +
                @classmethod
         | 
| 45 | 
            +
                def validate_hf_username(cls, v: str) -> str:
         | 
| 46 | 
            +
                    v = v.strip()
         | 
| 47 | 
            +
                    if not HF_USERNAME_RE.match(v):
         | 
| 48 | 
            +
                        raise gr.Error("Invalid Hugging Face username (letters, numbers, -, _; min 2, max ~39).")
         | 
| 49 | 
            +
                    return v
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                @field_validator("display_name")
         | 
| 52 | 
            +
                @classmethod
         | 
| 53 | 
            +
                def validate_display_name(cls, v: Optional[str]) -> Optional[str]:
         | 
| 54 | 
            +
                    if v is None:
         | 
| 55 | 
            +
                        return None
         | 
| 56 | 
            +
                    v = v.strip()
         | 
| 57 | 
            +
                    if not v:
         | 
| 58 | 
            +
                        return None
         | 
| 59 | 
            +
                    if len(v) > 20:
         | 
| 60 | 
            +
                        raise ValueError("Display name is too long (max 20 chars).")
         | 
| 61 | 
            +
                    return v
         | 
| 62 | 
            +
                
         | 
| 63 | 
            +
                @field_validator("model_tag", mode="before")
         | 
| 64 | 
            +
                @classmethod
         | 
| 65 | 
            +
                def normalize_url(cls, v):
         | 
| 66 | 
            +
                    if v is None:
         | 
| 67 | 
            +
                        return v
         | 
| 68 | 
            +
                    s = str(v).strip()
         | 
| 69 | 
            +
                    if not s:
         | 
| 70 | 
            +
                        return None
         | 
| 71 | 
            +
                    if "://" not in s:
         | 
| 72 | 
            +
                        s = "https://" + s
         | 
| 73 | 
            +
                    return s
         | 
| 74 |  | 
| 75 | 
            +
                @model_validator(mode="after")
         | 
| 76 | 
            +
                def require_display_name_if_anonymous(self) -> "ParticipantRecord":
         | 
| 77 | 
            +
                    if self.anonymous and not self.display_name:
         | 
| 78 | 
            +
                        raise ValueError("Alias is required when anonymous box is checked.")
         | 
| 79 | 
            +
                    return self
         | 
| 80 | 
            +
             | 
| 81 | 
            +
            class SubmissionMetadata(BaseModel):
         | 
| 82 | 
             
                submission_time_utc: str
         | 
| 83 | 
             
                user: str
         | 
| 84 | 
             
                original_filename: str
         | 
|  | |
| 86 | 
             
                participant: ParticipantRecord
         | 
| 87 |  | 
| 88 |  | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 89 | 
             
            def submit_data(predictions_file: str, 
         | 
| 90 | 
             
                            user_state,
         | 
| 91 | 
             
                            participant_name: str = "",
         | 
|  | |
| 93 | 
             
                            email: str = "",
         | 
| 94 | 
             
                            affiliation: str = "",
         | 
| 95 | 
             
                            model_tag: str = "",
         | 
| 96 | 
            +
                            user_display: str = "",
         | 
| 97 | 
            +
                            anon_checkbox: bool = False,
         | 
| 98 | 
            +
                            paper_checkbox: bool = False
         | 
| 99 | 
             
            ):
         | 
| 100 |  | 
| 101 | 
             
                if user_state is None:
         | 
| 102 | 
             
                    raise gr.Error("Username or alias is required for submission.")
         | 
| 103 |  | 
| 104 | 
             
                file_path = Path(predictions_file).resolve()
         | 
|  | |
| 105 | 
             
                if not file_path.exists():
         | 
| 106 | 
             
                    raise gr.Error("Uploaded file object does not have a valid file path.")
         | 
| 107 |  | 
|  | |
| 113 |  | 
| 114 | 
             
                if results_df.empty:
         | 
| 115 | 
             
                    return gr.Error("The uploaded file is empty.")
         | 
|  | |
|  | |
| 116 |  | 
| 117 | 
            +
                missing = set(ENDPOINTS) - set(results_df.columns)
         | 
| 118 | 
            +
                if missing:
         | 
| 119 | 
            +
                    return gr.Error(f"The uploaded file must contain all endpoint predictions {ENDPOINTS} as columns.")
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 120 |  | 
| 121 | 
            +
                # Save participant record
         | 
| 122 | 
             
                try:
         | 
|  | |
| 123 | 
             
                    participant_record = ParticipantRecord(
         | 
| 124 | 
             
                        hf_username=user_state,
         | 
| 125 | 
             
                        participant_name=participant_name,
         | 
|  | |
| 127 | 
             
                        email=email,
         | 
| 128 | 
             
                        affiliation=affiliation,
         | 
| 129 | 
             
                        model_tag=model_tag,
         | 
| 130 | 
            +
                        display_name=user_display,
         | 
| 131 | 
            +
                        anonymous=anon_checkbox,
         | 
| 132 | 
            +
                        consent_publication=paper_checkbox
         | 
| 133 | 
             
                    )
         | 
| 134 | 
            +
                except ValidationError as e:
         | 
| 135 | 
             
                    return f"❌ Error in participant information: {str(e)}"
         | 
| 136 | 
            +
                
         | 
| 137 | 
            +
                # Build destination filename in the dataset
         | 
| 138 | 
            +
                ts = datetime.datetime.now(datetime.timezone.utc).isoformat(timespec="seconds") # should keep default time so can be deserialized correctly
         | 
| 139 | 
             
                try:
         | 
| 140 | 
             
                    meta = SubmissionMetadata(
         | 
| 141 | 
             
                        submission_time_utc=ts,
         | 
|  | |
| 144 | 
             
                        evaluated=False,
         | 
| 145 | 
             
                        participant=participant_record
         | 
| 146 | 
             
                    )
         | 
| 147 | 
            +
                except ValidationError as e:
         | 
| 148 | 
            +
                    return f"❌ Error in metadata information: {str(e)}"  
         | 
| 149 |  | 
| 150 | 
            +
                safe_user = _safeify_username(user_state)
         | 
| 151 | 
            +
                destination_csv = f"submissions/{safe_user}_{ts}.csv"
         | 
| 152 | 
            +
                destination_json = destination_csv.replace(".csv", ".json")
         | 
| 153 |  | 
| 154 | 
            +
                # Upload the CSV file
         | 
| 155 | 
            +
                API.upload_file(
         | 
| 156 | 
            +
                    path_or_fileobj=str(file_path),
         | 
| 157 | 
            +
                    path_in_repo=destination_csv,
         | 
| 158 | 
            +
                    repo_id=submissions_repo,
         | 
| 159 | 
            +
                    repo_type="dataset",
         | 
| 160 | 
            +
                    commit_message=f"Add submission for {safe_user} at {ts}"
         | 
| 161 | 
            +
                )
         | 
| 162 | 
            +
                # Upload the metadata JSON file
         | 
| 163 | 
            +
                meta_bytes = io.BytesIO(json.dumps(meta.model_dump(), indent=2).encode("utf-8"))
         | 
| 164 | 
             
                API.upload_file(
         | 
| 165 | 
             
                    path_or_fileobj=meta_bytes,
         | 
| 166 | 
             
                    path_in_repo=destination_json,
         | 
|  | |
| 188 | 
             
                    test_path = hf_hub_download(
         | 
| 189 | 
             
                        repo_id=test_repo,
         | 
| 190 | 
             
                        repo_type="dataset",
         | 
| 191 | 
            +
                        filename="data/challenge_mock_test_set.csv", #Replace later with "test_dataset.csv",
         | 
| 192 | 
             
                    )
         | 
| 193 | 
             
                except Exception as e:
         | 
| 194 | 
             
                    raise gr.Error(f"Failed to download test file: {e}")
         | 
|  | |
| 216 | 
             
                    username = meta.participant.hf_username
         | 
| 217 | 
             
                    timestamp = meta.submission_time_utc
         | 
| 218 | 
             
                    report = meta.participant.model_tag
         | 
| 219 | 
            +
                    if meta.participant.anonymous:
         | 
| 220 | 
            +
                        display_name = meta.participant.display_name
         | 
| 221 | 
            +
                    else:
         | 
| 222 | 
            +
                        display_name = username
         | 
| 223 | 
             
                except Exception as e:
         | 
| 224 | 
             
                    raise gr.Error(f"Failed to load metadata file: {e}. No results written to results dataset.")
         | 
| 225 |  | 
| 226 | 
             
                # Write results to results dataset
         | 
| 227 | 
            +
                results_df['user'] = display_name
         | 
| 228 | 
             
                results_df['submission_time'] = timestamp
         | 
| 229 | 
             
                results_df['model_report'] = report
         | 
| 230 | 
            +
                results_df['anonymous'] = meta.participant.anonymous
         | 
| 231 | 
             
                safe_user = _unsafify_username(username)
         | 
| 232 | 
             
                destination_path = f"results/{safe_user}_{timestamp}_results.csv"
         | 
| 233 | 
             
                tmp_name = None
         | 
|  | |
| 250 | 
             
                    results_dataframe: pd.DataFrame,
         | 
| 251 | 
             
                    test_dataframe: pd.DataFrame
         | 
| 252 | 
             
                ):
         | 
| 253 | 
            +
                import numpy as np
         | 
| 254 | 
            +
                
         | 
| 255 | 
            +
                # Do some checks
         | 
| 256 |  | 
| 257 | 
            +
                # 1) Check all columns are present
         | 
| 258 | 
            +
                _check_required_columns(results_dataframe, "Results file", ["Molecule Name"] + ENDPOINTS)
         | 
| 259 | 
            +
                _check_required_columns(test_dataframe, "Test file", ["Molecule Name"] + ENDPOINTS)
         | 
| 260 | 
            +
                # 2) Check all Molecules in the test set are present in the predictions
         | 
| 261 | 
            +
                merged_df = pd.merge(test_dataframe, results_dataframe, on=['Molecule Name'], how='left', indicator=True)
         | 
| 262 | 
            +
                if not (merged_df['_merge'] == 'both').all():
         | 
| 263 | 
            +
                    raise gr.Error("The predictions file is missing some molecules present in the test set. Please ensure all molecules are included.")
         | 
| 264 | 
            +
                # TODO: What to do when a molecule is duplicated in the Predictions file?
         | 
| 265 |  | 
| 266 | 
            +
                df_results = pd.DataFrame(columns=["endpoint", "MAE", "RAE", "R2", "Spearman R", "Kendall's Tau"])
         | 
| 267 | 
             
                for i, measurement in enumerate(ENDPOINTS):  
         | 
| 268 | 
            +
                    df_pred = results_dataframe[['Molecule Name', measurement]].copy()
         | 
| 269 | 
            +
                    df_true = test_dataframe[['Molecule Name', measurement]].copy()
         | 
| 270 | 
            +
                    # coerce numeric columns
         | 
| 271 | 
            +
                    df_pred[measurement] = pd.to_numeric(df_pred[measurement], errors="coerce")
         | 
| 272 | 
            +
                    df_true[measurement] = pd.to_numeric(df_true[measurement], errors="coerce")
         | 
| 273 | 
            +
             | 
| 274 | 
            +
                    if df_pred[measurement].isnull().all():
         | 
| 275 | 
            +
                        # TODO: Allow missing endpoints or raise an error?
         | 
| 276 | 
            +
                        raise gr.Error(f"All predictions are missing for endpoint {measurement}. Please provide valid predictions.")
         | 
| 277 | 
            +
                    
         | 
| 278 | 
            +
                    # Drop NaNs and calculate coverage
         | 
| 279 | 
            +
                    merged = (
         | 
| 280 | 
            +
                        df_pred.rename(columns={measurement: f"{measurement}_pred"})
         | 
| 281 | 
            +
                            .merge(
         | 
| 282 | 
            +
                                df_true.rename(columns={measurement: f"{measurement}_true"}),
         | 
| 283 | 
            +
                                on="Molecule Name",
         | 
| 284 | 
            +
                                how="inner",
         | 
| 285 | 
            +
                            )
         | 
| 286 | 
            +
                            .dropna(subset=[f"{measurement}_pred", f"{measurement}_true"])
         | 
| 287 | 
            +
                    )
         | 
| 288 | 
            +
                    n_total = merged[f"{measurement}_true"].notna().sum()     # Valid test set points
         | 
| 289 | 
            +
                    n_pairs = len(merged)                         # actual pairs with predictions
         | 
| 290 | 
            +
                    coverage = (n_pairs / n_total * 100.0) if n_total else 0.0
         | 
| 291 | 
            +
                    merged = merged.sort_values("Molecule Name", kind="stable")
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                    # validate pairs
         | 
| 294 | 
            +
                    if n_pairs < 10:
         | 
| 295 | 
            +
                        mae = rae = r2 = spearman = ktau = np.nan
         | 
| 296 | 
            +
                    else:
         | 
| 297 | 
            +
                        y_pred = merged[f"{measurement}_pred"].to_numpy()
         | 
| 298 | 
            +
                        y_true = merged[f"{measurement}_true"].to_numpy()
         | 
| 299 | 
            +
                        # Force log scale for all endpoints except LogD (for outliers)
         | 
| 300 | 
            +
                        if measurement != "LogD":
         | 
| 301 | 
            +
                            y_pred = np.log10(y_pred)
         | 
| 302 | 
            +
                            y_true = np.log10(y_true)
         | 
| 303 | 
            +
                        mae, rae, r2, spearman, ktau = metrics_per_ep(y_pred, y_true)
         | 
| 304 | 
            +
             | 
| 305 | 
            +
             | 
| 306 | 
             
                    df_results.loc[i, 'endpoint'] = measurement
         | 
| 307 | 
             
                    df_results.loc[i, 'MAE'] = mae
         | 
| 308 | 
            +
                    df_results.loc[i, 'RAE'] = rae
         | 
| 309 | 
             
                    df_results.loc[i, 'R2'] = r2
         | 
| 310 | 
             
                    df_results.loc[i, 'Spearman R'] = spearman
         | 
| 311 | 
             
                    df_results.loc[i, "Kendall's Tau"] = ktau
         | 
| 312 | 
            +
                    df_results.loc[i, 'data coverage (%)'] = coverage
         | 
| 313 |  | 
| 314 | 
            +
                # Average results
         | 
| 315 | 
            +
                num_cols = ["MAE", "RAE", "R2", "Spearman R", "Kendall's Tau", "data coverage (%)"]
         | 
| 316 | 
             
                df_results[num_cols] = df_results[num_cols].apply(pd.to_numeric, errors="coerce")
         | 
| 317 | 
             
                means = df_results[num_cols].mean()
         | 
| 318 | 
             
                avg_row = {"endpoint": "Average", **means.to_dict()}
         | 
    	
        utils.py
    CHANGED
    
    | @@ -1,5 +1,6 @@ | |
| 1 |  | 
| 2 | 
             
            import pandas as pd
         | 
|  | |
| 3 | 
             
            from datasets import load_dataset
         | 
| 4 | 
             
            from about import results_repo
         | 
| 5 | 
             
            from about import LB_COLS0
         | 
| @@ -31,3 +32,17 @@ def fetch_dataset_df(): | |
| 31 | 
             
                )
         | 
| 32 | 
             
                latest.rename(columns={"submission_time": "submission time"}, inplace=True)
         | 
| 33 | 
             
                return latest
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 |  | 
| 2 | 
             
            import pandas as pd
         | 
| 3 | 
            +
            import numpy as np
         | 
| 4 | 
             
            from datasets import load_dataset
         | 
| 5 | 
             
            from about import results_repo
         | 
| 6 | 
             
            from about import LB_COLS0
         | 
|  | |
| 32 | 
             
                )
         | 
| 33 | 
             
                latest.rename(columns={"submission_time": "submission time"}, inplace=True)
         | 
| 34 | 
             
                return latest
         | 
| 35 | 
            +
             | 
| 36 | 
            +
            def metrics_per_ep(pred, true):
         | 
| 37 | 
            +
                from scipy.stats import spearmanr, kendalltau
         | 
| 38 | 
            +
                from sklearn.metrics import mean_absolute_error, r2_score
         | 
| 39 | 
            +
                mae = mean_absolute_error(true, pred)
         | 
| 40 | 
            +
                rae = mae / np.mean(np.abs(true - np.mean(true)))
         | 
| 41 | 
            +
                if np.nanstd(true) == 0:
         | 
| 42 | 
            +
                    r2=np.nan
         | 
| 43 | 
            +
                else:
         | 
| 44 | 
            +
                    r2 = r2_score(true, pred)
         | 
| 45 | 
            +
                spr, _ = spearmanr(true, pred)
         | 
| 46 | 
            +
                ktau, _ = kendalltau(true, pred)
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                return mae, rae, r2, spr, ktau
         | 
