def test_pymc3(self): # test objective logl against pymc3 # don't run this test if pymc3 is not installed try: import pymc3 as pm except ImportError: return logl = self.objective.logl() from refnx.analysis import pymc_objective from refnx.analysis.objective import _to_pymc3_distribution mod = pymc_objective(self.objective) with mod: pymc_logl = mod.logp({ 'p0': self.p[0].value, 'p1': self.p[1].value }) assert_allclose(logl, pymc_logl) # now check some of the distributions with pm.Model(): p = Parameter(1, bounds=(1, 10)) d = _to_pymc3_distribution('a', p) assert_almost_equal(d.distribution.logp(2).eval(), p.logp(2)) assert_(np.isneginf(d.distribution.logp(-1).eval())) q = Parameter(1, bounds=PDF(stats.uniform(1, 9))) d = _to_pymc3_distribution('b', q) assert_almost_equal(d.distribution.logp(2).eval(), q.logp(2)) assert_(np.isneginf(d.distribution.logp(-1).eval())) p = Parameter(1, bounds=PDF(stats.uniform)) d = _to_pymc3_distribution('c', p) assert_almost_equal(d.distribution.logp(0.5).eval(), p.logp(0.5)) p = Parameter(1, bounds=PDF(stats.norm)) d = _to_pymc3_distribution('d', p) assert_almost_equal(d.distribution.logp(2).eval(), p.logp(2)) p = Parameter(1, bounds=PDF(stats.norm(1, 10))) d = _to_pymc3_distribution('e', p) assert_almost_equal(d.distribution.logp(2).eval(), p.logp(2))
def test_parameter_bounds(self): x = Parameter(4, bounds=Interval(-4, 4)) assert_equal(x.logp(), uniform.logpdf(0, -4, 8)) x.bounds = None assert_(isinstance(x._bounds, Interval)) assert_equal(x.bounds.lb, -np.inf) assert_equal(x.bounds.ub, np.inf) assert_equal(x.logp(), 0) x.setp(bounds=norm(0, 1)) assert_almost_equal(x.logp(1), norm.logpdf(1, 0, 1)) # all created parameters were mistakenly being given the same # default bounds instance! x = Parameter(4) y = Parameter(5) assert_(id(x.bounds) != id(y.bounds))
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