Skip to content

FIx instrument model#136

Merged
henrikjacobsenfys merged 10 commits intodevelopfrom
fix-sample-model-templates
Mar 25, 2026
Merged

FIx instrument model#136
henrikjacobsenfys merged 10 commits intodevelopfrom
fix-sample-model-templates

Conversation

@henrikjacobsenfys
Copy link
Member

Fixes #135

I planned to also work on #101 , hence the name of the branch, but I need to think more about it. For now, it's good to fix this bug.

@henrikjacobsenfys henrikjacobsenfys added [scope] bug Bug report or fix (major.minor.PATCH) [priority] high Should be prioritized soon labels Mar 17, 2026
@codecov
Copy link

codecov bot commented Mar 17, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.74%. Comparing base (860c3ca) to head (0da0140).
⚠️ Report is 1 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #136      +/-   ##
===========================================
+ Coverage    97.64%   97.74%   +0.10%     
===========================================
  Files           37       37              
  Lines         2460     2486      +26     
  Branches       413      421       +8     
===========================================
+ Hits          2402     2430      +28     
+ Misses          34       32       -2     
  Partials        24       24              
Flag Coverage Δ
integration 0.00% <0.00%> (ø)
unittests 97.74% <100.00%> (+0.10%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@seventil
Copy link

As I see from the API of InstrumentModel, ResolutionModel and BackgroundModel, Q can be set to None in each of those. Default value is also None for all of their init parameter Q. When you set Q in the InstrumentModel it propagates to InstrumentModel and ResolutionModel. But if you set Q to None in the InstrumentModel afterwards, the value will not be propagated to ResolutionModel and BackgroundModel, creating a discrepancy in state between the three models.
I understand that your workflows might avoid such situation, but the API itself allows it. Is that an issue? Why even allow to set Q property to None?

@henrikjacobsenfys
Copy link
Member Author

Well, the issue is that the expected workflow is something like this:

# new background model, can be given a Q but we don't have to
background_model=BackgroundModel(components=background_components) 
instrument_model=InstrumentModel(
background_model=background_model,
resolution_model=previous_fit.instrument_model.resolution_model) 
# resolution model has specific components with specific values for each Q 
#determined by a fit. InstrumentModel does not yet know about these Q
# InstrumentModel is (and should be, I think) responsible for making sure 
#resolution_model has the right Q. But right now, instrument_model's Q is None, so that's 
# what it passes on to its resolution_model, resetting it, which I don't want


# new sample model, can also be given a Q, but doesn't need it
sample_model=SampleModel(components=sample_components) 
experiment=Experiment(data=some_data) # contains information about Q

# Analysis passes the Q from experiment to its instrument_model,
# which as mentioned passes it on to its resolution_model and background_model
# If the Q from Analysis matches what is already in resolution_model, then it
# does not overwrite its components.
analysis=Analysis(experiment=experiment,
sample_model=sample_model,
instrument_model=instrument_model) 

I probably need a more sophisticated way of handling the state of these things, but it's kind of difficult. I don't want users to have to think about supplying their InstrumentModel with the correct Q from the Experiment - that's the job of Analysis. So this intermediate step where the InstrumentModel is created and a ResolutionModel is passed to it is kind of tricky.

One potential solution is to get rid of InstrumentModel and just pass a SampleModel, BackgroundModel, ResolutionModel directly to Analysis. This does make Analysis into a quite large class, but it may be the best solution? Thoughts are very welcome.

In this case, the workflow is

background_model=BackgroundModel(components=background_components) 
resolution_model=previous_fit.instrument_model.resolution_model) 
sample_model=SampleModel(components=sample_components) 
experiment=Experiment(data=some_data) # contains information about Q

analysis=Analysis(
experiment=experiment,
background_model=background_model,
sample_model=sample_model,
resolution_model=resolution_model
) 

Hmm, the more I think about it, the more I like it. I had it like this at first, then changed it, and I don't remember why anymore...

@henrikjacobsenfys
Copy link
Member Author

henrikjacobsenfys commented Mar 18, 2026

Thinking a bit more..

perhaps the solution is to initialize Q to None. Then if self.Q is not None, the Q setter throws an error unless the new Q is identical to what's already there.

I can add a clear_Q or destroy_Q or something method to allow changing Q, that makes it clear that it breaks what's in the model.

Copy link
Member

@rozyczko rozyczko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor issues raised

Comment on lines 377 to 378
instrument_model._background_model.Q = new_Q
instrument_model._resolution_model.Q = new_Q
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these not be asserts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should those also have a conditional for if self._Q is not None?

@henrikjacobsenfys
Copy link
Member Author

Not quite done yet (missing unit tests, for one), but updated so that Q cannot be set if it's not None. This avoids many of the potential problems.

@henrikjacobsenfys
Copy link
Member Author

@seventil and @rozyczko Would you take a look at this again? :)

@seventil
Copy link

seventil commented Mar 23, 2026

LGTM for me. Text below is something you might want to take into consideration

I am a bit confused by current implementation. I think the best way to undertand the implementation requirements is to derive them from use-case requirements. As far as I understand them there are two workflows:

  1. InstrumentModel is instantiated, ResolutionModel holds the previous Q (is there always a previous?)
  2. InstrumentModel gets (non-None) Q from the experiment, Qs across InstrumentModel/ResolutionModel/BackgroundModel are synchronised.

Assuming these two, there is no reason for InstrumentModel Q setter to accept None values. Just make the Q value in Q setter non-optional. (as you kinda do with return, but the code doesn't set None to this Q i think anyway, so the whole branch with accepting Q as value but then not changing anything is unnecessary anyway imho)

What is the Q getter from InstrumentModel used for? - you can know whether or not it's a problem if getters output is optional None. If it's not used, why have it as a property even? If it's used - what would InstrumentModel.Q==None mean for the code that's reading it? If it's ok, I'd just self._Q=None in init jus to have _Q attr in the object, and then use the setter-route for any valid (non-None) Q assignment either from init attribute or if updated from Experiment.

Copy link
Member

@rozyczko rozyczko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with the code

@henrikjacobsenfys
Copy link
Member Author

LGTM for me. Text below is me yapping to myself to understand what's going on.

I am a bit confused by current implementation. I think the best way to undertand the implementation requirements is to derive them from use-case requirements. As far as I understand them there are two workflows:

  1. InstrumentModel is instantiated, ResolutionModel holds the previous Q (is there always a previous?)

There is not always a previous Q, which is part of the difficulty

  1. InstrumentModel gets (non-None) Q from the experiment, Qs across InstrumentModel/ResolutionModel/BackgroundModel are synchronised.

Yes, that is the idea. If it matters, it gets it from Analysis.

Assuming these two, there is no reason for InstrumentModel Q setter to accept None values.

I don't know if this is a good reason, but my thinking was this: what happens if the user removes an Experiment from their Analysis? This is allowed, but then Q becomes None, which gets passed to the InstrumentModel. I could of course make a check in Analysis and prevent it, but I deemed it simpler to allow it without errors. Perhaps this falls under the "never fail silently" and should be changed.

Just make the Q value in Q setter non-optional. (as you kinda do with return, but the code doesn't set None to this Q i think anyway, so the whole branch with accepting Q as value but then not changing anything is unnecessary anyway imho)

Again, not saying this is optimal, but I do want to be able to change Experiment, which (I think) should update Q everywhere, except

What is the Q getter from InstrumentModel used for?

I didn't think much about it - I made a setter, so I also made a getter.

  • you can know whether or not it's a problem if getters output is optional None. If it's not used, why have it as a property even? If it's used - what would InstrumentModel.Q==None mean for the code that's reading it? If it's ok, I'd just self._Q=None in init jus to have _Q attr in the object, and then use the setter-route for any valid (non-None) Q assignment either from init attribute or if updated from Experiment.

Perhaps we can chat a bit about this tomorrow? It's clearly not a perfect implementation yet, but here's what I'm trying to make happen:

Analysis takes an Experiment, SampleModel and InstrumentModel, which itself contains a BackgroundModel and ResolutionModel. Everything except the Experiment is optional for the following. The Experiment is the source of truth for Q.

All the SomethingModel contain a list of either Parameter or ComponentCollection or both, one for each Q. They should be automatically generated from a template, since I don't want users to copy/paste their model to every Q manually.

However, if the SomethingModels have been fitted or changed by the user, I don't want them to be overwritten by accident. This is why setting to None does nothing.

It should be easy to transfer any and all SomethingModels to a new Analysis and supplement with new ones where Analysis uses the template to generate all the components. It should also be easy to give the Analysis a new Experiment.

Finally, it should be easy to work with all of these things outside Analysis, mostly for testing out ideas for the users. So I want users to be able to set Q of their InstrumentModel without an Experiment.

@henrikjacobsenfys henrikjacobsenfys merged commit 8d97ad8 into develop Mar 25, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[priority] high Should be prioritized soon [scope] bug Bug report or fix (major.minor.PATCH)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants