Skip to content

Conversation

@henrikjacobsenfys
Copy link
Member

@henrikjacobsenfys henrikjacobsenfys commented Oct 17, 2025

Adding SampleModel which can contain ModelComponents and calculate the model at given x, with and without detailed balance.

@henrikjacobsenfys henrikjacobsenfys added [scope] enhancement Adds/improves features (major.MINOR.patch) [priority] high Should be prioritized soon labels Oct 17, 2025
@henrikjacobsenfys henrikjacobsenfys added this to the First release milestone Oct 17, 2025
@henrikjacobsenfys
Copy link
Member Author

henrikjacobsenfys commented Oct 17, 2025

Code coverage is not 100%, but I think all the essentials are there, and I'd rather get feedback on the important things than polish tests that may need to change anyway :)

Edit: apparently codecov is upset about the missing tests, so I added tests and discovered a minor bug in the process.

@codecov
Copy link

codecov bot commented Oct 17, 2025

Codecov Report

❌ Patch coverage is 97.24771% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.73%. Comparing base (01b9490) to head (0d3e3eb).

Files with missing lines Patch % Lines
src/easydynamics/sample_model/sample_model.py 97.22% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop      #56      +/-   ##
===========================================
+ Coverage    96.60%   96.73%   +0.13%     
===========================================
  Files           12       13       +1     
  Lines          412      521     +109     
===========================================
+ Hits           398      504     +106     
- Misses          14       17       +3     
Flag Coverage Δ
unittests 96.73% <97.24%> (+0.13%) ⬆️

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.

@henrikjacobsenfys henrikjacobsenfys changed the title A Sample Model Oct 17, 2025
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.

part 1 after a quick look

Numeric = Union[float, int]


class SampleModel(ObjBase, MutableMapping):
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't you inherit from TheoreticalModelBase or at least from BaseObjectCollection?

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably? I'm not exactly sure

params = []
for comp in self.components.values():
params.extend(comp.get_parameters())
return params
Copy link
Member

Choose a reason for hiding this comment

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

or just

    from itertools import chain
    
    # Create generator for temperature parameter
    temp_params = (self._temperature,) if self._temperature is not None else ()
    
    # Create generator for component parameters
    comp_params = (param for comp in self.components.values() 
                   for param in comp.get_parameters())
    
    # Chain them together and return as list
    return list(chain(temp_params, comp_params))

(slight teaching aspect for itertools - a VERY important python module)

if is_not_fixed and is_independent:
fit_parameters.append(parameter)

return fit_parameters
Copy link
Member

Choose a reason for hiding this comment

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

Again, not a bad loop (easy to see what's being done) but if you want to make it more "modular" you could use list comprehension, e.g.

    def is_fit_parameter(param: Parameter) -> bool:
        """Check if a parameter can be used for fitting."""
        return (not getattr(param, "fixed", False) and 
                getattr(param, "_independent", True))
    
    return [param for param in self.get_parameters() if is_fit_parameter(param)]

@henrikjacobsenfys
Copy link
Member Author

@rozyczko Would you have another look when you have time? I'm working on other things the rest of the day and am away tomorrow, so no rush.

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.

A few more comments after pondering on the file for too long ;)

"""

CollectionBase.__init__(self, name=name)
TheoreticalModelBase.__init__(self, name=name)
Copy link
Member

Choose a reason for hiding this comment

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

This should probably be just

super().__init__(name=name)

to properly handle multiple inheritances. Otherwise it violates MRO best practices and makes maintenance more difficult (by being too explicit)

-------
List[ModelComponent]
"""
return list(self)
Copy link
Member

Choose a reason for hiding this comment

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

The class mixes direct list access (list(self)) with a components property. Pick one approach:

  • Either make components the primary interface and remove list(self) calls
  • Or fully embrace the collection interface and remove the components property

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point. I will embrace the collection

Name to assign to the component. If None, uses the component's own name.
"""
if not isinstance(component, ModelComponent):
raise TypeError("component must be an instance of ModelComponent.")
Copy link
Member

Choose a reason for hiding this comment

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

Maybe uppercase Component since it is the start of the sentence?


# Add initial components if provided. Mostly used for serialization.
if data:
# Just to be safe
Copy link
Member

Choose a reason for hiding this comment

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

This comment adds no value. Either explain WHY it's needed or remove it, please.



class SampleModel(CollectionBase, TheoreticalModelBase):
"""
Copy link
Member

Choose a reason for hiding this comment

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

It would probably be useful to add __len__ so querying the model is simpler

>>> model = SampleModel()
>>> model.add_component(Gaussian(name="g1"))
>>> len(model)

To do this, just query super():

def __len__(self) -> int:
        return super().__len__()

Copy link
Member Author

Choose a reason for hiding this comment

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

Already works! :)

self.insert(index=len(self), value=component)

def remove_component(self, name: str):
"""
Copy link
Member

Choose a reason for hiding this comment

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

Why find all indices if you only delete the first? This is either inefficient or you expect duplicate names?
Just delete the first index found.

List[ModelComponent]
"""
return list(self)

Copy link
Member

Choose a reason for hiding this comment

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

Also, this creation of a new list every time components() is accessed is pretty wasteful. Either cache it or use list(self) directly as said earlier.

) # noqa: E501

def convert_unit(self, unit: Union[str, sc.Unit]) -> None:
"""
Copy link
Member

Choose a reason for hiding this comment

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

Please consider adding the rollback mechanism, which you did in ModelComponent.

Comment on lines 206 to 211
if not self.components:
raise ValueError("No components in the model to evaluate.")
result = None
for component in list(self):
value = component.evaluate(x)
result = value if result is None else result + value
Copy link
Member

Choose a reason for hiding this comment

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

or just

if not self.components:
    raise ValueError("No components in the model to evaluate.")
return sum(component.evaluate(x) for component in self)


class SampleModel(CollectionBase, TheoreticalModelBase):
"""
A model of the scattering from a sample, combining multiple model components.
Copy link
Member

Choose a reason for hiding this comment

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

Similarly, you could extend the API to include __contains__ so we can do:

>>> model = SampleModel()
>>> gaussian = Gaussian(name="g1")
>>> model.add_component(gaussian)
>>> "g1" in model
True
>>> gaussian in model
True
>>> "nonexistent" in model
False

with something like

def __contains__(self, item: Union[str, ModelComponent]) -> bool:
    if isinstance(item, str):
        # Check by component name
        return any(comp.name == item for comp in self)
    elif isinstance(item, ModelComponent):
        # Check by component instance
        return any(comp is item for comp in self)
    else:
        return False

@henrikjacobsenfys
Copy link
Member Author

As always, very useful comments :) here's the updated version

Copy link

@damskii9992 damskii9992 left a comment

Choose a reason for hiding this comment

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

First and by far the biggest comment.
Do you WANT your SampleModel to BE a list and dictionary? Or do you want it to contain a list/dictionary?
It should only be the former if you REALLY want to be able to iterate over your model, slice it to get components, or do len(sample_model) instead of, for example len(sample_model.components).
I don't really think you want it to be a list/dictionary . . .
And even if you did, you should probably write up your own collections class, as relying on CollectionBase is dangerous as it is subject to be removed (or at least heavily modified).

@henrikjacobsenfys
Copy link
Member Author

First and by far the biggest comment. Do you WANT your SampleModel to BE a list and dictionary? Or do you want it to contain a list/dictionary? It should only be the former if you REALLY want to be able to iterate over your model, slice it to get components, or do len(sample_model) instead of, for example len(sample_model.components). I don't really think you want it to be a list/dictionary . . . And even if you did, you should probably write up your own collections class, as relying on CollectionBase is dangerous as it is subject to be removed (or at least heavily modified).

Well, I honestly don't care too much exactly what it is. I want to have a collection of components that I/the user can easily access and work with, I want it to be integrated with the rest of corelib, i.e. reuse as much of corelib as possible, which also helps minimizing the amount of code I have to write/maintain in dynamics-lib. CollectionBase checks all those boxes. I'm not attached to it in any way, though, so happy enough to do it in a different way.

Would you recommend the components to be stored as a dict or list?

@henrikjacobsenfys
Copy link
Member Author

Updated to no longer use CollectionBase.

I'm still using the BasedBase/ObjBase _kwargs to store the components, since it seems to be the most straightforward way to get the corelib functionality with global object etc

@henrikjacobsenfys henrikjacobsenfys marked this pull request as draft November 21, 2025 13:42
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] enhancement Adds/improves features (major.MINOR.patch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants