Пример #1
0
def test_applyo():
    x = var()
    assert run(0, x, applyo("add", (1, 2, 3), x)) == (("add", 1, 2, 3), )
    assert run(0, x, applyo(x, (1, 2, 3), ("add", 1, 2, 3))) == ("add", )
    assert run(0, x, applyo("add", x, ("add", 1, 2, 3))) == ((1, 2, 3), )

    a_lv, b_lv, c_lv = var(), var(), var()

    from operator import add

    assert run(0, c_lv, applyo(add, (1, 2), c_lv)) == (3, )
    assert run(0, c_lv, applyo(add, etuple(1, 2), c_lv)) == (3, )
    assert run(0, c_lv, applyo(add, a_lv, c_lv)) == (cons(add, a_lv), )

    for obj in (
        (1, 2, 3),
        (add, 1, 2),
        [1, 2, 3],
        [add, 1, 2],
            etuple(1, 2, 3),
            etuple(add, 1, 2),
    ):
        o_rator, o_rands = operator(obj), arguments(obj)
        assert run(0, a_lv, applyo(o_rator, o_rands,
                                   a_lv)) == (term(o_rator, o_rands), )
        # Just acts like `conso` here
        assert run(0, a_lv, applyo(o_rator, a_lv, obj)) == (arguments(obj), )
        assert run(0, a_lv, applyo(a_lv, o_rands, obj)) == (operator(obj), )

    # Just acts like `conso` here, too
    assert run(0, c_lv, applyo(a_lv, b_lv, c_lv)) == (cons(a_lv, b_lv), )

    # with pytest.raises(ConsError):
    assert run(0, a_lv, applyo(a_lv, b_lv, object())) == ()
    assert run(0, a_lv, applyo(1, 2, a_lv)) == ()
Пример #2
0
def test_assoccomm():
    x, a, b, c = tt.dvectors("xabc")
    test_expr = x + 1
    q = var()

    res = run(1, q, applyo(tt.add, etuple(*test_expr.owner.inputs), test_expr))
    assert q == res[0]

    res = run(1, q, applyo(q, etuple(*test_expr.owner.inputs), test_expr))
    assert tt.add == res[0].reify()

    res = run(1, q, applyo(tt.add, q, test_expr))
    assert mt(tuple(test_expr.owner.inputs)) == res[0]

    x = var()
    res = run(0, x, eq_comm(mt.mul(a, b), mt.mul(b, x)))
    assert (mt(a), ) == res

    res = run(0, x, eq_comm(mt.add(a, b), mt.add(b, x)))
    assert (mt(a), ) == res

    (res, ) = run(0, x, eq_assoc(mt.add(a, b, c), mt.add(a, x)))
    assert res == mt(b + c)

    (res, ) = run(0, x, eq_assoc(mt.mul(a, b, c), mt.mul(a, x)))
    assert res == mt(b * c)
Пример #3
0
def test_sexp_unify_reify():
    """Make sure we can unify and reify etuples/S-exps."""
    # Unify `A . (x + y)`, for `x`, `y` logic variables
    A = tf.compat.v1.placeholder(tf.float64,
                                 name="A",
                                 shape=tf.TensorShape([None, None]))
    x = tf.compat.v1.placeholder(tf.float64,
                                 name="x",
                                 shape=tf.TensorShape([None, 1]))
    y = tf.compat.v1.placeholder(tf.float64,
                                 name="y",
                                 shape=tf.TensorShape([None, 1]))

    z = tf.matmul(A, tf.add(x, y))

    z_sexp = etuplize(z, shallow=False)

    # Let's just be sure that the original TF objects are preserved
    assert z_sexp[1].reify() == A
    assert z_sexp[2][1].reify() == x
    assert z_sexp[2][2].reify() == y

    A_lv, x_lv, y_lv = var(), var(), var()
    dis_pat = etuple(
        TFlowMetaOperator(mt.matmul.op_def, var()),
        A_lv,
        etuple(TFlowMetaOperator(mt.add.op_def, var()), x_lv, y_lv),
    )

    s = unify(dis_pat, z_sexp, {})

    assert s[A_lv] == mt(A)
    assert s[x_lv] == mt(x)
    assert s[y_lv] == mt(y)

    # Now, we construct a graph that reflects the distributive property and
    # reify with the substitutions from the un-distributed form
    out_pat = etuple(mt.add, etuple(mt.matmul, A_lv, x_lv),
                     etuple(mt.matmul, A_lv, y_lv))
    z_dist = reify(out_pat, s)

    # Evaluate the tuple-expression and get a meta object/graph
    z_dist_mt = z_dist.eval_obj

    # If all the logic variables were reified, we should be able to
    # further reify the meta graph and get a concrete TF graph
    z_dist_tf = z_dist_mt.reify()

    assert isinstance(z_dist_tf, tf.Tensor)

    # Check the first part of `A . x + A . y` (i.e. `A . x`)
    assert z_dist_tf.op.inputs[0].op.inputs[0] == A
    assert z_dist_tf.op.inputs[0].op.inputs[1] == x
    # Now, the second, `A . y`
    assert z_dist_tf.op.inputs[1].op.inputs[0] == A
    assert z_dist_tf.op.inputs[1].op.inputs[1] == y
Пример #4
0
 def distributes(in_lv, out_lv):
     return lall(
         # lhs == A * (x + b)
         eq(etuple(mt.dot, var("A"), etuple(mt.add, var("x"), var("b"))),
            etuplize(in_lv)),
         # rhs == A * x + A * b
         eq(
             etuple(mt.add, etuple(mt.dot, var("A"), var("x")),
                    etuple(mt.dot, var("A"), var("b"))),
             out_lv,
         ),
     )
Пример #5
0
 def distributes(in_lv, out_lv):
     A_lv, x_lv, y_lv, z_lv = vars(4)
     return lall(
         # lhs == A * (x + y + z)
         eq_assoccomm(
             etuple(_dot, A_lv,
                    etuple(at.add, x_lv, etuple(at.add, y_lv, z_lv))),
             in_lv,
         ),
         # This relation does nothing but provide us with a means of
         # generating associative-commutative matches in the `kanren`
         # output.
         eq((A_lv, x_lv, y_lv, z_lv), out_lv),
     )
Пример #6
0
def test_ConstrainedVar():
    cvar = ConstrainedVar(lambda x: isinstance(x, str))

    assert repr(cvar).startswith("ConstrainedVar(")
    assert repr(cvar).endswith(f", {cvar.token})")

    s = unify(cvar, 1)
    assert s is False

    s = unify(1, cvar)
    assert s is False

    s = unify(cvar, "hi")
    assert s[cvar] == "hi"

    s = unify("hi", cvar)
    assert s[cvar] == "hi"

    x_lv = var()
    s = unify(cvar, x_lv)
    assert s == {cvar: x_lv}

    s = unify(cvar, x_lv, {x_lv: "hi"})
    assert s[cvar] == "hi"

    s = unify(x_lv, cvar, {x_lv: "hi"})
    assert s[cvar] == "hi"

    s_orig = {cvar: "hi", x_lv: "hi"}
    s = unify(x_lv, cvar, s_orig)
    assert s == s_orig

    s_orig = {cvar: "hi", x_lv: "bye"}
    s = unify(x_lv, cvar, s_orig)
    assert s is False

    x_at = at.vector("x")
    y_at = at.vector("y")
    op1_np = CustomOpNoProps(1)
    r_at = etuple(op1_np, x_at, y_at)

    def constraint(x):
        return isinstance(x, tuple)

    a_lv = ConstrainedVar(constraint)
    res = reify(etuple(op1_np, a_lv), {a_lv: r_at})

    assert res[1] == r_at
Пример #7
0
def cdr_Variable(x):
    if x.owner:
        x_e = etuple(_car(x), *x.owner.inputs, evaled_obj=x)
    else:
        raise ConsError("Not a cons pair.")

    return x_e[1:]
Пример #8
0
def test_etuples():
    x_at = at.vector("x")
    y_at = at.vector("y")

    z_at = etuple(x_at, y_at)

    res = apply(at.add, z_at)

    assert res.owner.op == at.add
    assert res.owner.inputs == [x_at, y_at]

    w_at = etuple(at.add, x_at, y_at)

    res = w_at.evaled_obj
    assert res.owner.op == at.add
    assert res.owner.inputs == [x_at, y_at]

    # This `Op` doesn't expand into an `etuple` (i.e. it's "atomic")
    op1_np = CustomOpNoProps(1)

    res = apply(op1_np, z_at)
    assert res.owner.op == op1_np

    q_at = op1_np(x_at, y_at)
    res = etuplize(q_at)
    assert res[0] == op1_np

    with pytest.raises(TypeError):
        etuplize(op1_np)

    class MyMultiOutOp(Op):
        def make_node(self, *inputs):
            outputs = [MyType()(), MyType()()]
            return Apply(self, list(inputs), outputs)

        def perform(self, node, inputs, outputs):
            outputs[0] = np.array(inputs[0])
            outputs[1] = np.array(inputs[0])

    x_at = at.vector("x")
    op1_np = MyMultiOutOp()
    res = apply(op1_np, etuple(x_at))
    assert len(res) == 2
    assert res[0].owner.op == op1_np
    assert res[1].owner.op == op1_np
Пример #9
0
def cdr_Op(x):
    if not hasattr(x, "__props__"):
        raise ConsError("Not a cons pair.")

    x_e = etuple(
        _car(x),
        *[getattr(x, p) for p in getattr(x, "__props__", ())],
        evaled_obj=x,
    )
    return x_e[1:]
Пример #10
0
def test_kanren_basic():
    A_at = at.matrix("A")
    x_at = at.vector("x")

    y_at = at.dot(A_at, x_at)

    q = var()
    res = list(run(None, q, eq(y_at, etuple(_dot, q, x_at))))

    assert res == [A_at]
Пример #11
0
def cdr_MetaVariable(x):
    """Return the arguments/tail/CDR of a variable object.

    See `cdr_MetaSymbol`
    """
    try:
        x_e = etuple(_car(x), *x.base_arguments, eval_obj=x)
    except NotImplementedError:
        raise ConsError("Not a cons pair.")

    return x_e[1:]
Пример #12
0
def test_convert_strs_to_vars():

    res = convert_strs_to_vars("a")
    assert isinstance(res, Var)
    assert res.token == "a"

    x_at = at.vector()
    y_at = at.vector()
    res = convert_strs_to_vars((("a", x_at), y_at))
    assert res == etuple(etuple(var("a"), x_at), y_at)

    def constraint(x):
        return isinstance(x, str)

    res = convert_strs_to_vars((({
        "pattern": "a",
        "constraint": constraint
    }, x_at), y_at))
    assert res == etuple(etuple(ConstrainedVar(constraint, "a"), x_at), y_at)

    # Make sure constrained logic variables are the same across distinct uses
    # of their string names
    res = convert_strs_to_vars(({
        "pattern": "a",
        "constraint": constraint
    }, "a"))
    assert res[0] is res[1]

    var_map = {"a": var("a")}
    res = convert_strs_to_vars(("a", ), var_map=var_map)
    assert res[0] is var_map["a"]

    # Make sure numbers and NumPy arrays are converted
    val = np.r_[1, 2]
    res = convert_strs_to_vars((val, ))
    assert isinstance(res[0], Constant)
    assert np.array_equal(res[0].data, val)
Пример #13
0
def normal_normal_regression(Y, X, beta, Y_args_tail=None, beta_args=None):
    """Create a goal for a normal-normal regression of the form `Y ~ N(X * beta, sd**2)`."""
    Y_args_tail = Y_args_tail or var()
    beta_args = beta_args or var()

    Y_args, Y_mean_lv = var(), var()

    res = lall(
        # `Y` is a `NormalRV`
        applyo(mt.NormalRV, Y_args, Y),
        # `beta` is also a `NormalRV`
        applyo(mt.NormalRV, beta_args, beta),
        # Obtain its mean parameter and remaining args
        applyo(Y_mean_lv, Y_args_tail, Y_args),
        # Relate it to a dot product of `X` and `beta`
        applyo(mt.dot, etuple(X, beta), Y_mean_lv),
    )

    return res
Пример #14
0
def test_unify_Type():
    t1 = TensorType(np.float64, (True, False))
    t2 = TensorType(np.float64, (True, False))

    # `Type`, `Type`
    s = unify(t1, t2)
    assert s == {}

    # `Type`, `ExpressionTuple`
    s = unify(t1, etuple(TensorType, "float64", (1, None)))
    assert s == {}

    from aesara.scalar.basic import Scalar

    st1 = Scalar(np.float64)
    st2 = Scalar(np.float64)

    s = unify(st1, st2)
    assert s == {}
Пример #15
0
    def _convert(y):
        if isinstance(y, str):
            v = var_map.get(y, var(y))
            var_map[y] = v
            return v
        elif isinstance(y, dict):
            pattern = y["pattern"]
            if not isinstance(pattern, str):
                raise TypeError(
                    "Constraints can only be assigned to logic variables (i.e. strings)"
                )
            constraint = y["constraint"]
            v = var_map.get(pattern, ConstrainedVar(constraint, pattern))
            var_map[pattern] = v
            return v
        elif isinstance(y, tuple):
            return etuple(*tuple(_convert(e) for e in y))
        elif isinstance(y, (Number, np.ndarray)):
            from aesara.tensor import as_tensor_variable

            return as_tensor_variable(y)
        return y
Пример #16
0
def test_etuple_term():
    """Test `etuplize` and `etuple` interaction with `term`."""
    # Take apart an already constructed/evaluated meta
    # object.
    e2 = mt.add(mt.vector(), mt.vector())

    e2_et = etuplize(e2)

    assert isinstance(e2_et, ExpressionTuple)

    # e2_et_expect = etuple(
    #     mt.add,
    #     etuple(mt.TensorVariable,
    #            etuple(mt.TensorType,
    #                   'float64', (False,), None),
    #            None, None, None),
    #     etuple(mt.TensorVariable,
    #            etuple(mt.TensorType,
    #                   'float64', (False,), None),
    #            None, None, None),
    # )
    e2_et_expect = etuple(mt.add, e2.base_arguments[0], e2.base_arguments[1])
    assert e2_et == e2_et_expect
    assert e2_et.eval_obj is e2

    # Make sure expression expansion works from Theano objects, too.
    # First, do it manually.
    tt_expr = tt.vector() + tt.vector()

    mt_expr = mt(tt_expr)
    assert mt_expr.obj is tt_expr
    assert mt_expr.reify() is tt_expr
    e3 = etuplize(mt_expr)
    assert e3 == e2_et
    assert e3.eval_obj is mt_expr
    assert e3.eval_obj.reify() is tt_expr

    # Now, through `etuplize`
    e2_et_2 = etuplize(tt_expr)
    assert e2_et_2 == e3 == e2_et
    assert isinstance(e2_et_2, ExpressionTuple)
    assert e2_et_2.eval_obj == tt_expr

    test_expr = mt(tt.vector("z") * 7)
    assert rator(test_expr) == mt.mul
    assert rands(test_expr)[0] == mt(tt.vector("z"))

    dim_shuffle_op = rator(rands(test_expr)[1])

    assert isinstance(dim_shuffle_op, mt.DimShuffle)
    assert rands(rands(test_expr)[1]) == etuple(mt(7))

    with pytest.raises(ConsError):
        rator(dim_shuffle_op)
    # assert rator(dim_shuffle_op) == mt.DimShuffle
    # assert rands(dim_shuffle_op) == etuple((), ("x",), True)

    const_tensor = rands(rands(test_expr)[1])[0]
    with pytest.raises(ConsError):
        rator(const_tensor)
    with pytest.raises(ConsError):
        rands(const_tensor)

    et_expr = etuplize(test_expr)
    exp_res = etuple(mt.mul, mt(tt.vector("z")),
                     etuple(mt.DimShuffle((), ("x", ), True), mt(7))
                     # etuple(etuple(mt.DimShuffle, (), ("x",), True), mt(7))
                     )

    assert et_expr == exp_res
    assert exp_res.eval_obj == test_expr
Пример #17
0
def cdr_Type(x):
    x_e = etuple(_car(x),
                 *[getattr(x, p) for p in getattr(x, "__props__", ())],
                 evaled_obj=x)
    return x_e[1:]
Пример #18
0
def test_unify_Variable():
    x_at = at.vector("x")
    y_at = at.vector("y")

    z_at = x_at + y_at

    # `Variable`, `Variable`
    s = unify(z_at, z_at)
    assert s == {}

    # These `Variable`s have no owners
    v1 = MyType()()
    v2 = MyType()()

    assert v1 != v2

    s = unify(v1, v2)
    assert s is False

    op_lv = var()
    z_pat_et = etuple(op_lv, x_at, y_at)

    # `Variable`, `ExpressionTuple`
    s = unify(z_at, z_pat_et, {})

    assert op_lv in s
    assert s[op_lv] == z_at.owner.op

    res = reify(z_pat_et, s)

    assert isinstance(res, ExpressionTuple)
    assert equal_computations([res.evaled_obj], [z_at])

    z_et = etuple(at.add, x_at, y_at)

    # `ExpressionTuple`, `ExpressionTuple`
    s = unify(z_et, z_pat_et, {})

    assert op_lv in s
    assert s[op_lv] == z_et[0]

    res = reify(z_pat_et, s)

    assert isinstance(res, ExpressionTuple)
    assert equal_computations([res.evaled_obj], [z_et.evaled_obj])

    # `ExpressionTuple`, `Variable`
    s = unify(z_et, x_at, {})
    assert s is False

    # This `Op` doesn't expand into an `ExpressionTuple`
    op1_np = CustomOpNoProps(1)

    q_at = op1_np(x_at, y_at)

    a_lv = var()
    b_lv = var()
    # `Variable`, `ExpressionTuple`
    s = unify(q_at, etuple(op1_np, a_lv, b_lv))

    assert s[a_lv] == x_at
    assert s[b_lv] == y_at
Пример #19
0
def scale_loc_transform(in_expr, out_expr):
    """Create relations for lifting and sinking scale and location parameters of distributions.

    I.e. f(a + b*x) -> a + b * f(x)

    For example, `in_expr`: f(a + b*x) == `out_expr`: a + b * f(x).

    TODO: Match larger distribution families and perform transforms from there.

    XXX: PyMC3 rescaling issue (?) doesn't allow us to take the more general
    approach, which involves separate scale and location rewrites.

    """
    # Scale and location transform expression "pattern" for a Normal term.
    normal_mt = mt.NormalRV(var(), var(), size=var(), rng=var(), name=var())
    n_name_lv = normal_mt.name
    n_mean_lv, n_sd_lv, n_size_lv, n_rng_lv = normal_mt.owner.inputs
    offset_name_mt = var()
    rct_norm_offset_mt = etuple(
        mt.add,
        n_mean_lv,
        etuple(
            mt.mul,
            n_sd_lv,
            mt.NormalRV(0.0,
                        1.0,
                        size=n_size_lv,
                        rng=n_rng_lv,
                        name=offset_name_mt),
        ),
    )

    # Scale and location transform expression "pattern" for a Cauchy term.
    cauchy_mt = mt.CauchyRV(var(), var(), size=var(), rng=var(), name=var())
    c_name_lv = cauchy_mt.name
    c_mean_lv, c_beta_lv, c_size_lv, c_rng_lv = cauchy_mt.owner.inputs
    rct_cauchy_offset_mt = etuple(
        mt.add,
        c_mean_lv,
        etuple(
            mt.mul,
            c_beta_lv,
            mt.CauchyRV(0.0,
                        1.0,
                        size=c_size_lv,
                        rng=c_rng_lv,
                        name=offset_name_mt),
        ),
    )

    # TODO:
    # uniform_mt = mt.UniformRV(var(), var(), size=var(), rng=var(), name=var())
    # u_name_lv = uniform_mt.name
    # u_a_lv, u_b_lv, u_size_lv, u_rng_lv = uniform_mt.owner.inputs
    # rct_uniform_scale_mt = etuple(
    #     mt.mul,
    #     u_b_lv,
    #     mt.UniformRV(0.0, 1.0, size=u_size_lv, rng=u_rng_lv, name=offset_name_mt),
    # )
    # rct_uniform_loc_mt = etuple(mt.add, u_c_lv,
    #                             mt.UniformRV(u_a_lv, u_b_lv,
    #                                          size=u_size_lv,
    #                                          rng=u_rng_lv,
    #                                          name=offset_name_mt))

    rels = conde(
        [
            eq(in_expr, normal_mt),
            constant_neq(n_sd_lv, 1),
            constant_neq(n_mean_lv, 0),
            eq(out_expr, rct_norm_offset_mt),
            concat(n_name_lv, "_offset", offset_name_mt),
        ],
        [
            eq(in_expr, cauchy_mt),
            constant_neq(c_beta_lv, 1),
            # TODO: Add a positivity constraint for the scale.
            constant_neq(c_mean_lv, 0),
            eq(out_expr, rct_cauchy_offset_mt),
            concat(c_name_lv, "_offset", offset_name_mt),
        ],
        # TODO:
        # [eq(in_expr, uniform_mt),
        #  lall(
        #    constant_eq(u_a_lv, 0),
        #    eq(out_expr, rct_uniform_scale_mt),
        #    concat(u_name_lv, "_scale", offset_name_mt),
        #  )],
    )

    return rels
Пример #20
0
def test_normal_normal_regression():
    tt.config.compute_test_value = "ignore"
    theano.config.cxx = ""
    np.random.seed(9283)

    N = 10
    M = 3
    a_tt = tt.vector("a")
    R_tt = tt.vector("R")
    X_tt = tt.matrix("X")
    V_tt = tt.vector("V")

    a_tt.tag.test_value = np.random.normal(size=M)
    R_tt.tag.test_value = np.abs(np.random.normal(size=M))
    X = np.random.normal(10, 1, size=N)
    X = np.c_[np.ones(10), X, X * X]
    X_tt.tag.test_value = X
    V_tt.tag.test_value = np.ones(N)

    beta_rv = NormalRV(a_tt, R_tt, name="\\beta")

    E_y_rv = X_tt.dot(beta_rv)
    E_y_rv.name = "E_y"
    Y_rv = NormalRV(E_y_rv, V_tt, name="Y")

    y_tt = tt.as_tensor_variable(Y_rv.tag.test_value)
    y_tt.name = "y"
    y_obs_rv = observed(y_tt, Y_rv)
    y_obs_rv.name = "y_obs"

    #
    # Use the relation with identify/match `Y`, `X` and `beta`.
    #
    y_args_tail_lv, b_args_tail_lv = var(), var()
    beta_lv = var()

    y_args_lv, y_lv, Y_lv, X_lv = var(), var(), var(), var()
    (res, ) = run(
        1,
        (beta_lv, y_args_tail_lv, b_args_tail_lv),
        applyo(mt.observed, y_args_lv, y_obs_rv),
        eq(y_args_lv, (y_lv, Y_lv)),
        normal_normal_regression(Y_lv, X_lv, beta_lv, y_args_tail_lv,
                                 b_args_tail_lv),
    )

    # TODO FIXME: This would work if non-op parameters (e.g. names) were covered by
    # `operator`/`car`.  See `TheanoMetaOperator`.
    assert res[0].eval_obj.obj == beta_rv
    assert res[0] == etuplize(beta_rv)
    assert res[1] == etuplize(Y_rv)[2:]
    assert res[2] == etuplize(beta_rv)[1:]

    #
    # Use the relation with to produce `Y` from given `X` and `beta`.
    #
    X_new_mt = mt(tt.eye(N, M))
    beta_new_mt = mt(NormalRV(0, 1, size=M))
    Y_args_cdr_mt = etuplize(Y_rv)[2:]
    Y_lv = var()
    (res, ) = run(
        1, Y_lv,
        normal_normal_regression(Y_lv, X_new_mt, beta_new_mt, Y_args_cdr_mt))
    Y_out_mt = res.eval_obj

    Y_new_mt = etuple(mt.NormalRV, mt.dot(X_new_mt,
                                          beta_new_mt)) + Y_args_cdr_mt
    Y_new_mt = Y_new_mt.eval_obj

    assert Y_out_mt == Y_new_mt
Пример #21
0
def normal_qr_transform(in_expr, out_expr):
    """Produce a relation for normal-normal regression and its QR-reduced form.

    TODO XXX: This isn't entirely correct (e.g. it needs to also
    transform the variance terms), but it demonstrates all the requisite
    functionality for this kind of model reformulation.

    """
    y_lv, Y_lv, X_lv, beta_lv = var(), var(), var(), var()
    Y_args_lv, beta_args_lv = var(), var()
    QR_lv, Q_lv, R_lv = var(), var(), var()
    beta_til_lv, beta_new_lv = var(), var()
    beta_mean_lv, beta_sd_lv = var(), var()
    beta_size_lv, beta_rng_lv = var(), var()
    Y_new_lv = var()
    X_op_lv = var()

    in_expr = etuplize(in_expr)

    res = lall(
        # Only applies to regression models on observed RVs
        eq(in_expr, etuple(mt.observed, y_lv, Y_lv)),
        # Relate the model components
        normal_normal_regression(Y_lv, X_lv, beta_lv, Y_args_lv, beta_args_lv),
        # Let's not do all this to an already QR-reduce graph;
        # otherwise, we'll loop forever!
        applyo(X_op_lv, var(), X_lv),
        # XXX: This type of dis-equality goal isn't the best,
        # but it will definitely work for now.
        neq(mt.nlinalg.qr_full, X_op_lv),
        # Relate terms for the QR decomposition
        eq(QR_lv, etuple(mt.nlinalg.qr_full, X_lv)),
        eq(Q_lv, etuple(itemgetter(0), QR_lv)),
        eq(R_lv, etuple(itemgetter(1), QR_lv)),
        # The new `beta_tilde`
        eq(beta_args_lv, (beta_mean_lv, beta_sd_lv, beta_size_lv, beta_rng_lv)),
        eq(
            beta_til_lv,
            etuple(
                mt.NormalRV,
                # Use these `tt.[ones|zeros]_like` functions to preserve the
                # correct shape (and a valid `tt.dot`).
                etuple(mt.zeros_like, beta_mean_lv),
                etuple(mt.ones_like, beta_sd_lv),
                beta_size_lv,
                beta_rng_lv,
            ),
        ),
        # Relate the new and old coeffs
        eq(beta_new_lv, etuple(mt.dot, etuple(mt.nlinalg.matrix_inverse, R_lv), beta_til_lv)),
        # Use the relation the other way to produce the new/transformed
        # observation distribution
        normal_normal_regression(Y_new_lv, Q_lv, beta_til_lv, Y_args_lv),
        eq(
            out_expr,
            [
                (
                    in_expr,
                    etuple(mt.observed, y_lv, etuple(update_name_suffix, Y_new_lv, Y_lv, "")),
                ),
                (beta_lv, beta_new_lv),
            ],
        ),
    )
    return res