Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

question about the dij structure #477

Open
chh105 opened this issue Jan 11, 2021 · 18 comments
Open

question about the dij structure #477

chh105 opened this issue Jan 11, 2021 · 18 comments

Comments

@chh105
Copy link

chh105 commented Jan 11, 2021

What is the question? Which file(s) does it concern?
I had a question about combining dij structures from two separate forward calculations. For example, say I compute dij for a one beam plan with (couch,gantry) = (0,90) and another dij for a one beam plan with (0,-90). If I wanted to save on compute for a two beam plan with couch angles [0,0] and gantry angles [90,-90], would it be possible to combine the dij structures?

If you can combine dij structures, would I just concatenate the physical dose matrices in the column-wise direction, concatenate the other vectors (i.e. bixelNum, rayNum, beamNum) in the row-wise direction, etc?

I have checked the following matRad Wiki pages and references:
documentation for the dij structure

@markbangert
Copy link
Contributor

In principle you can combine DIJ structures by column-wise concatenation. Thereby you kind of increase the number of bixels. Be careful that you also modify all meta information (numberOfBixels etc.) accordingly if you want to use the matrix for optimization within matRad.

However, I do not really see how this maked sense in the context of a forward calculation. There you can simply add the dose cubes?!

@chh105
Copy link
Author

chh105 commented Jan 11, 2021

Thanks for the quick response. Also sorry if I didnt describe my use case very well, but I'm searching through a bunch of candidate angles and iteratively adding the best angle to create a n-beam plan. Most of the computation time is spent computing dij for each beam, so I was hoping to save and reuse the information

@markbangert
Copy link
Contributor

In case you want to use matRad for beam angle selection you certainly should precompute the Dij matrices and reuse them. Ideally you establish some grid before and then glue them together for the combinations that you want to investigate. For such a use case, however, it might even make sense to write your own backpropagation function because the sparse matrix concatenation will also take quite some time.

Also you may want to check out approaches like https://pubmed.ncbi.nlm.nih.gov/26936695/ to save something on the optimization side.

@chh105
Copy link
Author

chh105 commented Jan 11, 2021

Very interesting. I'll probably start by just concatenating and see if it runs fast enough. For the optimization part, I've just been running IPOPT for ~15 iterations which finishes relatively quickly (I think this is similar to the early stopping method in your paper). I've also been using an objective function with a squared deviation for the ptv and squared overdosing to the body (with equal weights). I was wondering if you tried various objective functions besides squared over/under dosing and whether there was much difference between the ones you tried.

@markbangert
Copy link
Contributor

No we have not experimented with different obj functions. I have investigated different methods on the beam angle selection problem itself, though. Iterative adding, however, was pretty much clinically equivalent.

https://aapm.onlinelibrary.wiley.com/doi/abs/10.1118/1.4771932

@chh105
Copy link
Author

chh105 commented Jan 11, 2021

Ok I'll probably just stick with the current optimization approach then. Anyways thanks for your help!

@chh105
Copy link
Author

chh105 commented Jan 14, 2021

Just had a follow up question about combining physicalDose matrices. Say we compute two physicalDose matrices for (couch=0, gantry=90) and (couch=0, gantry=-90) and combine them by concatenating column-wise. We can also compute a physicalDose matrix for (couch=[0,0], gantry=90,-90). I would expect these two matrices to be the same (isEqual returns 1), but it seems that they are very slightly different (isEqual returns 0 and the number of nonzero terms in the physicalDose matrices differs by around 100). Is this acceptable/expected behavior?

@markbangert
Copy link
Contributor

markbangert commented Jan 14, 2021 via email

@chh105
Copy link
Author

chh105 commented Jan 14, 2021

I think the pln and stf structures should be unmodified. To reproduce this manually, I setup everything in the GUI, computed three dij structures, and then compared the two physicalDose matrices (one where things are combined and one copied from the dij structure). I'm wondering if maybe I overlooked something

@chh105
Copy link
Author

chh105 commented Jan 14, 2021

Edit: Actually sorry after checking again, I think the pln and stf structures are being updated by the gui. I think this might be causing the isocenter to be different for the precomputed and regular dij calculations. Just so to clarify, should the couch/gantry angles be different for the precomputed/regular dij calculations while the isocenter remains the same?

@chh105
Copy link
Author

chh105 commented Jan 14, 2021

I think I'm just making this more confusing with my comments, so I wrote a script to reproduce things.

gantry = [90 -90];
couch = [0 0];
pln.propStf.gantryAngles = gantry;
pln.propStf.couchAngles = couch;
pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles);
pln.propStf.isoCenter = ones(pln.propStf.numOfBeams,1) * matRad_getIsoCenter(cst,ct,0);
stf = matRad_generateStf(ct,cst,pln);

% compute dij for first angle
pln.propStf.gantryAngles = gantry(1);
pln.propStf.couchAngles = couch(1);
pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles);
stf = matRad_generateStf(ct,cst,pln);
if strcmp(pln.radiationMode,'photons')
    dij_1 = matRad_calcPhotonDose(ct,stf,pln,cst);
    %dij = matRad_calcPhotonDoseVmc(ct,stf,pln,cst);
elseif strcmp(pln.radiationMode,'protons') || strcmp(pln.radiationMode,'carbon')
    dij_1 = matRad_calcParticleDose(ct,stf,pln,cst);
end

% compute dij for second angle
pln.propStf.gantryAngles = gantry(2);
pln.propStf.couchAngles = couch(2);
pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles);
stf = matRad_generateStf(ct,cst,pln);
if strcmp(pln.radiationMode,'photons')
    dij_2 = matRad_calcPhotonDose(ct,stf,pln,cst);
    %dij = matRad_calcPhotonDoseVmc(ct,stf,pln,cst);
elseif strcmp(pln.radiationMode,'protons') || strcmp(pln.radiationMode,'carbon')
    dij_2 = matRad_calcParticleDose(ct,stf,pln,cst);
end

% compute dij for 2 beam config
pln.propStf.gantryAngles = gantry;
pln.propStf.couchAngles = couch;
pln.propStf.numOfBeams = numel(pln.propStf.gantryAngles);
stf = matRad_generateStf(ct,cst,pln);
if strcmp(pln.radiationMode,'photons')
    dij = matRad_calcPhotonDose(ct,stf,pln,cst);
    %dij = matRad_calcPhotonDoseVmc(ct,stf,pln,cst);
elseif strcmp(pln.radiationMode,'protons') || strcmp(pln.radiationMode,'carbon')
    dij = matRad_calcParticleDose(ct,stf,pln,cst);
end

% precomputed dose mat
dose_mat = [];
dose_mat = [dose_mat,dij_1.physicalDose{1}];
dose_mat = [dose_mat,dij_2.physicalDose{1}];
% regular dose mat
regular_dose_mat = dij.physicalDose{1};

%% comparisons
subplot(2,1,1)
spy(dose_mat)
subplot(2,1,2)
spy(regular_dose_mat)
sprintf('NNZ precomputed:%d',nnz(dose_mat))
sprintf('NNZ regular:%d',nnz(regular_dose_mat))
diff = full(sum(dose_mat(:)-regular_dose_mat(:))^2/sum(dose_mat(:))^2);
sprintf('Diff:%e',diff)

@markbangert
Copy link
Contributor

markbangert commented Jan 25, 2021 via email

@chh105
Copy link
Author

chh105 commented Jan 25, 2021

Yes I still haven't figured it out. There seems to be a small difference between the concatenated physical dose matrices and the regular physical dose matrix.

@markbangert
Copy link
Contributor

Thank you for providing the script. The behaviour you observe is related to the dose influence matrix sampling. In order to save memory we perform a random sampling of the influence data in

[ix,bixelDose] = matRad_DijSampling(ix,bixelDose,radDepthVdoseGrid{1}(ix),rad_distancesSq,Type,r0);
. As we are re-setting the state of the random engine only in the beginning of the entire dose calculation at
rng('default');
we have the funny behaviour that the first beams are binary identical while the second beam differs. If you want to make this consistent you can reset the random engine before the calculation of every beam. @wahln what do you think about including this for the main release? I am somewhat hesitant because resetting the random engine is always fishy with regard to statistical independence... But we could even think about setting the random engine based on a hash computed on the index set of the beamlets within the dij sampling function itself. That way we would even have reproducability for individual bixels given they have the exact same geometry...

@markbangert markbangert reopened this Jan 26, 2021
@chh105
Copy link
Author

chh105 commented Jan 26, 2021

Thanks for figuring this out so quickly. Just to clarify, are you saying that the random engine needs to be reset for each beam when computing dij for a multiple beam plan? For example, would you paste rng('default'); after the for loop call in line 143 (for i = 1:dij.numOfBeams % loop over all beams)?

@markbangert
Copy link
Contributor

markbangert commented Jan 26, 2021 via email

@wahln
Copy link
Contributor

wahln commented Jan 27, 2021

Nice that you guys figured that out. I think we should allow some configuration of the sampling that enforces reproducible / consistent results. I will think about some configuration parameters in pln.propDoseCalc that allow people to enable/disable & steer the sampling process / set seeds & choose seed mechanic (per beam etc.).

@stale
Copy link

stale bot commented Feb 10, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale Automatic label for stale issues label Feb 10, 2021
@wahln wahln added enhancement and removed stale Automatic label for stale issues labels Feb 15, 2021
@wahln wahln self-assigned this May 12, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants