Discord

Checklist NOAI China 2025 Round 2 (China Stage, national final) · Task 2

Chemical Reaction Kinetics Simulation

English title: 化学反应动力学模拟

Predict the half-life of the reactant L2M in an organometallic reaction from the initial concentrations of L2M, D and L.

  • Tabular
  • Regression

The task

The reaction L2M + D ⇌ L2MD is modelled by five elementary reactions (three reversible, two irreversible) that also produce a by-product L2Ms. The statement gives the rate equations and the difference equations for all eight species, and explains that a higher initial concentration of L slows the overall reaction, which is measured by the half-life t1/2 of L2M (the time for c(L2M) to fall to half its initial value).

The training data contain concentration-time traces (recorded every 10.0 s) for 1,000 simulated experiments plus a summary table with initial concentrations, monitoring time, t1/2, reaction extent and yields. For the validation and test experiments only the initial concentrations c(L2M), c(D) and c(L) are given, and t1/2 must be predicted. The hint recommends deriving informed features from the chemistry and using a simple model.

Abridged by SOTA from the official materials. The official statement has the exact rules, and it wins wherever this summary differs.

In English

Some of this task's files were published only in Chinese. SOTA translated that file into English on 17 September 2026. Only the words changed in the notebooks: markdown, code comments, messages and printed output. The code, file names and paths are the original's, so a translated notebook runs with the original data.

Read the baseline notebook in English 46 words and 13 code cells
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
import os
import math
file_name = '/bohr/training-set-qd7r/v3/data_train/exp0_trace.dat'
data = pd.read_csv(file_name)

train, val = train_test_split(data, test_size=0.05, random_state=42)
class CustomDataset(Dataset):
    def __init__(self, initial_c, t12):
        self.input = torch.tensor(initial_c, dtype=torch.float32)
        self.label = torch.tensor(t12, dtype=torch.float32)

    def __len__(self):
        return len(self.label);

    def __getitem__(self, i):
        return self.input[i], self.label[i];

Training phase

input_columns = [1, 2, 3]
initial_c = train.iloc[:, input_columns].values
output_columns = 5
t12_all = train.iloc[:, output_columns].values    

dataset = CustomDataset(initial_c, t12_all);
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
class BaselineModel(nn.Module):
    def __init__(self):
        super(BaselineModel, self).__init__()
        self.fc1 = nn.Linear(3, 12)
        self.fc2 = nn.Linear(12, 24)
        self.fc3 = nn.Linear(24, 36)
        self.fc4 = nn.Linear(36, 1)
    
    def forward(self, x):
        x1 = torch.relu(self.fc1(x))
        x2 = torch.relu(self.fc2(x1))
        x3 = torch.nn.functional.leaky_relu(self.fc3(x2))
        x4 = self.fc4(x3)

        return x4
num_epochs = 20
LR = 0.01
model = BaselineModel()
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=LR)

model.train()
for epoch in range(num_epochs):
    tot_loss = 0
    batch_num = 0
    for input, label in dataloader:
        #print(input)
        #print(label)
        optimizer.zero_grad()
        output = model(input)
        output = torch.squeeze(output)
        loss = criterion(output, label)
        tot_loss += loss
        batch_num += 1
        loss.backward()
        optimizer.step()

    avg_loss = tot_loss / batch_num
    print(f'epoch {epoch}; loss {avg_loss: .4f}')

Validation phase

input_columns = [1, 2, 3]
initial_c = val.iloc[:, input_columns].values
output_columns = 5
t12_all = val.iloc[:, output_columns].values
num = t12_all.size
input = torch.tensor(initial_c, dtype=torch.float32)
t12_act = torch.tensor(t12_all, dtype=torch.float32)
model.eval()
tot_score = 0

with torch.no_grad():
    for i in range(num):
        output = model(input[i])
        pred = output.item()
        act = t12_act[i].item()
        score = max(0, 1 - math.log(1+0.1*abs(pred-act))/5)
        tot_score += score
        
        print(f't1/2 predicted: {pred: .4f}; t1/2 actual: {act: .4f}; score: {score}')

avg_score = tot_score / num
print(f'final score: {avg_score}')

Testing phase

Validation set

Contestants can see the public score on the submission platform

## Obtain the validation and test sets used for the A/B leaderboard evaluation
## [They can only be obtained normally after the notebook has been submitted to the competition]
## [An error during the code-debugging stage is normal, because the data below are not disclosed to contestants and cannot be read at this stage]

if os.environ.get('ANSWER_PATH'):
    PATH = os.environ.get("ANSWER_PATH") + "/" 
else:
    print("When the baseline runs, the test set cannot be read, so errors will follow later; this is normal.")
data_file_name = PATH + 'data_val/val_data_question.dat'
data = pd.read_csv(data_file_name)
input_columns = [1, 2, 3]
initial_c = data.iloc[:, input_columns].values
input_ = torch.tensor(initial_c, dtype=torch.float32)


model.eval()
tot_score = 0
pd_pred = pd.DataFrame(columns = ['Exp #', 't12_simulated'])

with torch.no_grad():
    for i in range(len(initial_c)):
        t12_pred = model(input_[i])
        pred = t12_pred.item()
        pd_pred.loc[i, 'Exp #']= i
        pd_pred.loc[i, 't12_simulated'] = pred


pd_pred['t12_simulated'] = pd_pred['t12_simulated'].apply(lambda x: f"{x:.4e}")
pd_pred.to_csv('submission_val.csv', index=False)
Test set

Contestants cannot access the test set, nor obtain a test-set score after submission The testing sets and its results are made not accessible for contestants.

data_file_name = PATH + 'data_test/test_data_question.dat'
data = pd.read_csv(data_file_name)
input_columns = [1, 2, 3]
initial_c = data.iloc[:, input_columns].values
input_test = torch.tensor(initial_c, dtype=torch.float32)


model.eval()
pd_pred_test = pd.DataFrame(columns = ['Exp #', 't12_simulated'])

with torch.no_grad():
    for i in range(len(initial_c)):
        t12_pred = model(input_test[i])
        pred = t12_pred.item()
        pd_pred_test.loc[i, 'Exp #']= i
        pd_pred_test.loc[i, 't12_simulated'] = pred

pd_pred_test['t12_simulated'] = pd_pred_test['t12_simulated'].apply(lambda x: f"{x:.4e}")
pd_pred_test.to_csv('submission_test.csv', index=False)
import zipfile

# Define the files to be packaged and the compressed file name.
files_to_zip = ['submission_val.csv', 'submission_test.csv']
zip_filename = 'submission.zip'

# Create a zip file.
with zipfile.ZipFile(zip_filename, 'w') as zipf:
    for file in files_to_zip:
        # Add files to the zip file.
        zipf.write(file, os.path.basename(file))

print(f'{zip_filename} is created succefully!')

Translated by SOTA. The Chinese original is the official version and wins wherever the two differ. Translation of the organisers' baseline notebook; bilingual headings and comments keep only their English part. Bohrium's kernel status records were dropped from the saved outputs so that the file is a valid Jupyter notebook; the official English statement is linked on this page. If you organise this olympiad and would like the translation removed, email [email protected] and we will take it down.

At a glance

You get
data_train: exp0_trace.dat-exp999_trace.dat (t, c(L2M), c(D), c(L), c(L2MD), c(L2Ms)) and training_data.dat; validation set of 100 and test set of 412 experiments (initial concentrations only), read through environment variables.
You submit
submission.ipynb (a trained model without the training process is allowed) producing submission.zip with submission_val.csv and submission_test.csv: columns 'Experiment Number' and 't12', with headers, floats in scientific notation with 4 decimal places.
Scoring
Per experiment: Score_i = max(0, 1 - ln(1 + 0.1 * |t_pred - t_true|) / 5); final score = mean over experiments. Leaderboard A: validation set; leaderboard B (final): test set.
Format
NOAI 2025 China Stage final, 2 June 2025: four problems in 6 hours on the Bohrium platform with A/B leaderboards; contestants had unrestricted access to a ChatGPT-4o-level LLM. Republished on Bohrium as the 'NOAI2026 teaching test' / 'APOAI2026 Mock Competition' (practice window 5 Dec 2025 - 20 Jun 2026). Question 2; reference solving time 1-2.5 hours.

Details

Year
2025, Beijing New Talent School, Beijing, China
Round
Round 2 (China Stage, national final) · Task 2
Language
English; English translation by SOTA
License
Not stated by the source