-
Notifications
You must be signed in to change notification settings - Fork 2
Added dictionary learning (DL) functionality to LSQML, cleaned up tensor operations in RPIE + DL #42
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
base: main
Are you sure you want to change the base?
Conversation
optimal step for sparse code update using uncoupled object and probe step calculation. after that, do the coupled step length calc
changes to rPIE and synthesissparseprobe class with cleaner tensor products using torch.einsum
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This pull request adds dictionary learning (DL) functionality to the LSQML reconstructor and refactors tensor operations in both PIE and LSQML reconstructors using torch.einsum for improved clarity and efficiency. The implementation focuses on independent step length calculation for sample and sparse code update directions.
- Added complete dictionary learning support to LSQML with optimal step size calculation for sparse code updates
- Refactored tensor operations in PIE reconstructor using
torch.einsumfor better readability and performance - Added configuration option for averaging sparse codes over scan positions
Reviewed Changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ptychi/reconstructors/pie.py | Refactored sparse code update calculations using torch.einsum and improved tensor reshaping logic |
| src/ptychi/reconstructors/lsqml.py | Added complete dictionary learning functionality with optimal step length calculation for sparse code updates |
| src/ptychi/data_structures/probe.py | Updated sparse code weight computation and probe generation to use torch.einsum operations |
| src/ptychi/api/task.py | Extended probe building logic to support LSQML with dictionary learning options |
| src/ptychi/api/options/lsqml.py | Added experimental options structure for LSQML probe configuration |
| src/ptychi/api/options/base.py | Added use_avg_spos_sparse_code configuration option for sparse code averaging behavior |
src/ptychi/reconstructors/lsqml.py
Outdated
| # if self.parameter_group.probe.use_avg_spos_sparse_code: | ||
| # delta_p_i = torch.tile( delta_p_i, ( n_spos, 1, 1 ) ) |
Copilot
AI
Aug 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This commented-out code should either be implemented or removed. Leaving commented code in production reduces maintainability and clarity.
| # if self.parameter_group.probe.use_avg_spos_sparse_code: | |
| # delta_p_i = torch.tile( delta_p_i, ( n_spos, 1, 1 ) ) |
| sel = sparse_code_sorted[0][self.parameter_group.probe.probe_sparse_code_nnz, :] | ||
| sel = sparse_code_sorted[0][..., self.parameter_group.probe.probe_sparse_code_nnz] | ||
|
|
||
| #(TODO: soft thresholding option as default?) |
Copilot
AI
Aug 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO comments should include more specific information about implementation timeline, requirements, or be converted to proper issue tracking.
| #(TODO: soft thresholding option as default?) | |
| # TODO: Consider implementing soft thresholding as the default option for enforcing sparsity. | |
| # See issue #123 on GitHub (https://github.com/AdvancedPhotonSource/pty-chi/issues/123) for requirements and discussion. |
| if (self.parameter_group.probe.representation == "sparse_code"): | ||
| # TODO: move this into SynthesisDictLearnProbe class | ||
|
|
||
| # TODO: move these into SynthesisDictLearnProbe class |
Copilot
AI
Aug 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO comments should include more specific information about implementation timeline, requirements, or be converted to proper issue tracking.
| # TODO: move these into SynthesisDictLearnProbe class | |
| # TODO: Move these calculations into SynthesisDictLearnProbe class for better modularity. | |
| # See issue tracker: https://github.com/AdvancedPhotonSource/pty-chi/issues/XXX | |
| # Target: Refactor by Q3 2025. Requirements: Move rc, n_scpm, n_spos, obj_patches_conj, conjT_i_delta_exwv_i, and related logic. |
src/ptychi/reconstructors/lsqml.py
Outdated
| torch.conj( obj_patches_vec ), | ||
| numer) | ||
|
|
||
| # real is used to throw away small imag part due to numerical precision errors |
Copilot
AI
Aug 5, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment about using .real to handle numerical precision errors should be more specific about when this occurs and potential alternatives.
| # real is used to throw away small imag part due to numerical precision errors | |
| # In theory, numer/denom should be real, but small imaginary parts can arise due to floating-point | |
| # precision errors in complex arithmetic. We use .real to discard these. Alternatively, one could use | |
| # torch.real_if_close or check that the imaginary part is negligible before discarding it. |
define a separate representation = 'sparse_code' to the OPR options since I'm using the shared probe update representation currently.
| chi: Tensor, | ||
| delta_p_i: Tensor, | ||
| delta_p_hat: Tensor, | ||
| probe_current_slice: Tensor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make this argument optional since it is only used by SDL. Default to None:
probe_current_slice: Optional[Tensor] = None
| chi: Tensor, | ||
| delta_p_i: Tensor, | ||
| delta_p_hat: Tensor, | ||
| probe_current_slice: Tensor, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be an optional argument too
| if batch_size == 1: | ||
| return | ||
|
|
||
| update_eigenmode = probe.optimization_enabled(current_epoch) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not put this check here. The parent function update_variable_probe already checks it. update_opr_probe_modes_and_weights is only called when probe.optimization_enabled() returns True.
|
|
||
| if (probe.representation == "sparse_code") and update_eigenmode: | ||
|
|
||
| sz = delta_p_i.shape |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make things inside the if block a separate method, something like update_opr_mode_sparse_code, and move it to SynthesisDictLearnProbe, then just call probe.update_opr_mode_sparse_code(...) here.
|
|
||
| probe_current_slice_vec = torch.reshape( probe_current_slice[:,0,...], (Nspos, rc) ).T | ||
|
|
||
| #================================================================================== |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please keep comments succinct; avoid using ASCII bars to divide segments. Major segments in a code block should be separated into different child functions/methods instead of by bars.
|
|
||
| if probe.optimization_enabled(current_epoch): | ||
| probe.set_data(probe_data) | ||
| if probe.optimization_enabled(current_epoch): # and not (probe.representation == "sparse_code") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to set it again, so you can uncomment the and not
src/ptychi/data_structures/probe.py
Outdated
| sparse_code_probe = self.get_sparse_code_weights() | ||
| self.register_parameter("sparse_code_probe", torch.nn.Parameter(sparse_code_probe)) | ||
|
|
||
| use_avg_spos_sparse_code = self.options.experimental.sdl_probe_options.use_avg_spos_sparse_code |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's just a boolean you don't have to register it as buffer. Also, instead of setting it as an attribute in __init__, let's just reference it from self.options on the fly in the method where it is used. This way if the user changes the value in the options object in the middle of a reconstruction, the new value can take effect dynamically.
src/ptychi/reconstructors/lsqml.py
Outdated
| ) | ||
| if (self.parameter_group.probe.representation == "sparse_code"): | ||
|
|
||
| rc = chi.shape[-1] * chi.shape[-2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd also make everything side the if a separate method and call it here. Also avoid using ASCII dividers.
If the routines for sparse code update is the same across all reconstructors or if there is at least something common between them, please put the common parts in SynthesisDictLearnProbe.
src/ptychi/reconstructors/lsqml.py
Outdated
| #====================================================================== | ||
| # sparse code update directions vs scan position and shared probe modes | ||
|
|
||
| obj_patches_slice_i_conj = torch.conj( obj_patches[:, i_slice, ...] ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove extra space in the brackets. Make code more compact by removing unnecessary blank spaces.
|
|
||
| if (self.parameter_group.probe.representation == "sparse_code"): | ||
| # TODO: move this into SynthesisDictLearnProbe class | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separate method for all everything inside the if. If the routines for sparse code update is the same across all reconstructors or if there is at least something common between them, please put the common parts in SynthesisDictLearnProbe.
| # Start from the second OPR mode which is the first after the main mode - i.e., the first eigenmode. | ||
| for i_opr_mode in range(1, probe.n_opr_modes): | ||
| # Just take the first incoherent mode. | ||
| eigenmode_i = probe.get_mode_and_opr_mode(mode=0, opr_mode=i_opr_mode) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here you can regenerate the eigenmodes using update SDL coefficients (probe.generate() to update the probe.data attribute) so that you can reuse the existing code
| update_eigenmode = probe.optimization_enabled(current_epoch) # why is this needed again? To even get into this function, we need this to already be true? | ||
| update_eigenmode_weights = self.eigenmode_weight_optimization_enabled(current_epoch) | ||
|
|
||
| if self.options.use_optimal_update: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Split SDL and optimal OPR weight and eigenmode updates into 2 pull requests
| Ws = (weights_data[ indices, 1:]).to(torch.complex64) | ||
|
|
||
| Tsconj_chi = (obj_patches[:,0,...].conj() * chi[:,0,...]) | ||
| Tsconj_chi = adjoint_shift_probe_update_direction( indices, Tsconj_chi[:,None,...], first_mode_only=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one should be replaceable by delta_p_i
| Tsconj_chi = (obj_patches[:,0,...].conj() * chi[:,0,...]) | ||
| Tsconj_chi = adjoint_shift_probe_update_direction( indices, Tsconj_chi[:,None,...], first_mode_only=True) | ||
|
|
||
| chi = adjoint_shift_probe_update_direction( indices, chi, first_mode_only=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably ignorable because we have been mixing shifted and unshifted variables before and it works. In that case the adjoint_shift_probe_update argument can be removed
Features/fixes
Added dictionary learning (DL) functionality to LSQML, and cleaned up the rPIE + DL calculations for tensor products using torch.einsum. This pull request is only for independent step length calculation for the sample and sparse code update directions; the joint solution for step length calculation for the sample and sparse code update directions is coming soon.
Related issues (optional)
n/s
Mentions
Checklist
Have you...
Formatted your code properly adhering to PEP-8? Considering using RUFF to format your code automatically.
Nope, need to finally learn how to do this
Resolved all merge conflicts with the main branch?
[ ]
Checked the diffs between your branch and the main branch to ensure that your changes are not introducing any regressions or unnecessary/irrelevant changes?