jfkcooper/HOGBEN

Magnetic `refnx` layers

Opened this issue · 2 comments

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:

  • Physically: The layer can be initialised with MagneticLayer(thick = 30, density = 4, mag=3, weight=5). And the SLD is calculated based on the provided arguments. This does however require the magnetic moment, the mass density as well as the atomic weight. The same parameters are required for the nuclear SLD, minus the magnetic moment.
  • From material: If we go the above route, the required parameters (at least for density and atomic weight, not sure about magnetic moment), can be retrieved from the element in the periodictable module. This could allow users to insert a material directly, which may be convenient. Especially if you're not optimising the SLD
  • From SLD directly: Just input the SLD directly, and initialise the layer like MagneticLayer(thick=30, SLDn=4, SLDm=3). This is closer to vanilla refnx (at least so far, we've been working with SLD directly there), and this allows to optimise for the SLD directly if we want to provide a theoretically best scattering profile.

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.

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.

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.