def test_construction(self): # import here to get the tests to work with Python 2.7 on linux from GTC.context import _context x_value = 1.2 x_u = 0.5 x_dof = 6 x1 = UncertainReal._elementary(x_value, x_u, x_dof, None, independent=True) x2 = UncertainReal._elementary(x_value, x_u, x_dof, None, independent=True) # uid's must be in order self.assertTrue(x1._node.uid < x2._node.uid) self.assertEqual(x_dof, x1._node.df) self.assertEqual(x_dof, x2._node.df) self.assertTrue( _context._registered_leaf_nodes[x1._node.uid].df == x_dof) self.assertTrue( _context._registered_leaf_nodes[x2._node.uid].df == x_dof) # illegal dof is checked when the object is created self.assertRaises(ValueError, UncertainReal._elementary, x_value, x_u, 0, None, False)
def _builder(o_name, _nodes, _tagged_reals): """ Construct an intermediate un object for `o_name`. """ obj = _tagged_reals[o_name] if isinstance(obj, TaggedElementaryReal): un = UncertainReal._archived_elementary(uid=obj.uid, x=obj.x) _tagged_reals[o_name] = un elif isinstance(obj, TaggedIntermediateReal): un = UncertainReal( obj.value, _vector_index_to_node(obj.u_components), _vector_index_to_node(obj.d_components), _ivector_index_to_node(obj.i_components, _nodes), _nodes[obj.uid], ) _tagged_reals[o_name] = un else: assert False, "unexpected: {!r}".format(obj) return un
def implicit_real(fn, x_min, x_max, epsilon): """Return the uncertain real number ``x``, that solves :math:`fn(x) = 0` The function fn() must take a single argument and x_min and x_max must define a range in which there is one (and only one) sign change in fn(x). The number 'epsilon' is a tolerance for accepting convergence. A RuntimeError will be raised if the root-search algorithm fails. An AssertionError will be raised if the preconditions for a search to begin are not satisfied. Parameters ---------- fn : a function of one argument x_min, x_max, epsilon : float Returns ------- UncertainReal .. versionadded:: 1.3.4 """ xk, dy_dx = nr_get_root(fn, x_min, x_max, epsilon) # In an implicit function F(x,...) = 0, where # we solve F = 0 by finding a value for `x`, # `x` depends implicitly on the other arguments. # The influence set of `F` and the influence set # of `x` should are the same. # `x` is not an elementary uncertain number; # it is implicitly a function of the other # arguments to F(). # The components of uncertainty of `x` are related to # the components of `F` as follows: # u_i(x) = -( dF/dx_i / dF/dx ) * u_i(xi) y = fn(UncertainReal._constant(xk)) dx_dy = -1 / dy_dx return UncertainReal(xk, vector.scale_vector(y._u_components, dx_dy), vector.scale_vector(y._d_components, dx_dy), vector.scale_vector(y._i_components, dx_dy))
def test(self): x = ureal(1, 1, 4) self.assertEqual(4, x.df) self.assertEqual(4, welch_satterthwaite(x)[1]) # product with zero values x1 = ureal(0, 1, 4) x2 = ureal(0, 1, 3) y = x1 * x2 self.assertEqual(0, y.u) self.assertTrue(nan is y.df) # Pathological case - not sure it can be created in practice x1 = ureal(1, 1) x2 = UncertainReal._elementary(1, 0, 4, label=None, independent=True) x3 = UncertainReal._elementary(1, 0, 3, label=None, independent=True) y = x1 + x2 + x3 self.assertEqual(len(y._u_components), 3) self.assertTrue(inf is y.df)
def nr_get_root(fn, x_min, x_max, epsilon): """Return the x-location of the root and the derivative at that point. This is a utility function used by implicit_real(). It searches within the range for a real root. Parameters ---------- fn : a function with a single argument x_min, x_max, epsilon : float Returns ------- (float,float) .. versionadded:: 1.3.4 """ if x_max <= x_min: raise RuntimeError("Invalid search range: {!s}".format((x_min, x_max))) lower, upper = x_min, x_max ureal = lambda x, u: UncertainReal._elementary(x, u, inf, None, True) value = lambda x: x.x if isinstance(x, UncertainReal) else float(x) x = ureal(lower, 1.0) f_x = fn(x) fl = value(f_x) assert isinstance(f_x,UncertainReal),\ "fn() must return an UncertainReal, got: %s" % type(f_x) if abs(fl) < epsilon: return fl, f_x.sensitivity(x) x = ureal(upper, 1.0) f_x = fn(x) fu = value(f_x) if abs(fu) < epsilon: return fu, f_x.sensitivity(x) if fl * fu >= 0.0: raise RuntimeError( "range does not appear to contain a root: {}".format((fl, fu))) if fl > 0.0: lower, upper = upper, lower # First place to look is the middle xk = (lower + upper) / 2.0 dx2 = abs(upper - lower) dx = dx2 x = ureal(xk, 1.0) f_x = fn(x) f = value(f_x) df = f_x.sensitivity(x) for i in xrange(100): if (((xk - upper) * df - f) * ((xk - lower) * df - f) > 0.0 or (abs(2.0 * f) > abs(dx2 * df))): # Bisect if Newton out of range or # not decreasing fast enough. dx2 = dx dx = (upper - lower) / 2.0 # If the bisection is too small then the root is found if (abs(dx) <= epsilon): return xk, df else: xk = lower + dx else: # Use Newton step dx2 = dx dx = f / df # If the change is ~ 0 then accept the root if (abs(dx) <= epsilon): return xk, df else: xk -= dx # Test convergence if (abs(dx) <= epsilon): return xk, df # Evaluate for next iteration; x = ureal(xk, 1.0) f_x = fn(x) f = value(f_x) df = f_x.sensitivity(x) if (f < 0.0): lower = xk else: upper = xk raise RuntimeError("Failed to converge")
def line_fit_wtls(x, y, u_x=None, u_y=None, a_b=None, r_xy=None): """Perform straight-line regression with uncertainty in ``x`` and ``y`` .. versionadded:: 1.2 :arg x: list of uncertain real numbers for the independent variable :arg y: list of uncertain real numbers for the dependent variable :arg u_x: a sequence of uncertainties for the ``x`` data :arg u_y: a sequence of uncertainties for the ``y`` data :arg a_b: a pair of initial estimates for the intercept and slope :arg r_xy: correlation between x-y pairs [default: 0] Returns a :class:`~type_b.LineFitWTLS` object The elements of ``x`` and ``y`` must be uncertain numbers with non-zero uncertainties. If specified, the optional arguments ``u_x`` and ``u_y`` will be used uncertainties to weight the data for the regression, otherwise the uncertainties of the uncertain numbers in the sequences are used. The optional argument ``a_b`` can be used to provide a pair of initial estimates for the intercept and slope. Otherwise, initial estimates will be obtained by calling `line_fit_wls`. Implements a Weighted Total Least Squares algorithm that allows for correlation between x-y pairs. See reference: M Krystek and M Anton, *Meas. Sci. Technol.* **22** (2011) 035101 (9pp) **Example**:: # Pearson-York test data # see, e.g., Lybanon, M. in Am. J. Phys 52 (1), January 1984 >>> xin=[0.0,0.9,1.8,2.6,3.3,4.4,5.2,6.1,6.5,7.4] >>> wx=[1000.0,1000.0,500.0,800.0,200.0,80.0,60.0,20.0,1.8,1.0] >>> yin=[5.9,5.4,4.4,4.6,3.5,3.7,2.8,2.8,2.4,1.5] >>> wy=[1.0,1.8,4.0,8.0,20.0,20.0,70.0,70.0,100.0,500.0] # Convert weights to standard uncertainties >>> uxin=[1./math.sqrt(wx_i) for wx_i in wx ] >>> uyin=[1./math.sqrt(wy_i) for wy_i in wy ] # Define uncertain numbers >>> x = [ ureal(xin_i,uxin_i) for xin_i,uxin_i in zip(xin,uxin) ] >>> y = [ ureal(yin_i,uyin_i) for yin_i,uyin_i in zip(yin,uyin) ] # TLS returns uncertain numbers >>> a,b = type_b.line_fit_wtls(x,y).a_b >>> a ureal(5.47991018...,0.29193349...,inf) >>> b ureal(-0.48053339...,0.057616740...,inf) """ N = len(x) if N != len(y): raise RuntimeError( "Different sequence lengths: len({!r}) != len({!r})".format(x, y)) if (u_x is not None or u_y is not None): if (u_x is None or u_y is None): raise RuntimeError("You must supply ``u_x`` and ``u_y``") elif (r_xy is None): # default value will be uncorrelated r_xy = [0] * len(u_x) if len(u_x) != N or len(u_y) != N: raise RuntimeError( "incompatible sequence lengths: {!r}, {!r}".format(u_x, u_y)) for x_i, y_i in izip(x, y): assert isinstance(x_i, UncertainReal), 'uncertain real required' assert isinstance(y_i, UncertainReal), 'uncertain real required' # Needed to define UNs locally ureal = lambda x, u: UncertainReal._elementary(x, u, inf, None, True) if a_b is None: a_b = line_fit_wls(x, y, u_y).a_b a0 = value(a_b[0]) b0 = value(a_b[1]) # initial value for `alpha` alpha0 = math.atan(b0) # chi_sq(alpha0) -> chisquared chi_sq = ChiSq(x, y, u_x, u_y, r_xy) # Search for the minimum chi-squared wrt alpha x1 = alpha0 - HALF_PI x2 = alpha0 + HALF_PI # `brent` requires three points that bracket the minimum. # the `x1`, `alpha0`, `x2` parameters should be real, # but `data` will return an uncertain number # and expects an uncertain number argument. # # Returns x, fn(x) and df_dx(x), all floats alpha1, fn_alpha1, df_alpha1 = _dbrent(x1, alpha0, x2, chi_sq) # dChiSq_a( alpha ) will return dChiSq_dalpha(`alpha`) # dChiSq_a(alpha0) -> 1st partial derivative of chisquared at alpha0 dChiSq_a = dChiSq_dalpha(x, y, u_x, u_y, r_xy) # Need the partial derivative of dChiSq_a wrt alpha alpha = ureal(alpha1, 1) F_alpha = dChiSq_a(alpha) dalpha_dF = -1.0 / F_alpha.sensitivity(alpha) # Now we define `alpha` with sensitivity to the ``x`` and ``y`` data, # via the object ``F_alpha``, which represents the 1st partial derivative # of chi-squared at alpha1 (ideally zero, but really only close to the root). F_alpha = dChiSq_a(UncertainReal._constant(alpha1)) alpha = UncertainReal(alpha1, scale_vector(F_alpha._u_components, dalpha_dF), scale_vector(F_alpha._d_components, dalpha_dF), scale_vector(F_alpha._i_components, dalpha_dF)) # The sensitivity of p_hat to the x and y data is via # `alpha`, `x_bar` and `y_bar` in eqn (43) p_hat = chi_sq.p_hat(alpha) # Note we have reversed the definitions of `a` and `b` here b = alpha._tan() a = p_hat / alpha._cos() N = len(x) ssr = chi_sq(UncertainReal._constant(alpha1)).x return LineFitWTLS(a, b, ssr, N)
def _dbrent(ax, bx, cx, fn, tol=math.sqrt(EPSILON)): """ Minimise fn() and return x, fn(x) and df_dx(x), all floats `fn` must be a univariate function of an uncertain real that returns an uncertain real number. `ax`, `bx` and `cx` must be floats. `bx` must be between `ax` and `cx` and fn(bx) must be less than both fn(ax) and fn(cx). `context` - a GTC context `tol` - the fractional precision See also Numerical Recipes in C, 2nd ed, Section 10.3 """ ITMAX = 100 deriv = lambda y, x: y.sensitivity(x) ureal = lambda x, u: UncertainReal._elementary(x, u, inf, None, True) e = 0.0 # The distance moved on the step before last a = ax if ax < cx else cx b = ax if ax > cx else cx assert a <= bx and bx <= b, "Invalid initial values in _dbrent" # if fn( ureal(bx,1)) > fn( ureal(a,1)) or fn(ureal(bx,1)) > fn(ureal(b,1)): # assert False x = w = v = bx _u_ = ureal(x, 1.0) fn_u = fn(_u_) fw = fv = fx = value(fn_u) dw = dv = dx = deriv(fn_u, _u_) # The routine keeps track of `a` and `b`, which bracket the minimum, # `x` is the point with the least function value found so far, # `w` is the point with the second least value, `v` is the previous # value of `w`, `u` is the point at which the function was most # recently evaluated. for i in xrange(ITMAX): xm = 0.5 * (a + b) tol1 = tol * abs(x) + ZEPS tol2 = 2.0 * tol1 if abs(x - xm) <= (tol2 - 0.5 * (b - a)): return x, fx, dx if abs(e) > tol1: # initialise the d's to be out of bracket d1 = 2.0 * (b - a) d2 = d1 # Secant method if dw != dx: d1 = (w - x) * dx / (dx - dw) if dv != dx: d2 = (v - x) * dx / (dx - dv) # Choose one estimate. # Insist that it be within the bracket # and on the side pointed to by the derivative at `x` u1 = x + d1 u2 = x + d2 OK1 = (a - u1) * (u1 - b) > 0.0 and dx * d1 <= 0.0 OK2 = (a - u2) * (u2 - b) > 0.0 and dx * d2 <= 0.0 olde, e = e, d if OK1 or OK2: if OK1 and OK2: d = d1 if abs(d1) < abs(d2) else d2 elif OK1: d = d1 else: d = d2 if abs(d) <= abs(0.5 * olde): u = x + d if (u - a < tol2) or (b - u < tol2): d = math.copysign(tol1, xm - x) else: # choose segment by the sign of the derivative e = a - x if dx >= 0.0 else b - x d = 0.5 * e else: e = a - x if dx >= 0.0 else b - x d = 0.5 * e else: e = a - x if dx >= 0.0 else b - x d = 0.5 * e if abs(d) >= tol1: u = x + d _u_ = ureal(u, 1.0) fn_u = fn(_u_) fu = value(fn_u) du = deriv(fn_u, _u_) else: # Smallest step possible u = x + math.copysign(tol1, d) _u_ = ureal(u, 1.0) fn_u = fn(_u_) fu = value(fn_u) du = deriv(fn_u, _u_) # If the minimum sized step downhill # goes up, then we are done! if fu > fx: return u, fu, du assert a <= u and u <= b, (a, u, b) # invariant if fu <= fx: # Found a new best point # Update the bracket on one side so that the previous # `x` value is now the limit and the new `x` value is # contained. if u >= x: a = x else: b = x v, fv, dv = w, fw, dw w, fw, dw = x, fx, dx x, fx, dx = u, fu, du assert a <= x and x <= b, (a, x, b) # invariant else: # The point `x` has not been bettered # `x` does not change, but `u` was inside the # bracket so we can tighten the noose. if u < x: a = u else: b = u # `w` is the second best point and `v` is the 3rd best if (fu <= fw) or (x == w): v, fv, dv = w, fw, dw w, fw, dw = u, fu, du elif (fu < fv) or (v == x) or (v == w): v, fv, dv = u, fu, du assert a <= x and x <= b, (a, x, b) # invariant assert fx <= fw and fx <= fv # invariant raise RuntimeError('Exceeded iteration limit in `_dbrent`')