jfkcooper / HOGBEN

Experimental design in neutron reflectometry using the Fisher information.
BSD 3-Clause "New" or "Revised" License
6 stars 2 forks source link

Magnetic `refnx` layers #101

Open sstendahl opened 10 months ago

sstendahl commented 10 months ago

Thought I'd open an issue on this before I forget with the christmas season in sight.

Basically, I've got a working implementation with magnetic layers, but the question mainly remains on how the scattering length densities should be calculated. There's two distinct ways I'm thinking about this:

Of course it may also be worthwhile to support multiple versions. The implementation is quite simple, and we can just create a MagneticLayer class, and inherit from there for the three above options. This would however require users to import the specific kind of layer they want to work with, but that may not be that terrible.

Another option is to dynamically "guess" what the user tries from their input arguments. So with a single class, and if both SLDn and density are given for example, raise an error that incompatible arguments are given. But that will likely be much less clean code-wise, and make things less maintainable. So I tend to lean towards having a seperate class (inheriting from a MagneticLayer class) for each input type.

sstendahl commented 10 months ago

Just for reference, I've got an implementation right now for the first option (from physical parameters) and the last option (from SLD directly). This looks something like this:

class MagneticLayerSLD(refnx.reflect.structure.Slab):
    def __init__(self,
                 SLDn = 1,
                 SLDm = 0,
                 thick = 10,
                 rough = 6,
                 underlayer = True,
                 name="magnetic_layer"):
        self.SLD_n = SLDn
        self.SLD_m = SLDm
        self.thickness = thick
        self.roughness = rough
        self.underlayer = underlayer
        self.name = name
        super().__init__(thick=self.thickness, sld=self.SLD_n, rough=self.roughness, name=self.name)

    @property
    def spin_up(self):
        SLD_value = self.SLD_n + self.SLD_m
        return SLD(SLD_value, name='spin_up')(thick=self.thickness, rough=self.rough)

    @property
    def spin_down(self):
        SLD_value = self.SLD_n - self.SLD_m
        return SLD(SLD_value, name='spin_down')(thick=self.thickness, rough=self.rough)

A list of the magnetic structures can be retrieved using sample.get_structures() which returns the spin-up and spin-down case:

    def get_structures(self):
        """
        Get a list of the possible sample structures.
        """
        structures = []
        spin_up_structure = self.structure.copy()
        spin_down_structure = self.structure.copy()
        for i, layer in enumerate(self.structure):
            if isinstance(layer, MagneticLayer) or isinstance(layer, MagneticLayerSLD):
                spin_up_structure[i] = layer.spin_up
                spin_down_structure[i] = layer.spin_down
        structures.extend([spin_up_structure, spin_down_structure])

        return structures

Even if there's no magnetic layer in the sample, it returns a spin-up and a spin-down structure. However, these will be identical, which is exactly as intended, but the name of this method is not ideal. This above method can lead to pretty easy implementation to take both cases into account. For example to get a list of models, qs and counts etc.. to generate a Fisher object I do the following:

    @classmethod
    def from_sample(cls,
                    sample,
                    angle_times,
                    contrasts = None,
                    underlayers = None,
                    instrument = None):
        """
        Get Fisher object using a sample.
        Seperate constructor for magnetic simulation maybe? Probably depends
        on new simulate function either way.
        """

        qs, counts, models = [], [], []
        structures = sample.get_structures()
        for structure in structures:
            model = refnx.reflect.ReflectModel(structure)
            sim = SimulateReflectivity(model, angle_times)
            data = sim.simulate()
            qs.append(data[0])
            counts.append(data[3])
            models.append(model)

        xi = sample.get_varying_parameters()
        return cls(qs, xi, counts, models)

So a Fisher object can just be created like Fisher.from_sample(sample). And it creates an object that includes both spin-up and spin-down direction. Some arguments may be nice to specify which spin-states should be included (which in turn would then be parsed into get_structures(), so that it's still possible to create such an object without the spin states, but again it's a bit WIP right now.

andyfaff commented 2 months ago

refnx has a refnx.reflect.structure._PolarisedSlab, and refnx.reflect.structure.Structure has a private _spin attribute that can be used to specify whether a Structure should be considered to be spin up/spin down.

At the moment they're both private as they're considered to be in a prototype phase. Of course, you could copy the _PolarisedSlab to your code if you wanted to, and monkeypatch Structure to add the _spin attribute yourself.