Refactoring training and inference script to match source code changes#44
Conversation
There was a problem hiding this comment.
Pull request overview
This PR refactors the training and inference entrypoint scripts to align CLI/config handling with recent model/dataset evolution (encoder selection via --encoder_type, expanded dataset filtering knobs, scheduler configuration, and saving a resolved encoder config for inference compatibility).
Changes:
- Replaces
--use_slaewith--encoder_type {gvp,slae,esm}and adds helpers to resolve encoder config/dimensions from a sample batch. - Adds dataset quality + per-water filtering CLI args and logs the active/ignored filters into
config.json. - Adds gradient accumulation and warmup+main scheduler support, and updates inference to prefer
resolved_encoder_configplus configurable geometry cache selection.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 10 comments.
| File | Description |
|---|---|
scripts/train.py |
Refactors CLI/config, adds dataset filter knobs, encoder resolution, gradient accumulation, warmup + main schedulers, and richer checkpoint/config saving. |
scripts/inference.py |
Updates config/model build logic to use resolved encoder config when present, adds geometry cache override, and passes encoder/cache settings into dataset creation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def main(): | ||
| """Run inference pipeline on a list of PDB structures.""" | ||
| setup_logging_for_tqdm() | ||
| args = parse_args() |
There was a problem hiding this comment.
setup_logging_for_tqdm() is already called at module import time, and it’s called again here in main(). This can lead to duplicate handlers or unexpected log formatting depending on implementation. Prefer initializing logging in one place (typically inside main() only) and removing the module-level side effect.
| train_loader = get_dataloader( | ||
| args.train_list, args.processed_dir, | ||
| batch_size=args.batch_size, shuffle=True, | ||
| pdb_list_file=args.train_list, | ||
| processed_dir=args.processed_dir, | ||
| batch_size=args.batch_size, | ||
| shuffle=True, | ||
| num_workers=args.num_workers, | ||
| base_pdb_dir=args.base_pdb_dir, | ||
| include_mates=args.include_mates, | ||
| pin_memory=args.pin_memory, | ||
| prefetch_factor=args.prefetch_factor, | ||
| persistent_workers=args.persistent_workers, | ||
| duplicate_single_sample=args.duplicate_single_sample, | ||
| **dataset_kwargs, |
There was a problem hiding this comment.
get_dataloader() in src/dataset.py does not accept prefetch_factor/persistent_workers and will forward them into ProteinWaterDataset(**dataset_kwargs), where they are not valid constructor args either. This will raise a TypeError at runtime. Either update src.dataset.get_dataloader to accept these DataLoader-only kwargs and pass them into torch.utils.data.DataLoader, or stop passing them here until the dataset/loader APIs support them.
| # Handle remaining gradients at end of epoch | ||
| if (step + 1) % args.grad_accum_steps != 0: | ||
| if args.grad_clip > 0: |
There was a problem hiding this comment.
train_epoch() uses step after the dataloader loop to decide whether to flush remaining gradients. If train_loader is empty, step is never set and this will raise UnboundLocalError. Consider handling the empty-loader case explicitly (e.g., initialize step = -1 before the loop or early-return when len(train_loader) == 0).
| encoder_type=encoder_type, | ||
| include_mates=include_mates, | ||
| geometry_cache_name=geometry_cache_name, |
There was a problem hiding this comment.
ProteinWaterDataset.__init__ in src/dataset.py does not currently accept encoder_type or geometry_cache_name. Passing these here will raise a TypeError. Either extend ProteinWaterDataset to support these options, or remove them from the constructor call and rely on the dataset’s existing parameters until the underlying dataset API is updated.
| encoder_type=encoder_type, | |
| include_mates=include_mates, | |
| geometry_cache_name=geometry_cache_name, | |
| include_mates=include_mates, |
| val_loader = get_dataloader( | ||
| args.val_list, args.processed_dir, | ||
| batch_size=args.batch_size, shuffle=False, | ||
| pdb_list_file=args.val_list, | ||
| processed_dir=args.processed_dir, | ||
| batch_size=args.batch_size, | ||
| shuffle=False, | ||
| num_workers=args.num_workers, | ||
| base_pdb_dir=args.base_pdb_dir, | ||
| include_mates=args.include_mates, | ||
| duplicate_single_sample=1, # Don't duplicate for validation | ||
| pin_memory=args.pin_memory, | ||
| prefetch_factor=args.prefetch_factor, | ||
| persistent_workers=args.persistent_workers, | ||
| duplicate_single_sample=args.duplicate_single_sample, | ||
| **dataset_kwargs, | ||
| ) |
There was a problem hiding this comment.
dataset_kwargs includes encoder_type and geometry_cache_name, but ProteinWaterDataset.__init__ (in src/dataset.py) currently does not accept those parameters. As written, get_dataloader(..., **dataset_kwargs) will error when constructing the dataset. Either extend ProteinWaterDataset to accept/use these fields, or remove them from dataset_kwargs until the core dataset supports encoder/geometry cache selection.
| pin_memory=args.pin_memory, | ||
| prefetch_factor=args.prefetch_factor, | ||
| persistent_workers=args.persistent_workers, | ||
| duplicate_single_sample=args.duplicate_single_sample, |
There was a problem hiding this comment.
Validation loader is now using duplicate_single_sample=args.duplicate_single_sample, which will duplicate the validation set when running single-sample overfitting experiments and can artificially inflate/alter validation metrics. Consider forcing duplicate_single_sample=1 for validation (as was done previously) so eval reflects the true dataset size.
| duplicate_single_sample=args.duplicate_single_sample, | |
| duplicate_single_sample=1, |
| n_message_gvps=args.n_message_gvps, | ||
| n_update_gvps=args.n_update_gvps, |
There was a problem hiding this comment.
FlowWaterGVP (in src/flow.py) does not currently accept n_message_gvps or n_update_gvps keyword arguments, so this constructor call will raise TypeError: __init__() got an unexpected keyword argument .... Either plumb these through FlowWaterGVP/ProteinWaterUpdate (and into GVPMultiEdgeConv) or remove these arguments from the script until the model supports them.
| n_message_gvps=args.n_message_gvps, | |
| n_update_gvps=args.n_update_gvps, |
| metrics = flow_matcher.training_step( | ||
| batch, optimizer, | ||
| grad_clip=args.grad_clip, | ||
| batch, | ||
| use_self_conditioning=args.use_self_cond, | ||
| accumulation_steps=args.grad_accum_steps, | ||
| ) |
There was a problem hiding this comment.
FlowMatcher.training_step() in src/flow.py currently requires an optimizer argument and performs optimizer.zero_grad() / backward() / step() internally. This call now omits the optimizer and passes accumulation_steps, which is not a supported parameter, so training will fail with a TypeError (and gradient accumulation logic would also double-step if both places manage stepping). To fix: either update FlowMatcher.training_step to support gradient accumulation (accept accumulation_steps, do scaled loss/backward only, and let the outer loop step), or revert this call to the existing API and keep optimizer stepping inside FlowMatcher.
| from loguru import logger | ||
| from tqdm import tqdm | ||
|
|
||
| from src.constants import NUM_RBF |
There was a problem hiding this comment.
src/constants.py does not define NUM_RBF, so from src.constants import NUM_RBF will fail at import time. Either add NUM_RBF to src/constants.py (and keep it consistent with the model’s expected RBF dimension) or stop importing it here and use the correct existing constant/config key.
| from src.constants import NUM_RBF |
| n_message_gvps=config.get("n_message_gvps", 2), | ||
| n_update_gvps=config.get("n_update_gvps", 2), |
There was a problem hiding this comment.
FlowWaterGVP.__init__ (in src/flow.py) does not currently accept n_message_gvps / n_update_gvps keyword args, so this call will raise TypeError. If you intend these to be configurable, the parameters need to be added/plumbed through the model implementation; otherwise remove them from inference model construction to match the actual FlowWaterGVP signature.
| n_message_gvps=config.get("n_message_gvps", 2), | |
| n_update_gvps=config.get("n_update_gvps", 2), |
| # build result dicts similar to rk4 | ||
| results = [] | ||
| for graph, water_pred in zip(graphs, water_preds): | ||
| results.append({ | ||
| "protein_pos": graph["protein"].pos.numpy(), | ||
| "water_true": graph["water"].pos.numpy(), | ||
| "water_pred": water_pred, | ||
| "trajectory": None, | ||
| "pdb_id": getattr(graph, 'pdb_id', None), | ||
| }) | ||
| results.append( | ||
| { | ||
| "protein_pos": graph["protein"].pos.numpy(), | ||
| "water_true": graph["water"].pos.numpy(), | ||
| "water_pred": water_pred, | ||
| "trajectory": None, | ||
| "pdb_id": getattr(graph, "pdb_id", None), | ||
| } | ||
| ) |
There was a problem hiding this comment.
why is this dict building here rather than inside euler_integration function?
There was a problem hiding this comment.
A more detailed/updated explanation would be helpful. I had to go to PR #43 to see the difference between processed_dir and geometry_cache
There was a problem hiding this comment.
Added more detail to the help message
There was a problem hiding this comment.
I would assume all the metrics to compute that rely on ground truth comparison are technically not part of the inference logic. If there is no ground truth or one thinks the deposited info is wrong, your script should still be able to make a prediction of water placement?
There was a problem hiding this comment.
Added a skip_metrics flag for the case one does not have/does not want to use ground truth data. In the future I would assume something cleaner with config files.
| # TODO: Remove hardcoded default paths. These should be required arguments | ||
| # or loaded from environment variables / config files for portability. | ||
| # Current hardcoded paths: | ||
| # - processed_dir: /home/srivasv/flow_cache/ | ||
| # - base_pdb_dir: /sb/wankowicz_lab/data/srivasv/pdb_redo_data | ||
| # - edia_dir: /sb/wankowicz_lab/data/srivasv/edia_results | ||
| # - save_dir: /home/srivasv/flow_checkpoints | ||
| # - wandb_dir: /home/srivasv/wandb_logs |
| # TODO: Add support for loading configuration from YAML/JSON config files. | ||
| # This would allow users to save and share training configurations easily. | ||
| # Example: --config config.yaml would load all arguments from the file, | ||
| # with CLI args taking precedence for overrides. |
| pred=out['water_pred'], | ||
| true=out['water_true'], | ||
| threshold=1.0 | ||
| pred=out["water_pred"], true=out["water_true"], threshold=1.0 |
There was a problem hiding this comment.
should probably expose threshold as an argument, same as in inference.py?
There was a problem hiding this comment.
Yep, addressing this
| p.add_argument( | ||
| "--no_persistent_workers", | ||
| dest="persistent_workers", | ||
| action="store_false", | ||
| help="Disable persistent_workers", | ||
| ) |
There was a problem hiding this comment.
I don't think you need these kind of paired flags anymore. You can just do p.add_argument("--persistent_workers", action="store_true") and don't set a default value and it should work fine. similarly for pin_memory.
There was a problem hiding this comment.
Yep, simplifying this
| device=device, | ||
| water_ratio=water_ratio, | ||
| ) | ||
| # build result dicts similar to rk4 |
Changes
scripts/train.py
scripts/inference.py (+185 lines)
Note: A lot of the changes here are conditioned on changes in the source code that are part of other PRs