Add AbstractDoseEstimation - #1643
Conversation
This HardwareObject is meant to provide a way to estimate absorbed dose by crystal during oscillation scans.
Implementation of DoseEstimator HardwareObject for BioMAX. Calculations inside the module have been based on the old dose estimation code used at BioMAX and is a subject to change in the future. It's correctness is not guaranteed, nonetheless it serves as a reference implementation to iterate on afterwards.
This object does not output probable estimates, it's purpose is to make sure that dose estimation works on the demo.yaml beamline. It also serves as an example for using pydantic models for config validation.
There was a problem hiding this comment.
Hmmm. This may be mostly a question of style, but I would not be that happy having to write an implementation to this abstract class. Could we discuss this?
Mainly I think that this very much exaggerates using Pydantic. First of all DoseEstimateParameters. The normal Python mechanism is that function parameters are defined in the function definition. Here they are defined in a Pydantic class, in a different file. Does that not obfuscate more than it helps? There are cases where data objects make sense, e.g. for the data needed to define an acquisition, that have some kind of independent objecthood and that are used in multiple contexts. But these are (just look at the name) defined simply by being the input parameters to a function. Why the extra packaging?
Similarly the various classes for DoseEstimation results and goals. What is gained by this rather cumbersome modelling just to group a few attributes together?
Are we aiming for a situation where the input and output of every function in MXCuBE is separately defined as a Pydantic object in a separate file? Is that really a good idea? And if that is not what we want, how do you decide which contexts can make do with standard Python, and which need the all-Pydantic treatment? I am generally in favour of data modelling, but part of the cost is that it takes more thought and work to set up a comprehensive model up front, and leads to more rigid code. What is the gain, here?
The Pydantic model is used in a few places, one of them being the backend adapter, which essentially calls the Using the same object in It also occurs to me now that the name The Pydantic models could be removed from |
|
@mockoocy OK, that is a good explanation. We can still discuss how much the front end should influence the back end (and I still have my opinions, no surprise there) but there are certainly good reasons on both sides. Can we see what the others say, before this is merged? Meanwhile, I will remove my 'modifications required', so as not to be technically blocking. |
I still think this should be discussed more before being merged (and my opinion is the same as in the previous review), but there are good arguments on both sides, so in the absence of a specific change I would rather not stand as blocking further developments.
| from mxcubecore.BaseHardwareObjects import HardwareObject | ||
|
|
||
|
|
||
| class DoseEstimationOk(BaseModel): |
There was a problem hiding this comment.
Would it be possible to merge DoseEstimationOk and DoseEstimationError to simply DoseEstimation containing i.e status, msg, dose_mgy, max_images, the later two being None incase of error ? Or containing them within a field called result ?
There was a problem hiding this comment.
I think I would just raise some named exception (e.g. DoseEstimationError) here and have a proper field with no None values there. Thus having just one return type (I'd make it a dataclass probably or a TypedDict for the sake of readability).
|
|
||
| def estimate_dose( # noqa: PLR0911 many returns make it clearer :) | ||
| self, | ||
| params: DoseEstimateParameters, |
There was a problem hiding this comment.
I would agree with @rhfogh for params, is there a reason for not simply defining the method with all the arguments needed ?
There was a problem hiding this comment.
Just convenience when writing code for the web frontend, I will just use bunch of kwargs there, someting like
- def estimate_dose(self, params: DoseEstimateParameters): ...
+ def estimate_dose(self, *, exp_time_s: float, num_images: int, ...): ...| beamline_flux_density = self._flux_hwo.flux_density | ||
|
|
||
| if beamline_flux_density == -1: | ||
| return DoseEstimationError( |
There was a problem hiding this comment.
I'm not against using DoeseEsitmationError in this way, it can be an elegant way to handle an expected error condition. We tend to use exceptions for both "unexpected" and expected error states. Why did you chose to with dose DoeseEsitmationError ?
If you would like to distinguish between exception and an "expected" error. I would merge, as I mentioned above, DoseEstimationOk and DoseEstimationError.
Otherwise the easiest would perhaps be to raise and let the calling code handle the error ?
There was a problem hiding this comment.
I will just go with something proposed in #1643 (comment) and raise indeed. It is a bit more "python-like" :), I will move the web niceties to mxcubeweb.
|
I understand the idea of using the This otherwise looks great to me :), @rhfogh, is there otherwise anything else in the structure that you would like to discuss. From what I see, I think we can merge this if the |
|
If the pydantic stuff is moved to mxcubeweb Iit is all fine with me. Not that I really like the style, but the web front end has its own legitimate requirements. |
|
Sorry, coming a bit late in the discussion (long holidays). In order to agree on the parameters and what comes from the form, hence the pydantic model, could we narrow down the number of parameters. It seems to me that we can estimate the dose, based on the dose rate and the total exposure time. Is this too simplistic? |
|
Could we integrate this with the dose rate PR, #1619. I particularly would point to the conceptual organisation proposed in recent comments. |
|
Hi all,
I suppose it could be up to facility to use
Yes, in principle, given dose rate it is quite simple to get the proper dose estimation later on. For example, BioMAX implementation tries to estimate flux at energy level provided by the user. To do so it calculates a relative error term between a real flux value and one coming from some cubic regression. This is then used to infer what would be the flux quantity given user-supplied energy. I see a reason that we could perhaps just operate on beamline values and just prohibit dose estimation when user requests different energy than the one flux has been measured for. It could probably save us some pain, and we could safely just send Sorry for this long wall of text, I hope it is digestible :-) |
|
I think the approach here is not ideal because it tries to put the entire calculation into a single function/object. The problem with that is that if at some later date you want to make a change to some of the parts or use different assumptions it all gets very complicated, as you would have to decompose it. I would propose the following: The top formula is We would need functions:
See the current AbstractFlux class for the current implementation of these two functions. If we ever get to using the real (non-top-hat) beam profile and the actual crystal shape to calculate dose, the effective flux density would get a lot more complicated, and there would have to be operational decisions as to which value to use, or whether to calculate some kind of weighted integral. For a simple case (large crystal, narrow beam, approximation) see GphlWorkflow.maximum_dose_rate(). Anyway, this being so dependent on assumptions it might be better not to put it into a top-level combined function (I am quoting Gleb Bourenkov here) The relative crystal sensitivity is in principle wavelength dependent. For now it can be assumed to be a constant passed in with the diffraction plan (I am not aware of any better implementation). The relevant time is easy to calculate, and can be left to be done locally. I think this would give a nice, clean separation between the various terms and make it easier to understand and maintain. Note that we might have not only calculations from crystal shape but also multi-sweep, multi-wavelength experiments, so it is better not to bundle all the calculations up. |
This is part of the dose estimation capability that we have presented at the developer's meeting in Berlin
This HardwareObject is meant to provide a way to estimate absorbed
dose by crystal during oscillation scans.
Includes abstract definition along implementations at BioMAX and a mockup one.
Note on the BioMAX implementation:
Calculations inside the module have been based on the old dose estimation
code used at BioMAX and is a subject to change in the future.
It's correctness is not guaranteed, nonetheless it serves as a reference
implementation to iterate on afterwards.