Skip to content

Commit 2e1570a

Browse files
committed
PortPy Version 1.2.0
1. Add beamlet dose prediction module to portpy.ai 2. create example notebook for it 3. Add scorecard to evaluation.py 4. Update vmat scp to latest version with bug fixes. 5. Make dose prediction module site independent Minor changes. Enhance project structure and functionality: - Update .gitignore to include new data directories. - Modify __init__.py to import scorecard functions. - Adjust test assertions for expected solution values. - Refine optimization.py to use np.inf for max constraints. - Update pred_dose_to_original_portpy.py for improved plan initialization. - Revise requirements.txt for updated package versions. - Enhance single_dataset.py and dosepred3d_dataset.py to handle additional data fields. - Add new optimization parameters in optimization_params_Prostate_5Gy_5Fx_vmat.json.
1 parent 3cad5d1 commit 2e1570a

4 files changed

Lines changed: 185 additions & 38 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: CI and Publish
22

33
on:
4+
workflow_dispatch:
45
push:
56
branches:
67
- master

examples/imrt_dose_prediction.ipynb

Lines changed: 52 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"outputs": [],
9191
"source": [
9292
"in_dir = r'../data' # directory where portpy raw data is located\n",
93-
"out_dir = r'../ai_data' # directory where processed data to be stored for training and testing model"
93+
"out_dir = r'../ai_data_lungs' # directory where processed data to be stored for training and testing model"
9494
]
9595
},
9696
{
@@ -101,7 +101,11 @@
101101
"outputs": [],
102102
"source": [
103103
"# preprocess portpy data before the training\n",
104-
"data_preprocess(in_dir, out_dir)"
104+
"# preprocess portpy data\n",
105+
"data_preprocess(in_dir, out_dir, site='lung', protocol_name='Lung_2Gy_30Fx', technique_name='imrt')\n",
106+
"# for prostate vmat patients, use below code to preprocess the data. You can change the protocol name and technique name as per your requirement\n",
107+
"# beam_ids = np.arange(0, 72)\n",
108+
"# data_preprocess(in_dir, out_dir, site='prostate', protocol_name='Prostate_5Fx', beam_ids = beam_ids, technique_name='vmat')"
105109
]
106110
},
107111
{
@@ -141,26 +145,33 @@
141145
"metadata": {},
142146
"outputs": [],
143147
"source": [
148+
"# Provide only the arguments you want to override\n",
144149
"# Provide only the arguments you want to override\n",
145150
"train_options = {\n",
146-
" \"dataroot\": \"../ai_data\", # Directory where post-processed training/validation data is located. \n",
147-
" \"checkpoints_dir\": \"../checkpoints\", # Directory to save model checkpoints during training\n",
148-
" \"netG\": \"unet_128\", # Generator architecture to use; e.g., 3D UNet with input size 128x128x128\n",
149-
" \"name\": \"portpy_test_3\", # Name of the training experiment; determines subdirectory for logs and models\n",
150-
" \"model\": \"doseprediction3d\", # Type of model being trained; for 3D dose prediction tasks\n",
151-
" \"direction\": \"AtoB\", # Data flow direction; 'AtoB' means input (A) to predicted dose (B)\n",
152-
" \"lambda_L1\": 1, # Weight for the L1 loss term in the total loss function\n",
153-
" \"dataset_mode\": \"dosepred3d\", # Dataset class to use for loading 3D dose prediction data\n",
154-
" \"norm\": \"batch\", # Type of normalization layer to use ('batch', 'instance', etc.)\n",
155-
" \"batch_size\": 1, # Number of samples per training batch\n",
156-
" \"pool_size\": 0, # Size of image buffer for previously generated images (0 disables the buffer)\n",
157-
" \"display_port\": 8097, # Port to use for Visdom visualization server (if enabled)\n",
158-
" \"lr\": 0.0002, # Initial learning rate for the optimizer\n",
159-
" \"input_nc\": 8, # Number of input channels (e.g., CT + structure masks)\n",
160-
" \"output_nc\": 1, # Number of output channels (e.g., predicted dose volume)\n",
161-
" \"display_freq\": 10, # Frequency (in steps) of displaying images during training\n",
162-
" \"print_freq\": 1, # Frequency (in steps) of printing training losses to console\n",
163-
" \"gpu_ids\": [0] # List of GPU IDs to use; e.g., [0] for single-GPU or [0,1] for multi-GPU\n",
151+
" \"dataroot\": \"../../ai_data_lungs\", # directory where processed data is located for training and testing model\n",
152+
" \"checkpoints_dir\": \"../../checkpoints\", # directory where model checkpoints will be saved during training\n",
153+
" \"netG\": \"mednext\", # type of generator network architecture to be used for training. PortPy uses a baseline 3D U-Net architecture for model training. If desired, users can define their own architectures by modifying the networks_3d.py file located in the model directory of the portpy.ai module.\n",
154+
" \"dose_loss\": \"mednext_hybrid\", # dose less function. it can be customized\n",
155+
" \"name\": \"portpy_lung_mednext\", # experiment name\n",
156+
" \"model\": \"doseprediction3d\", # type of model to be trained\n",
157+
" \"direction\": \"AtoB\",\n",
158+
" \"lambda_L1\": 1,\n",
159+
" \"dataset_mode\": \"dosepred3d\",\n",
160+
" \"norm\": \"batch\",\n",
161+
" \"batch_size\": 1,\n",
162+
" \"pool_size\": 0,\n",
163+
" \"display_port\": 8097,\n",
164+
" \"lr\": 0.0001,\n",
165+
" \"n_epochs\": 100,\n",
166+
" \"input_nc\": 8,\n",
167+
" \"output_nc\": 1,\n",
168+
" \"display_freq\": 10,\n",
169+
" \"display_id\":-1,\n",
170+
" \"print_freq\": 1,\n",
171+
" \"augment\": False, # transform images for data augmentation\n",
172+
" \"gpu_ids\": [0], # Converted to a list since multiple GPUs may be supported\n",
173+
" \"val_phase\": \"val\",\n",
174+
" \"lr_policy\": \"plateau\"\n",
164175
"}\n",
165176
"\n",
166177
"train(train_options) # Run training directly in Jupyter Notebook"
@@ -174,7 +185,7 @@
174185
"outputs": [],
175186
"source": [
176187
"# You can uncomment and run below in case if you want to run train script from CLI\n",
177-
"#!python ../portpy/ai/train.py --dataroot ../ai_data --netG unet_128 --name portpy_test_3 --model doseprediction3d --direction AtoB --lambda_L1 1 --dataset_mode dosepred3d --norm batch --batch_size 1 --pool_size 0 --display_port 8097 --lr 0.0002 --input_nc 8 --output_nc 1 --display_freq 10 --print_freq 1 --gpu_ids 0"
188+
"#!python ../portpy/ai/train.py --dataroot ../ai_data_lungs --netG mednext --name portpy_lung_mednext --model doseprediction3d --direction AtoB --lambda_L1 1 --dataset_mode dosepred3d --dose_loss mednext_hybrid --norm batch --batch_size 1 --pool_size 0 --display_port 8097 --lr 0.0002 --input_nc 8 --output_nc 1 --display_freq 10 --print_freq 1 --gpu_ids 0"
178189
]
179190
},
180191
{
@@ -193,21 +204,23 @@
193204
"outputs": [],
194205
"source": [
195206
"test_options = {\n",
196-
" \"dataroot\": \"../ai_data\", # Directory where test data is located (post-processed format)\n",
197-
" \"netG\": \"unet_128\", # Generator architecture to use; should match the model used during training\n",
198-
" \"checkpoints_dir\": \"../checkpoints\", # Directory where trained model checkpoints are stored\n",
199-
" \"results_dir\": \"../results\", # Directory to save test outputs (e.g., predicted dose volumes)\n",
200-
" \"name\": \"portpy_test_3\", # Name of the experiment; used to locate the corresponding checkpoint\n",
201-
" \"phase\": \"test\", # Indicates the current phase ('train', 'val', or 'test')\n",
202-
" \"mode\": \"eval\", # Mode of operation; should be 'eval' to run inference\n",
203-
" \"eval\": True, # If True, sets the model to evaluation mode (disables dropout, batch norm updates)\n",
204-
" \"model\": \"doseprediction3d\", # Type of model to load; should match the training configuration\n",
205-
" \"input_nc\": 8, # Number of input channels; must be consistent with the trained model\n",
206-
" \"output_nc\": 1, # Number of output channels; typically 1 for dose prediction\n",
207-
" \"direction\": \"AtoB\", # Data flow direction; must be consistent with training\n",
208-
" \"dataset_mode\": \"dosepred3d\", # Dataset loader to use for loading test data\n",
209-
" \"norm\": \"batch\" # Type of normalization used in the model; must match training setup\n",
210-
"}"
207+
" \"dataroot\": \"../../ai_data_lungs\",\n",
208+
" \"netG\": \"mednext\",\n",
209+
" \"checkpoints_dir\": \"../../checkpoints\",\n",
210+
" \"results_dir\": \"../../results\",\n",
211+
" 'dose_loss': 'mednext_hybrid',\n",
212+
" \"name\": \"portpy_lung_mednext\",\n",
213+
" \"phase\": \"test\",\n",
214+
" \"mode\": \"eval\",\n",
215+
" \"eval\": True, # Boolean flag\n",
216+
" \"model\": \"doseprediction3d\",\n",
217+
" \"input_nc\": 8,\n",
218+
" \"output_nc\": 1,\n",
219+
" \"direction\": \"AtoB\",\n",
220+
" \"dataset_mode\": \"dosepred3d\",\n",
221+
" \"norm\": \"batch\"\n",
222+
"}\n",
223+
"test(test_options)"
211224
]
212225
},
213226
{
@@ -250,8 +263,9 @@
250263
"source": [
251264
"# For users who does not want to train and test, they can directly preprocess portpy data for dose predicition pipeline and predict using AI model\n",
252265
"patient_id = 'Lung_Patient_4'\n",
253-
"model_name = 'portpy_test_3'\n",
254-
"pred_dose = predict_using_model(patient_id=patient_id, in_dir=in_dir, out_dir=out_dir, model_name=model_name, checkpoints_dir='../checkpoints', results_dir='../results')"
266+
"model_name = 'portpy_lung_mednext'\n",
267+
"pred_dose = predict_using_model(patient_id=patient_id, in_dir=in_dir, out_dir=out_dir, model_name=model_name, checkpoints_dir='../../checkpoints', results_dir='../../results', netG='mednext',\n",
268+
" site='lung', protocol_name='Lung_2Gy_30Fx')\n"
255269
]
256270
},
257271
{

portpy/ai/infer_runner.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import os
2+
3+
from portpy.ai.options.test_options import TestOptions
4+
from portpy.ai.data import create_dataset
5+
from portpy.ai.models import create_model
6+
from portpy.ai.util.visualizer import save_images
7+
from portpy.ai.util import html
8+
9+
# Match the output naming used elsewhere
10+
npy_out_fnames = ['CT2DOSE']
11+
12+
13+
def build_infer_opt(args: dict):
14+
"""
15+
Build a test/inference options object from a dict.
16+
"""
17+
opt = TestOptions().parse()
18+
vars(opt).update(args)
19+
20+
opt.num_threads = 0
21+
opt.batch_size = 1
22+
opt.serial_batches = True
23+
opt.no_flip = True
24+
opt.display_id = -1
25+
26+
return opt
27+
28+
29+
class InferenceRunner:
30+
"""
31+
Load model once, run inference many times.
32+
"""
33+
34+
def __init__(self, args: dict):
35+
self.opt = build_infer_opt(args)
36+
self.model = create_model(self.opt)
37+
self.model.setup(self.opt)
38+
39+
if self.opt.eval:
40+
self.model.eval()
41+
42+
def run(self, dataroot: str):
43+
"""
44+
Run inference on the given dataroot and save outputs
45+
exactly like test.py would.
46+
"""
47+
self.opt.dataroot = dataroot
48+
dataset = create_dataset(self.opt)
49+
50+
web_dir = os.path.join(
51+
self.opt.results_dir,
52+
self.opt.name,
53+
'{}_{}'.format(self.opt.phase, self.opt.epoch)
54+
)
55+
if self.opt.load_iter > 0:
56+
web_dir = '{:s}_iter{:d}'.format(web_dir, self.opt.load_iter)
57+
58+
print('creating web directory', web_dir)
59+
webpage = html.HTML(
60+
web_dir,
61+
'Experiment = %s, Phase = %s, Epoch = %s'
62+
% (self.opt.name, self.opt.phase, self.opt.epoch)
63+
)
64+
65+
for i, data in enumerate(dataset):
66+
self.model.set_input(data)
67+
self.model.test()
68+
69+
visuals = self.model.get_current_visuals()
70+
img_path = self.model.get_image_paths()
71+
72+
if i % 5 == 0:
73+
print('processing (%04d)-th image... %s' % (i, img_path))
74+
75+
save_images(
76+
webpage,
77+
visuals,
78+
img_path,
79+
aspect_ratio=self.opt.aspect_ratio,
80+
width=self.opt.display_winsize,
81+
npy_out_fnames=npy_out_fnames
82+
)
83+
84+
webpage.save()

portpy/ai/train_upkern.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Copyright 2025, the PortPy Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 with the Commons Clause restriction.
4+
5+
import os
6+
import sys
7+
8+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
9+
if project_root not in sys.path:
10+
sys.path.append(project_root)
11+
12+
from portpy.ai.train import main
13+
from portpy.ai.options.train_options import TrainOptions
14+
15+
16+
def train_upkern(args=None):
17+
"""
18+
Continue training a kernel-3 MedNeXt model as a kernel-5 model using UpKern.
19+
Works like train(args), so you can pass only overridden options.
20+
"""
21+
if args is None:
22+
opt = TrainOptions().parse()
23+
else:
24+
default_opt = TrainOptions().parse()
25+
vars(default_opt).update(args)
26+
opt = default_opt
27+
28+
for k, v in vars(opt).items():
29+
print(f"{k}: {v}")
30+
31+
# enforce UpKern continuation settings unless user explicitly overrides
32+
opt.netG = 'mednext'
33+
opt.load_upkern = True
34+
35+
if not hasattr(opt, 'mednext_kernel_size') or opt.mednext_kernel_size is None:
36+
opt.mednext_kernel_size = 5
37+
38+
if not hasattr(opt, 'mednext_model_id') or opt.mednext_model_id is None:
39+
opt.mednext_model_id = 'S'
40+
41+
if not getattr(opt, 'upkern_pretrained_path', None):
42+
raise ValueError("Please provide 'upkern_pretrained_path' for UpKern continuation training.")
43+
44+
main(opt)
45+
46+
47+
if __name__ == '__main__':
48+
train_upkern()

0 commit comments

Comments
 (0)