def __init__(self, extent, vs, dz, name='', interpolator=Pchip, zgrad=True, microslab_max_thickness=1): super(Spline, self).__init__() self.name = name self.microslab_max_thickness = microslab_max_thickness self.extent = (possibly_create_parameter(extent, name='%s - spline extent' % name)) self.dz = Parameters(name='dz - spline') for i, z in enumerate(dz): p = possibly_create_parameter(z, name='%s - spline dz[%d]' % (name, i)) p.range(0, 1) self.dz.append(p) self.vs = Parameters(name='vs - spline') for i, v in enumerate(vs): p = possibly_create_parameter(v, name='%s - spline vs[%d]' % (name, i)) self.vs.append(p) if len(self.vs) != len(self.dz): raise ValueError("dz and vs must have same number of entries") self.zgrad = zgrad self.interpolator = interpolator self.__cached_interpolator = { 'zeds': np.array([]), 'vs': np.array([]), 'interp': None, 'extent': -1 }
def parameters(self): p = Parameters(name=self.name) p.extend([ self.Popc.s_sld, self.Popc.water_per_lipid_head, self.Popc.water_per_lipid_tail, self.Popc.b_heads_real, self.Popc.b_heads_imag, self.Popc.b_tails_real, self.Popc.b_tails_imag, self.Popc.vm_heads, self.Popc.vm_tails, self.Popg.s_sld, self.Popg.water_per_lipid_head, self.Popg.water_per_lipid_tail, self.Popg.b_heads_real, self.Popg.b_heads_imag, self.Popg.b_tails_real, self.Popg.b_tails_imag, self.Popg.vm_heads, self.Popg.vm_tails, self.apm, self.roughness_top, self.roughness_bottom, self.ratio, self.volFrac ]) return p # def logp(self): # return 0
def __init__(self, thick, sld, rough, name='', vfsolv=0): super(Slab, self).__init__() self.thick = possibly_create_parameter(thick, name='%s - thick' % name) if isinstance(sld, SLD): self.sld = sld else: self.sld = SLD(sld) self.rough = possibly_create_parameter(rough, name='%s - rough' % name) self.vfsolv = ( possibly_create_parameter(vfsolv, name='%s - volfrac solvent' % name)) self.name = name p = Parameters(name=self.name) p.extend([self.thick, self.sld.real, self.sld.imag, self.rough, self.vfsolv]) self._parameters = p
def __init__( self, structures, scales=None, bkg=1e-7, name="", dq=5.0, threads=-1, quad_order=17, dq_type="pointwise", ): self.name = name self._parameters = None self.threads = threads self.quad_order = quad_order # all reflectometry models need a scale factor and background. Set # them all to 1 by default. pscales = Parameters(name="scale factors") if scales is not None and len(structures) == len(scales): tscales = scales elif scales is not None and len(structures) != len(scales): raise ValueError("You need to supply scale factor for each" " structure") else: tscales = [1 / len(structures)] * len(structures) for scale in tscales: p_scale = possibly_create_parameter(scale, name="scale") pscales.append(p_scale) self._scales = pscales self._bkg = possibly_create_parameter(bkg, name="bkg") # we can optimize the resolution (but this is always overridden by # x_err if supplied. There is therefore possibly no dependence on it. self._dq = possibly_create_parameter(dq, name="dq - resolution") self.dq_type = dq_type self._structures = structures
def setup_method(self, tmpdir): self.path = os.path.dirname(os.path.abspath(__file__)) self.tmpdir = tmpdir.strpath theoretical = np.loadtxt(os.path.join(self.path, 'gauss_data.txt')) xvals, yvals, evals = np.hsplit(theoretical, 3) xvals = xvals.flatten() yvals = yvals.flatten() evals = evals.flatten() # these best weighted values and uncertainties obtained with Igor self.best_weighted = [-0.00246095, 19.5299, -8.28446e-2, 1.24692] self.best_weighted_errors = [0.0220313708486, 1.12879436221, 0.0447659158681, 0.0412022938883] self.best_weighted_chisqr = 77.6040960351 self.best_unweighted = [-0.10584111872702096, 19.240347049328989, 0.0092623066070940396, 1.501362314145845] self.best_unweighted_errors = [0.34246565477, 0.689820935208, 0.0411243173041, 0.0693429375282] self.best_unweighted_chisqr = 497.102084956 self.p0 = np.array([0.1, 20., 0.1, 0.1]) self.names = ['bkg', 'A', 'x0', 'width'] self.bounds = [(-1, 1), (0, 30), (-5., 5.), (0.001, 2)] self.params = Parameters(name="gauss_params") for p, name, bound in zip(self.p0, self.names, self.bounds): param = Parameter(p, name=name) param.range(*bound) param.vary = True self.params.append(param) self.model = Model(self.params, fitfunc=gauss) self.data = Data1D((xvals, yvals, evals)) self.objective = Objective(self.model, self.data) return 0
def __init__(self, value, name=''): self.name = name self.imag = Parameter(0, name='%s - isld' % name) if isinstance(value, numbers.Real): self.real = Parameter(value.real, name='%s - sld' % name) elif isinstance(value, numbers.Complex): self.real = Parameter(value.real, name='%s - sld' % name) self.imag = Parameter(value.imag, name='%s - isld' % name) elif isinstance(value, SLD): self.real = value.real self.imag = value.imag elif isinstance(value, Parameter): self.real = value elif (hasattr(value, '__len__') and isinstance(value[0], Parameter) and isinstance(value[1], Parameter)): self.real = value[0] self.imag = value[1] self._parameters = Parameters(name=name) self._parameters.extend([self.real, self.imag])
def structure(self, structure): self._structure = structure p = Parameters(name='instrument parameters') p.extend([self.scale, self.bkg, self.dq]) self._parameters = Parameters(name=self.name) self._parameters.extend([p, structure.parameters])
def parameters(self): p = Parameters(name=self.name) p.extend([ self.apm, self.b_heads_real, self.b_heads_imag, self.vm_heads, self.thickness_heads, self.b_tails_real, self.b_tails_imag, self.vm_tails, self.thickness_tails, self.rough_head_tail, self.rough_preceding_mono ]) if self.head_solvent is not None: p.append(self.head_solvent.parameters) if self.tail_solvent is not None: p.append(self.tail_solvent.parameters) return p
def structure(self, structure): self._structure = structure p = Parameters(name="instrument parameters") p.extend([self.wav, self.delOffset]) self._parameters = Parameters(name=self.name) self._parameters.extend([p, structure.parameters])
def structure(self, structure): self._structure = structure p = Parameters(name="instrument parameters") p.extend([self.scale, self.bkg, self.dq, self.q_offset]) self._parameters = Parameters(name=self.name) self._parameters.extend([p, structure.parameters])
def parameters(self): """ Returns ------- Parameter, array_like An array of the parameters in the fitting. """ p = Parameters(name=self.name) p.extend([ self.vol[0], self.vol[1], self.realb[0], self.realb[1], self.imagb[0], self.imagb[1], self.d[0], self.d[1], self.phi[0], self.phi[1], self.sigma, ]) return p
def test_or(self): # concatenation of Parameters # Parameters with Parameter c = self.m | self.b assert_equal(len(c), 3) assert_equal(len(c.flattened()), 3) assert_(c.flattened()[1] is self.b) assert_(c.flattened()[2] is self.b) # Parameters with Parameters c = Parameters(name='c') d = c | self.m assert_(d.name == 'c')
def parameters(self): """ Parameters for the model. Returns: (refnx.analysis.Parameters) Model parameters. """ para = Parameters(name=self.name) para.extend([ self.b_real_h, self.b_imag_h, self.b_real_t, self.b_imag_t, self.thick_h, self.thick_t, self.mol_vol_h, self.mol_vol_t, self.rough_h_t, self.rough_t_a, self.phi_h, self.phi_t, ]) return para
def test_ior(self): # concatenation of Parameters # Parameters with Parameter c = Parameters(name='c') c |= self.b assert_equal(len(c), 1) assert_equal(len(c.flattened()), 1) assert_(c.flattened()[0] is self.b) # Parameters with Parameters c = Parameters(name='c') c |= self.m assert_(c.name == 'c') assert_equal(len(c), 1) assert_equal(len(c.flattened()), 2) assert_(c.flattened()[1] is self.b)
def test_model_subclass(self): class Line(Model): def __init__(self, parameters): super(Line, self).__init__(parameters) def model(self, x, p=None, x_err=None): if p is not None: self._parameters = p a, b = self._parameters return a.value + x * b.value a = Parameter(1.1) b = Parameter(2.2) p = Parameters([a, b]) Line(p)
def parameters(self): r""" :class:`refnx.analysis.Parameters`, all the parameters associated with this structure. """ p = Parameters(name='Stack - {0}'.format(self.name)) p.append(self.repeats) p.extend([component.parameters for component in self.components]) return p
def test_or(self): # concatenation of Parameters # Parameters with Parameter c = self.m | self.b assert_equal(len(c), 3) assert_equal(len(c.flattened()), 3) assert_(c.flattened()[1] is self.b) assert_(c.flattened()[2] is self.b) # Parameters with Parameters c = Parameters(name="c") d = c | self.m assert_(d.name == "c") # c and d should not be the same object assert c is not d
def parameters(self): r""" :class:`refnx.analysis.Parameters`, all the parameters associated with this structure. """ p = Parameters(name='Structure - {0}'.format(self.name)) p.extend([component.parameters for component in self.components]) if self._solvent is not None: p.append(self.solvent.parameters) return p
def parameters(self): r""" :class:`refnx.analysis.Parameters` - parameters associated with this model. """ p = Parameters(name='instrument parameters') p.extend([self.scales, self.bkg, self.dq]) self._parameters = Parameters(name=self.name) self._parameters.append([p]) self._parameters.extend( [structure.parameters for structure in self._structures]) return self._parameters
def parameters(self): p = Parameters(name=self.name) p.extend([self.thick]) # p.extend(self.sld.parameters) p.extend([self.rough, self.vol_protein, self.vfsolv]) # p.name = self.name # p.extend([self.vol_protein, # self.PLratio])#,self.solventSLD # if isinstance(solventSLD, SLD): # p.extend([self.solventSLD.parameters]) # elif isinstance(solventSLD, complex): # self.solventSLD = SLD(solventSLD) # else: # p.extend([self.solventSLD return p
def test_logp(self): self.p[0].range(0, 10) assert_almost_equal(self.objective.logp(), np.log(0.1)) # logp should set parameters self.objective.logp([8, 2]) assert_equal(np.array(self.objective.parameters), [8, 2]) # if we supply a value outside the range it should return -inf assert_equal(self.objective.logp([-1, 2]), -np.inf) # are auxiliary parameters included in log? assert_almost_equal(self.objective.logp([8, 2]), np.log(0.1)) p = Parameter(2.0, bounds=(1.0, 3.0)) self.objective.auxiliary_params = Parameters([p]) assert len(self.objective.varying_parameters()) == 2 assert_equal(self.objective.logp(), np.log(0.1)) assert p in self.objective.parameters.flattened() p.vary = True assert len(self.objective.varying_parameters()) == 3 assert_equal(self.objective.logp(), np.log(0.1) + np.log(0.5)) assert p in self.objective.varying_parameters().flattened()
class Spline(Component): """ Freeform modelling of the real part of an SLD profile using spline interpolation. Parameters ---------- extent : float or Parameter Total extent of spline region vs : Sequence of float/Parameter the real part of the SLD values of each of the knots. dz : Sequence of float/Parameter the lateral offset between successive knots. left : refnx.reflect.Component The Component to the left of this Spline region. right : refnx.reflect.Component The Component to the right of this Spline region. solvent : refnx.reflect.SLD An SLD instance representing the solvent name : str Name of component interpolator : scipy.interpolate Univariate Interpolator, optional Which scipy.interpolate Univariate Interpolator to use. zgrad : bool, optional If true then extra control knots are placed outside this spline with the same SLD as the materials on the left and right. With a monotonic interpolator this guarantees that the gradient is zero at either end of the interval. microslab_max_thickness : float Maximum size of the microslabs approximating the spline. Notes ----- This spline component only generates the real part of the SLD (thereby assuming that the imaginary part is negligible). The sequence dz are the lateral offsets of the knots normalised to a unit interval [0, 1]. The reason for using lateral offsets is so that the knots are monotonically increasing in location. When each dz offset is turned into a Parameter it is given bounds in [0, 1]. Thus with an extent of 500, and dz = [0.1, 0.2, 0.2], the knots will be at [0, 50, 150, 250, 500]. Notice that there are two extra knots for the start and end of the interval (disregarding the `zgrad` control knots). If ``np.sum(dz) > 1``, then the knot spacings are normalised to 1. e.g. dz of [0.1, 0.2, 0.9] would result in knots (in the normalised interval) of [0, 0.0833, 0.25, 1, 1]. If `vs` is monotonic then the output spline will be monotonic. If `vs` is not monotonic then there may be regions of the spline larger or smaller than `left` or `right`. The slab representation of this component are approximated using a 'microslab' representation of spline. The max thickness of each microslab is `microslab_max_thickness`. """ def __init__(self, extent, vs, dz, left, right, solvent, name='', interpolator=Pchip, zgrad=True, microslab_max_thickness=1): self.name = name self.left_slab = left self.right_slab = right self.solvent = solvent self.microslab_max_thickness = microslab_max_thickness self.extent = (possibly_create_parameter(extent, name='%s - spline extent' % name)) self.dz = Parameters(name='dz - spline') for i, z in enumerate(dz): p = possibly_create_parameter(z, name='%s - spline dz[%d]' % (name, i)) p.range(0, 1) self.dz.append(p) self.vs = Parameters(name='vs - spline') for i, v in enumerate(vs): p = possibly_create_parameter(v, name='%s - spline vs[%d]' % (name, i)) self.vs.append(p) if len(self.vs) != len(self.dz): raise ValueError("dz and vs must have same number of entries") self.zgrad = zgrad self.interpolator = interpolator self.__cached_interpolator = { 'zeds': np.array([]), 'vs': np.array([]), 'interp': None, 'extent': -1 } def _interpolator(self): dz = np.array(self.dz) zeds = np.cumsum(dz) # if dz's sum to more than 1, then normalise to unit interval. if zeds[-1] > 1: zeds /= zeds[-1] zeds = np.clip(zeds, 0, 1) vs = np.array(self.vs) left_sld = Structure.overall_sld( np.atleast_2d(self.left_slab.slabs[-1]), self.solvent)[..., 1] right_sld = Structure.overall_sld( np.atleast_2d(self.right_slab.slabs[0]), self.solvent)[..., 1] if self.zgrad: zeds = np.concatenate([[-1.1, 0 - EPS], zeds, [1 + EPS, 2.1]]) vs = np.concatenate([left_sld, left_sld, vs, right_sld, right_sld]) else: zeds = np.concatenate([[0 - EPS], zeds, [1 + EPS]]) vs = np.concatenate([left_sld, vs, right_sld]) # cache the interpolator cache_zeds = self.__cached_interpolator['zeds'] cache_vs = self.__cached_interpolator['vs'] cache_extent = self.__cached_interpolator['extent'] # you don't need to recreate the interpolator if (np.array_equal(zeds, cache_zeds) and np.array_equal(vs, cache_vs) and np.equal(self.extent, cache_extent)): return self.__cached_interpolator['interp'] else: self.__cached_interpolator['zeds'] = zeds self.__cached_interpolator['vs'] = vs self.__cached_interpolator['extent'] = float(self.extent) # TODO make vfp zero for z > self.extent interpolator = self.interpolator(zeds, vs) self.__cached_interpolator['interp'] = interpolator return interpolator def __call__(self, z): """ Calculates the spline value at z Parameters ---------- z : float Distance along spline Returns ------- sld : float Real part of SLD """ interpolator = self._interpolator() vs = interpolator(z / float(self.extent)) return vs @property def parameters(self): p = Parameters(name=self.name) p.extend([ self.extent, self.dz, self.vs, self.left_slab.parameters, self.right_slab.parameters, self.solvent.parameters ]) return p def logp(self): return 0 @property def slabs(self): num_slabs = np.ceil(float(self.extent) / self.microslab_max_thickness) slab_thick = float(self.extent / num_slabs) slabs = np.zeros((int(num_slabs), 5)) slabs[:, 0] = slab_thick # give last slab a miniscule roughness so it doesn't get contracted slabs[-1:, 3] = 0.5 dist = np.cumsum(slabs[..., 0]) - 0.5 * slab_thick slabs[:, 1] = self(dist) return slabs
def setup_method(self): self.a = Parameter(1, name='a') self.b = Parameter(2, name='b') self.m = Parameters() self.m.append(self.a) self.m.append(self.b)
class TestParameters(object): def setup_method(self): self.a = Parameter(1, name='a') self.b = Parameter(2, name='b') self.m = Parameters() self.m.append(self.a) self.m.append(self.b) def test_retrieve_by_name(self): p = self.m['a'] assert_(p is self.a) # or by index p = self.m[0] assert_(p is self.a) def test_repr(self): p = Parameter(value=5, vary=False, name='test') g = Parameters(name='name') f = Parameters() f.append(p) f.append(g) q = eval(repr(f)) assert (q.name is None) assert_equal(q[0].value, 5) assert (q[0].vary is False) assert (isinstance(q[1], Parameters)) def test_set_by_name(self): c = Parameter(3.) self.m['a'] = c assert_(self.m[0] is c) # can't set an entry by name, if there isn't an existing name in this # Parameters instance. from pytest import raises with raises(ValueError): self.m['abc'] = c def test_parameters(self): # we've added two parameters self.a.vary = True self.b.vary = True assert_equal(len(self.m.flattened()), 2) # the two entries should just be the objects assert_(self.m.varying_parameters()[0] is self.a) assert_(self.m.varying_parameters()[1] is self.b) def test_varying_parameters(self): # even though we've added a twice we should still only see 2 # varying parameters self.a.vary = True self.b.vary = True p = self.a | self.b | self.a assert_equal(len(p.varying_parameters()), 2) def test_pickle_parameters(self): # need to check that Parameters can be pickled/unpickle pkl = pickle.dumps(self.m) pickle.loads(pkl) def test_or(self): # concatenation of Parameters # Parameters with Parameter c = self.m | self.b assert_equal(len(c), 3) assert_equal(len(c.flattened()), 3) assert_(c.flattened()[1] is self.b) assert_(c.flattened()[2] is self.b) # Parameters with Parameters c = Parameters(name='c') d = c | self.m assert_(d.name == 'c') def test_ior(self): # concatenation of Parameters # Parameters with Parameter c = Parameters(name='c') c |= self.b assert_equal(len(c), 1) assert_equal(len(c.flattened()), 1) assert_(c.flattened()[0] is self.b) # Parameters with Parameters c = Parameters(name='c') c |= self.m assert_(c.name == 'c') assert_equal(len(c), 1) assert_equal(len(c.flattened()), 2) assert_(c.flattened()[1] is self.b)
class SLD(object): """ Object representing freely varying SLD of a material Parameters ---------- value : float, complex, Parameter, Parameters Scattering length density of a material. Units (10**-6 Angstrom**-2) name : str, optional Name of material. Notes ----- An SLD object can be used to create a Slab: >>> # an SLD object representing Silicon Dioxide >>> sio2 = SLD(3.47, name='SiO2') >>> # create a Slab of SiO2 20 A in thickness, with a 3 A roughness >>> sio2_layer = sio2(20, 3) The SLD object can also be made from a complex number, or from Parameters >>> sio2 = SLD(3.47+0.01j) >>> re = Parameter(3.47) >>> im = Parameter(0.01) >>> sio2 = SLD(re) >>> sio2 = SLD([re, im]) """ def __init__(self, value, name=''): self.name = name self.imag = Parameter(0, name='%s - isld' % name) if isinstance(value, numbers.Real): self.real = Parameter(value.real, name='%s - sld' % name) elif isinstance(value, numbers.Complex): self.real = Parameter(value.real, name='%s - sld' % name) self.imag = Parameter(value.imag, name='%s - isld' % name) elif isinstance(value, SLD): self.real = value.real self.imag = value.imag elif isinstance(value, Parameter): self.real = value elif (hasattr(value, '__len__') and isinstance(value[0], Parameter) and isinstance(value[1], Parameter)): self.real = value[0] self.imag = value[1] self._parameters = Parameters(name=name) self._parameters.extend([self.real, self.imag]) def __repr__(self): return ("SLD([{real!r}, {imag!r}]," " name={name!r})".format(**self.__dict__)) def __str__(self): sld = complex(self.real.value, self.imag.value) return 'SLD = {0} x10**-6 Å**-2'.format(sld) def __complex__(self): return complex(self.real.value, self.imag.value) def __call__(self, thick=0, rough=0): """ Create a :class:`Slab`. Parameters ---------- thick: refnx.analysis.Parameter or float Thickness of slab in Angstrom rough: refnx.analysis.Parameter or float Roughness of slab in Angstrom Returns ------- slab : refnx.reflect.Slab The newly made Slab. Example -------- >>> # an SLD object representing Silicon Dioxide >>> sio2 = SLD(3.47, name='SiO2') >>> # create a Slab of SiO2 20 A in thickness, with a 3 A roughness >>> sio2_layer = sio2(20, 3) """ return Slab(thick, self, rough, name=self.name) def __or__(self, other): # c = self | other slab = self() return slab | other @property def parameters(self): """ :class:`refnx.analysis.Parameters` associated with this component """ self._parameters.name = self.name return self._parameters
def parameters(self): p = Parameters(name=self.name) p.extend([self.gamma, self.dz, self.vf, self.polymer_sld.parameters]) p.extend([slab.parameters for slab in self.left_slabs]) p.extend([slab.parameters for slab in self.right_slabs]) return p
class FreeformVFPextent(Component): """ Freeform volume fraction profiles for a polymer brush. The extent of the brush is used as a fitting parameter. Parameters ---------- extent : Parameter or float The total extent of the spline region vf: sequence of Parameter or float Absolute volume fraction at each of the spline knots dz : sequence of Parameter or float Separation of successive knots, expressed as a fraction of `extent`. polymer_sld : SLD or float SLD of polymer name : str Name of component gamma : Parameter The dry adsorbed amount of polymer left_slabs : sequence of Slab Slabs to the left of the spline right_slabs : sequence of Slab Slabs to the right of the spline interpolator : scipy interpolator The interpolator for the spline zgrad : bool, optional Set to `True` to force the gradient of the volume fraction to zero at each end of the spline. monotonic_penalty : number, optional The penalty added to the log-probability to penalise non-monotonic spline knots. Set to a very large number (e.g. 1e250) to enforce a monotonically decreasing volume fraction spline. Set to a very negative number (e.g. -1e250) to enforce a monotonically increasing volume fraction spline. Set to zero (default) to apply no penalty. Note - the absolute value of `monotonic_penalty` is subtracted from the overall log-probability, the sign is only used to determine the direction that is requested. microslab_max_thickness : float Thickness of microslicing of spline for reflectivity calculation. """ def __init__(self, extent, vf, dz, polymer_sld, name='', gamma=None, left_slabs=(), right_slabs=(), interpolator=Pchip, zgrad=True, monotonic_penalty=0, microslab_max_thickness=1): self.name = name if isinstance(polymer_sld, SLD): self.polymer_sld = polymer_sld else: self.polymer_sld = SLD(polymer_sld) # left and right slabs are other areas where the same polymer can # reside self.left_slabs = [ slab for slab in left_slabs if isinstance(slab, Slab) ] self.right_slabs = [ slab for slab in right_slabs if isinstance(slab, Slab) ] self.microslab_max_thickness = microslab_max_thickness self.extent = (possibly_create_parameter(extent, name='%s - spline extent' % name)) # dz are the spatial spacings of the spline knots self.dz = Parameters(name='dz - spline') for i, z in enumerate(dz): p = possibly_create_parameter(z, name='%s - spline dz[%d]' % (name, i)) p.range(0, 1) self.dz.append(p) # vf are the volume fraction values of each of the spline knots self.vf = Parameters(name='vf - spline') for i, v in enumerate(vf): p = possibly_create_parameter(v, name='%s - spline vf[%d]' % (name, i)) p.range(0, 1) self.vf.append(p) if len(self.vf) != len(self.dz): raise ValueError("dz and vs must have same number of entries") self.monotonic_penalty = monotonic_penalty self.zgrad = zgrad self.interpolator = interpolator if gamma is not None: self.gamma = possibly_create_parameter(gamma, 'gamma') else: self.gamma = Parameter(0, 'gamma') self.__cached_interpolator = { 'zeds': np.array([]), 'vf': np.array([]), 'interp': None, 'extent': -1 } def _vfp_interpolator(self): """ The spline based volume fraction profile interpolator Returns ------- interpolator : scipy.interpolate.Interpolator """ dz = np.array(self.dz) zeds = np.cumsum(dz) # if dz's sum to more than 1, then normalise to unit interval. # clipped to 0 and 1 because we pad on the LHS, RHS later # and we need the array to be monotonically increasing if zeds[-1] > 1: zeds /= zeds[-1] zeds = np.clip(zeds, 0, 1) vf = np.array(self.vf) # use the volume fraction of the last left_slab as the initial vf of # the spline if len(self.left_slabs): left_end = 1 - self.left_slabs[-1].vfsolv.value else: left_end = vf[0] # in contrast use a vf = 0 for the last vf of # the spline, unless right_slabs is specified if len(self.right_slabs): right_end = 1 - self.right_slabs[0].vfsolv.value else: right_end = 0 # do you require zero gradient at either end of the spline? if self.zgrad: zeds = np.concatenate([[-1.1, 0 - EPS], zeds, [1 + EPS, 2.1]]) vf = np.concatenate([[left_end, left_end], vf, [right_end, right_end]]) else: zeds = np.concatenate([[0 - EPS], zeds, [1 + EPS]]) vf = np.concatenate([[left_end], vf, [right_end]]) # cache the interpolator cache_zeds = self.__cached_interpolator['zeds'] cache_vf = self.__cached_interpolator['vf'] cache_extent = self.__cached_interpolator['extent'] # you don't need to recreate the interpolator if (np.array_equal(zeds, cache_zeds) and np.array_equal(vf, cache_vf) and np.equal(self.extent, cache_extent)): return self.__cached_interpolator['interp'] else: self.__cached_interpolator['zeds'] = zeds self.__cached_interpolator['vf'] = vf self.__cached_interpolator['extent'] = float(self.extent) # TODO make vfp zero for z > self.extent interpolator = self.interpolator(zeds, vf) self.__cached_interpolator['interp'] = interpolator return interpolator def __call__(self, z): """ Calculates the volume fraction profile of the spline Parameters ---------- z : float Distance along vfp Returns ------- vfp : float Volume fraction """ interpolator = self._vfp_interpolator() vfp = interpolator(z / float(self.extent)) return vfp def moment(self, moment=1): """ Calculates the n'th moment of the profile Parameters ---------- moment : int order of moment to be calculated Returns ------- moment : float n'th moment """ zed, profile = self.profile() profile *= zed**moment val = simps(profile, zed) area = self.profile_area() return val / area @property def parameters(self): p = Parameters(name=self.name) p.extend([ self.extent, self.dz, self.vf, self.polymer_sld.parameters, self.gamma ]) p.extend([slab.parameters for slab in self.left_slabs]) p.extend([slab.parameters for slab in self.right_slabs]) return p def logp(self): logp = 0 # you're trying to enforce monotonicity if self.monotonic_penalty: monotonic, direction = _is_monotonic(self.vf) # if left slab has a lower vf than first spline then profile is # not monotonic if self.vf[0] > (1 - self.left_slabs[-1].vfsolv): monotonic = False if not monotonic: # you're not monotonic so you have to have the penalty # anyway logp -= np.abs(self.monotonic_penalty) else: # you are monotonic, but might be in the wrong direction if self.monotonic_penalty > 0 and direction > 0: # positive penalty means you want decreasing logp -= np.abs(self.monotonic_penalty) elif self.monotonic_penalty < 0 and direction < 0: # negative penalty means you want increasing logp -= np.abs(self.monotonic_penalty) # log-probability for area under profile logp += self.gamma.logp(self.profile_area()) return logp def profile_area(self): """ Calculates integrated area of volume fraction profile Returns ------- area: integrated area of volume fraction profile """ interpolator = self._vfp_interpolator() area = interpolator.integrate(0, 1) * float(self.extent) for slab in self.left_slabs: _slabs = slab.slabs() area += _slabs[0, 0] * (1 - _slabs[0, 4]) for slab in self.right_slabs: _slabs = slab.slabs() area += _slabs[0, 0] * (1 - _slabs[0, 4]) return area def slabs(self, structure=None): num_slabs = np.ceil(float(self.extent) / self.microslab_max_thickness) slab_thick = float(self.extent / num_slabs) slabs = np.zeros((int(num_slabs), 5)) slabs[:, 0] = slab_thick # give last slab a miniscule roughness so it doesn't get contracted slabs[-1:, 3] = 0.5 dist = np.cumsum(slabs[..., 0]) - 0.5 * slab_thick slabs[:, 1] = self.polymer_sld.real.value slabs[:, 2] = self.polymer_sld.imag.value slabs[:, 4] = 1 - self(dist) return slabs def profile(self, extra=False): """ Calculates the volume fraction profile Returns ------- z, vfp : np.ndarray Distance from the interface, volume fraction profile """ s = Structure() s |= SLD(0) m = SLD(1.) for i, slab in enumerate(self.left_slabs): layer = m(slab.thick.value, slab.rough.value) if not i: layer.rough.value = 0 layer.vfsolv.value = slab.vfsolv.value s |= layer polymer_slabs = self.slabs() offset = np.sum(s.slabs()[:, 0]) for i in range(np.size(polymer_slabs, 0)): layer = m(polymer_slabs[i, 0], polymer_slabs[i, 3]) layer.vfsolv.value = polymer_slabs[i, -1] s |= layer for i, slab in enumerate(self.right_slabs): layer = m(slab.thick.value, slab.rough.value) layer.vfsolv.value = 1 - slab.vfsolv.value s |= layer s |= SLD(0, 0) # now calculate the VFP. total_thickness = np.sum(s.slabs()[:, 0]) zed = np.linspace(0, total_thickness, total_thickness + 1) # SLD profile puts a very small roughness on the interfaces with zero # roughness. zed[0] = 0.01 z, s = s.sld_profile(z=zed) s[0] = s[1] # perhaps you'd like to plot the knot locations zeds = np.cumsum(self.dz) if np.sum(self.dz) > 1: zeds /= np.sum(self.dz) zeds = np.clip(zeds, 0, 1) zed_knots = zeds * float(self.extent) + offset if extra: return z, s, zed_knots, np.array(self.vf) else: return z, s
def __init__(self, gamma, vf, dz, polymer_sld, name='', left_slabs=(), right_slabs=(), interpolator=Pchip, zgrad=True, monotonic_penalty=0, microslab_max_thickness=1): self.name = name self.gamma = (possibly_create_parameter(gamma, name='%s - adsorbed amount' % name)) if isinstance(polymer_sld, SLD): self.polymer_sld = polymer_sld else: self.polymer_sld = SLD(polymer_sld) # left and right slabs are other areas where the same polymer can # reside self.left_slabs = [ slab for slab in left_slabs if isinstance(slab, Slab) ] self.right_slabs = [ slab for slab in right_slabs if isinstance(slab, Slab) ] self.microslab_max_thickness = microslab_max_thickness # dz are the spatial spacings of the spline knots self.dz = Parameters(name='dz - spline') for i, z in enumerate(dz): p = possibly_create_parameter(z, name='%s - spline dz[%d]' % (name, i)) p.range(0, 1) self.dz.append(p) # vf are the volume fraction values of each of the spline knots self.vf = Parameters(name='vf - spline') for i, v in enumerate(vf): p = possibly_create_parameter(v, name='%s - spline vf[%d]' % (name, i)) p.range(0, 2.) self.vf.append(p) if len(self.vf) != len(self.dz): raise ValueError("dz and vs must have same number of entries") self.monotonic_penalty = monotonic_penalty self.zgrad = zgrad self.interpolator = interpolator self.__cached_interpolator = { 'zeds': np.array([]), 'vf': np.array([]), 'interp': None }
def parameters(self): p = Parameters(name=self.name) p.extend([self.extent, self.decay, self.rough]) return p
class RI(Scatterer): """ Object representing a materials wavelength-dependent refractive index. A concern is how it needs to be linked to a model. This is to get around a major rewrite of refnx, but isn't the most elegant system. Another issue is that optical parameters are supplied in units of micro meters ('cause thats what seems to be used in refractive index repos and cauchy models), the wavelength of the incident radiation is supplied in nanometers (thats typical) and the fitting is done in angstroms. Very unpleasent. Parameters ---------- value : tuple, string Scattering length density of a material. Units (10**-6 Angstrom**-2) A : float or parameter Cauchy parameter A. If not none RI will use the cauchy model. Default None. B : float or parameter Cauchy parameter B in um^2. Default 0. C : float or parameter Cauchy parameter C in um^4. Default 0. name : str, optional Name of material. Notes ----- An SLD object can be used to create a Slab: >>> # an SLD object representing Silicon Dioxide >>> sio2 = SLD(3.47, name='SiO2') >>> # create a Slab of SiO2 20 A in thickness, with a 3 A roughness >>> sio2_layer = sio2(20, 3) The SLD object can also be made from a complex number, or from Parameters >>> sio2 = SLD(3.47+0.01j) >>> re = Parameter(3.47) >>> im = Parameter(0.01) >>> sio2 = SLD(re) >>> sio2 = SLD([re, im]) """ def __init__(self, value=None, A=None, B=0, C=0, name=""): if (type(value) is str and name == ""): # if there is no name get it from the path name = os.path.basename(value).split(".")[0] super(RI, self).__init__(name=name) assert np.logical_xor( value is None, A is None), "Supply either values or cauchy parameters" if value is not None: if type(value) is str: try: self._wav, self._RI, self._EC = np.loadtxt( value, skiprows=1, delimiter=",", encoding="utf8").T except ValueError: self._wav, self._RI = np.loadtxt( value, skiprows=1, delimiter=",", usecols=[0, 1], encoding="utf8", ).T self._EC = np.zeros_like(self._wav) elif len(value) == 2: self._RI, self._EC = value self._wav = None elif len(value) == 3: self._wav, self._RI, self._EC = value else: raise TypeError("format not recognised") # convert wavelength from um to nm self._wav = self._wav * 1000 else: self._wav = None self._RI = None self._EC = None self.model = None self.set_wav = None self._default_wav = 658 self._parameters = Parameters(name=name) if A is not None: self.A = possibly_create_parameter(A, name=f"{name} - cauchy A") self.B = possibly_create_parameter(B, name=f"{name} - cauchy B") self.C = possibly_create_parameter(C, name=f"{name} - cauchy C") self._parameters.extend([self.A, self.B, self.C]) # The RI needs access to the model to calculate the refractive index. # Can't think of a better way of doing this # reflect_modelSE is going to auto-link this when its called. @property def real(self): """Refractive index, n.""" if self.model is not None: wavelength = self.model.wav elif self.set_wav is not None: wavelength = self.set_wav else: wavelength = self._default_wav warnings.warn("Using default wavelength (model not linked)") if np.any(self._wav): # TODO - raise a warning if the wavelength supplied is outside the # wavelength range covered by the data file. return Parameter(np.interp(wavelength, self._wav, self._RI)) elif self.A is not None: return Parameter(self.A.value + (self.B.value * 1000**2) / (wavelength**2) + (self.C.value**1000**4) / (wavelength**4)) else: return Parameter(value=self._RI) @property def imag(self, wavelength=None): """Extinction coefficent, k.""" if self.model is not None: wavelength = self.model.wav elif self.set_wav is not None: wavelength = self.set_wav else: wavelength = self._default_wav warnings.warn("Using default wavelength (model not linked)") if np.any(self._wav): # TODO - raise a warning if the wavelength supplied is outside the # wavelength range covered by the data file. return Parameter(np.interp(wavelength, self._wav, self._EC)) elif self.A is not None: return Parameter(0) else: return Parameter(value=self._EC) @property def parameters(self): return self._parameters def __repr__(self): return str(f"n: {self.real.value}, k: {self.imag.value}") def __complex__(self): sldc = complex(self.real.value, self.imag.value) return sldc