Opened 5 years ago

Missing documentation of derivative operator/notation

Reported by: Owned by: schymans major sage-6.5 symbolics kcrisman, eviatarbach schymans N/A

Taking the derivative of a symbolic function returns the D-notation: sage: var('x y z') sage: f(x) = function('f',x,y,z); sage: f(x).diff(x,y) D[0, 1](f)(x, y, z)

Unfortunately, the meaning of this notation is not documented anywhere, neither in diff(), nor in derivative() nor in function(). There is a ton of tickets about improving ambiguities and malfunctions related to this notation, but it would be very helpful to at least document how it is supposed to work and what it means if a user sees output as above.

See here for related tickets:

• #6344 - allow typesetting in "diff" format (possibly only as non-default option)
• #6756 - add input to Sage in "diff" format for derivatives (the most controversial)
• #6480 - clarify or fix substituting functions inside of symbolic derivatives
• #7401 - bug in our interaction with Maxima with evaluating derivative at a point (needs work due to multivariate derivatives not being there)
• #12796 - allow evaluation at points

comment:1 Changed 5 years ago by nbruin

OK, I do not know where it should go in the documentation for best visibility (I'd think in "diff" somewhere), but the explanation should be along the lines of:

Partial derivatives are represented in sage using differential operators, referencing the position of the variable with respect to which the partial derivative is taken. This means that for a function f in $r+1$ variables we have

$D[i_1,\ldots,i_n](f)(x_0,\ldots,x_r) = \left.\frac{\partial f(t_0,\ldots,t_r)}{\partial t_{i_1}\cdots \partial t_{i_n}} \right|_{t_0=x_0,\ldots,t_r=x_r}$

An advantage of this notation is that it is clear which derivative is taken, regardless of the names of the variables. For instance, if we have

sage: var("x,y,t")
sage: f(x,y)=function('f',x,y)
sage: g=f(x,y).diff(x,y); g
D[0, 1](f)(x, y)
sage: g.subs(x=1,y=1)
D[0, 1](f)(1, 1)
sage: g.subs(x=t,y=t+1)
D[0, 1](f)(t, t+1)


Note that in the last two lines are completely unambiguous using operator notation, whereas Leibnitz notation would require the use of some arbitrary explicit choice of auxiliary variable names.

comment:2 follow-up: ↓ 3 Changed 5 years ago by kcrisman

• Description modified (diff)

Are there times where this notation is ambiguous, though? I seem to recall that being the case.

Also, I think something about this should show up at the top of the "sage/calculus/calculus.py" file as well. It's a constant source of questions I don't really know the answer to.

comment:3 in reply to: ↑ 2 Changed 5 years ago by nbruin

Are there times where this notation is ambiguous, though? I seem to recall that being the case.

I am not aware of any such cases. It's reflecting all information that sage has stored on the object, so an ambiguity would imply that sage is working with an ill-defined object.

If you rewrite it in Leibnitz notation using a set of auxiliary variables, you see that any expression has a clear interpretation.

Go ahead and put it in the docs!

comment:4 follow-up: ↓ 6 Changed 5 years ago by kcrisman

• Description modified (diff)

Can you comment on some of the chain-rule type issues in #6480, then? I have to say that in particular the stuff at http://ask.sagemath.org/question/9932/how-to-substitute-a-function-within-derivatives/ and #6480 is massively confusing. Heck, let's add #7401 while we're at it.

I don't even know whether any of those things are "really" right or wrong at this point. I suppose you shouldn't be allowed to substitute in a function that "isn't there" in 6 and 7, but then why does 8 "work"? In any case, shouldn't there be an error raised if one attempts something like this when it's not "legitimate"?

# 6. Fails.
x = var('x')
f = function('f', x)
g = function('g', x)
p = f.diff()
print p.substitute_function(f, g) # Outputs "D[0](f)(x)"

# 7. Fails.
x = var('x')
f = function('f', x)
g = function('g', x)
p = f.diff()
print p.substitute_function(f(x), g(x)) # Outputs "D[0](f)(x)"

# 8. Works.
x = var('x')
f = function('f')
g = function('g')
p = f(x).diff()
print p.substitute_function(f, g) # Outputs "D[0](g)(x)"


These are very subtle differences to anyone who is not in symbolic algebra/expressions, and part of the issue is the difference between expressions and functions, no doubt. So comment:1 is a good start, but definitely only a start.

comment:5 Changed 5 years ago by kcrisman

• Description modified (diff)

Okay, at least now I actually understand what the different tickets are about. Phew.

comment:6 in reply to: ↑ 4 ; follow-ups: ↓ 8 ↓ 11 Changed 5 years ago by nbruin

I don't even know whether any of those things are "really" right or wrong at this point.

There is an internal logic that explains the behaviour. I don't think this stuff can ever be made "intuitive" to the average calculus afficionado because the tradition in analysis notation is just to irredeemably confuse "function" and "function evaluated at...". Humans can handle this confusion to some extent, but I think it's just too incompatible with how computers represent math.

# 6. Fails.
x = var('x')
f = function('f', x)
g = function('g', x)
p = f.diff()
print p.substitute_function(f, g) # Outputs "D[0](f)(x)"


the problem: f is not a function, but a function evaluated at x, whatever x is. I'm not going to defend that function('f', x) doesn't return a function. THAT is the real source why this example seems confusing.

sage: f
f(x)


The fact that f.diff() doesn't fail is apparently a heuristic, because there's only one variable discernible in the expression. But then you see:

sage: p
D[0](f)(x)


as you can see, there's no f(x) appearing in that expression, so of course substituting f(x) for something else has no effect. Really,

p.substitute_function(f, g)


should give a TypeError, because the types of the arguments don't match. Indeed:

sage: p.substitute_function(f.operator(), g)
DeprecationWarning: Substitution using function-call syntax ...


Apparently sage does try to convert the argument g(x) (that is bound to g) to a function.

# 7. Fails.
x = var('x')
f = function('f', x)
g = function('g', x)
p = f.diff()
print p.substitute_function(f(x), g(x)) # Outputs "D[0](f)(x)"


same problem, but even worse: f(x)(x) should have been deprecated already.

# 8. Works.
x = var('x')
f = function('f')
g = function('g')
p = f(x).diff()
print p.substitute_function(f, g) # Outputs "D[0](g)(x)"


The fact that function('f') and function('f',x) return different types of arguments is really bad. It prevents any convincing explanation of the distinction that is essential here, because the interface itself confuses the two different issues. On top of that, we have a lot of documentation that was written by people who were equally confused, so the calculus doc makes a good effort to confuse any new user too.

These are very subtle differences to anyone who is not in symbolic algebra/expressions, and part of the issue is the difference between expressions and functions, no doubt. So comment:1 is a good start, but definitely only a start.

It's an entirely different issue, though. Apparently the documentation never explains what D[0](f)(x) means. We can do that independent of whether we go out of our way to confuse users about the distinction between functions and functions evaluated at ...

comment:7 follow-up: ↓ 10 Changed 5 years ago by eviatarbach

It would probably be good to add a note that D[0, 1](f)(x, y) does not in general equal D[1, 0](f)(x, y) (https://en.wikipedia.org/wiki/Symmetry_of_second_derivatives).

comment:8 in reply to: ↑ 6 Changed 5 years ago by schymans

The fact that function('f') and function('f',x) return different types of arguments is really bad. It prevents any convincing explanation of the distinction that is essential here, because the interface itself confuses the two different issues. On top of that, we have a lot of documentation that was written by people who were equally confused, so the calculus doc makes a good effort to confuse any new user too.

Wow, the reason while I hesitated about adding documentation of the D-notation was that I thought it should go into the documentation of function() and then I found out that the documentation of function() itself is already incomplete and confusing. Should we open a ticket for that, too? For example, none of the methods described in http://www.sagemath.org/doc/reference/calculus/sage/symbolic/function_factory.html show up when I type:

function?


The distinction between

f = function('f')


and

f = function('f', x)


is also not documented. Only now I realised that function('f') returns a function, while function('f',x) returns an expression. Furthermore, differentiation of a function is not supported, it needs to be converted to an expression first:

f = function('f')
fx = function('f', x)
print type(f)
print type(fx)
print type(f(x))
print fx.diff()
print f(x).diff()
print f.diff()
///
<class 'sage.symbolic.function_factory.NewSymbolicFunction'>
<type 'sage.symbolic.expression.Expression'>
<type 'sage.symbolic.expression.Expression'>
D[0](f)(x)
D[0](f)(x)
Traceback (most recent call last):    print f(x).diff()
File "", line 1, in <module>

File "/tmp/tmpMe44Jp/___code___.py", line 9, in <module>
exec compile(u'print f.diff()' + '\n', '', 'single')
File "", line 1, in <module>

AttributeError: 'NewSymbolicFunction' object has no attribute 'diff'


It's an entirely different issue, though. Apparently the documentation never explains what D[0](f)(x) means. We can do that independent of whether we go out of our way to confuse users about the distinction between functions and functions evaluated at ...

If we modify the documentation of diff(), though, we should explain why f(x).diff() works but f.diff() does not.

comment:9 follow-up: ↓ 14 Changed 5 years ago by schymans

Following on from the logic about function('f', x) being an expression, not a function, why does this fail, then?

# If all of f, g and p are expressions, why does this fail?
x = var('x')
f = function('f', x)
print type(f)
g = function('g', x)
print type(g)
p = f.diff()
print type(p)
print p.subs_expr(f==g) # Outputs "D[0](f)(x)"


comment:10 in reply to: ↑ 7 ; follow-ups: ↓ 12 ↓ 13 ↓ 15 Changed 5 years ago by schymans

It would probably be good to add a note that D[0, 1](f)(x, y) does not in general equal D[1, 0](f)(x, y) (https://en.wikipedia.org/wiki/Symmetry_of_second_derivatives).

I just wanted to propose the following simple documentation:

Partial derivatives are represented in sage using differential operators, referencing the positions of the variables with respect to which consecutive partial derivatives are taken.

An advantage of this notation is that it is clear in which order derivatives are taken and on which variables they are performed, regardless of the names of the variables. For instance, if we have

sage: var("x,y,t")
(x, y, t)
sage: f=function('f',x,y)
sage: g=f.diff(x,y); g
D[0, 1](f)(x, y)
sage: h=f.diff(y,x); g
D[0, 1](f)(x, y)


I was expecting the second to give either D[0, 1](f)(y, x) or D[1, 0](f)(x, y). What is going on, is the order of differentiations not honoured in the notation?

Interestingly, the following does not return True but two visually indistinguishable expressions. To me, this looks like a bug.

sage: f.diff(x,y) == f.diff(y,x)
D[0, 1](f)(x, y) == D[0, 1](f)(x, y)


I would much prefer this behaviour:

sage: f.diff(x,y) == f.diff(y,x)
f.diff(x,y) == f.diff(y,x)


It would be unambiguous and shorter. What is the advantage of the D-notation again?

Last edited 5 years ago by schymans (previous) (diff)

comment:11 in reply to: ↑ 6 Changed 5 years ago by schymans

the problem: f is not a function, but a function evaluated at x, whatever x is. I'm not going to defend that function('f', x) doesn't return a function. THAT is the real source why this example seems confusing.

I created a ticket to improve the documentation of function(): http://trac.sagemath.org/ticket/17447 I put you and kcrisman cc on that ticket, I hope you don't mind.

comment:12 in reply to: ↑ 10 ; follow-up: ↓ 16 Changed 5 years ago by kcrisman

I was expecting the second to give either D[0, 1](f)(y, x) or D[1, 0](f)(x, y). What is going on, is the order of differentiations not honoured in the notation?

In this case, it is not an example of Eviatar's (good) point. f always has the variables in the same order, so your first option is not possible. The second option would be legitimate but I guess Sage just assumes the Clairaut/Schwarz Theorem always holds for 'symbolic' functions.

Interestingly, the following does not return True but two visually indistinguishable expressions. To me, this looks like a bug.

sage: f.diff(x,y) == f.diff(y,x)
D[0, 1](f)(x, y) == D[0, 1](f)(x, y)


See above.

It would be unambiguous and shorter. What is the advantage of the D-notation again?

I'm still not 100% sold on it, especially since it doesn't LaTeX with subscripts, but this would be a second issue.

Questions:

• Is it worth trying to distinguish D[0,1] and D[1,0]?
• Would it be very hard to do so? (I have not looked at this code in a long time.)
• Is it easy to have the LaTeX be subscripts?
• Alternately (or with that), would it be possible to just "read off" the actual variable names and put those in, ala D[x,y] and D[y,x]? In principle it should be, since all such functions now have ordered variable names. I don't know how that would combine with the whole D[0,1](f)(x,x+1) thing, so maybe it's a bad idea.

comment:13 in reply to: ↑ 10 Changed 5 years ago by nbruin

I would much prefer this behaviour:

sage: f.diff(x,y) == f.diff(y,x)
f.diff(x,y) == f.diff(y,x)


It would be unambiguous and shorter. What is the advantage of the D-notation again?

Please write f(x,y) there so that it's clear you're differentiating an expression, not a function:

sage: var('x,y')
(x, y)
sage: f=function('f')
sage: f.diff(x,y)
AttributeError: 'NewSymbolicFunction' object has no attribute 'diff'


The advantage is that you can actually represent evaluations of the derivative:

sage: f(x,y).diff(x,y).subs(x=2,y=3)
D[0, 1](f)(2, 3)


Would you propose to print that as the (admittedly shorter) f.diff(2,3)? It's absolutely possible to print f(x,y).diff(x,y) for D[0,1](f)(x,y) because at some point we can see we have an expression with an operator that is an FDerivativeOperator, and where the arguments form a list of distinct symbolic variables. But we have to print D[0,1](f)(t,t^2). Printing f.diff(t,t^2) is just something else entirely.

comment:14 in reply to: ↑ 9 Changed 5 years ago by nbruin

print p.subs_expr(f==g) # Outputs "D[0](f)(x)"


Because you need subs_function there. These are two different routines that take different types of arguments and do different things with them. Objects of type SymbolicFunction cannot be used interchangeably with SymbolicExpression.

We could in principle extend subs to differentiate on type of passed argument and dispatch accordingly to subs_function or subs_expression (and raise an error if some impossible combination is tried).

comment:15 in reply to: ↑ 10 Changed 5 years ago by nbruin

I was expecting the second to give either D[0, 1](f)(y, x) or D[1, 0](f)(x, y). What is going on, is the order of differentiations not honoured in the notation?

Correct. Rewriting of differentials apparently assumes symmetry. That's not such a big issue, since in any reasonable application environment it holds anyway (if your functions aren't continuously differentiable you tend to have to use other things, such as distributions, anyway.

Illustration:

sage: D=sage.symbolic.operators.FDerivativeOperator
sage: D(f,[1,0])
D[1, 0](f)
sage: D(f,[0,1])
D[0, 1](f)
sage: D(f,[0,1])(x,y)
D[0, 1](f)(x, y)
sage: D(f,[1,0])(x,y)
D[0, 1](f)(x, y)


As you can see, the reordering happens on evaluation. It's not an ambiguity in notation, it's an assumption that's been programmed into sage. Perhaps it's already in Pynac?

Last edited 5 years ago by nbruin (previous) (diff)

comment:16 in reply to: ↑ 12 Changed 5 years ago by nbruin

Questions:

• Is it worth trying to distinguish D[0,1] and D[1,0]?

I think not, but it's not my field. When do you really work with non-continuously differentiable functions? Don't you use distributions then anyway? I think someone should point out a meaningful calculation where symmetry doesn't hold.

• Would it be very hard to do so? (I have not looked at this code in a long time.)

No. It's in the diff code somewhere. It's probably an explicit "sort" command you can just take out.

• Alternately (or with that), would it be possible to just "read off" the actual variable names and put those in, ala D[x,y] and D[y,x]? In principle it should be, since all such functions now have ordered variable names.

Where would you get the names from in the following example?

sage: D=sage.symbolic.operators.FDerivativeOperator
sage: D(f,[0,1,0])
D[0, 1, 0](f)
sage: D(f,[0,1,0])(x,y)
D[0, 0, 1](f)(x, y)


If you're going to bother matching indices and variable names, you'd better go the whole way and recognize that in the last example the arguments are distinct symbolic variables that match up nicely with the differentiation indices, so we can print

diff(f(x,y),x,x,y)


I don't know how that would combine with the whole D[0,1](f)(x,x+1) thing, so maybe it's a bad idea.

comment:17 Changed 5 years ago by eviatarbach

I think it is worth distinguishing. The standard counterexample to equality of mixed partials is f(x, y) = x*y*(x^2 - y^2)/(x^2 + y^2), with f(0, 0) = 0, which is a continuous function but the second derivatives at (0, 0) are unequal. This may not come about often in practice (it doesn't seem like we can run this example without having multivariable piecewise functions), but especially if people are using the formal functions it should have correct mathematical properties.

comment:18 follow-up: ↓ 19 Changed 5 years ago by schymans

Wow, I think the description of this ticket should be changed. It is not any more about the documentation of the D[] notation, but about a meaningful way of using and displaying symbolic differentials.

One thing I learned from this post: http://trac.sagemath.org/ticket/17447#comment:3 is: The example I used in the description of the ticket, and most of the examples following, should not be used! If we avoid this, then we are stuck with the problem that the diff() method is not defined for symbolic functions:

sage: var('x')
sage: function('f', x)
sage: print type(g)
sage: p = f.diff()
<class 'sage.symbolic.function_factory.NewSymbolicFunction'>
Traceback (click to the left of this block for traceback)
...
AttributeError: 'NewSymbolicFunction' object has no attribute 'diff'


What is the point of having a notation for differentials of symbolic functions, then? Should the first step not be to actually implement differentiation of symbolic functions and then explain the notation in the documentation of function?

comment:19 in reply to: ↑ 18 Changed 5 years ago by nbruin

What is the point of having a notation for differentials of symbolic functions, then? Should the first step not be to actually implement differentiation of symbolic functions and then explain the notation in the documentation of function?

It's already implemented, see 16. It's called FDerivativeOperator. It may be worthwhile having a nicer interface. Being able to write D[0,1](f) might be nice. See sage-devel:"D notation input for ODEs", which has a short and (I think) fully functional code snippet that implements it.

If you prefer inputting your derivatives with Leibnitz notation, you're going to need temporary variables, and in that case it's already about a efficient as you can get:

sage: f=sage.symbolic.function_factory.function('f')
sage: df=diff(f(x),x).operator()
sage: df
D[0](f)


comment:20 follow-up: ↓ 21 Changed 5 years ago by schymans

Thanks, Nils, I didn't see that. However, I am still not able to achieve what I was hoping for. Let me give a pratical example. I define an expression for pressure following the ideal gas law:

sage: var('R t p n T V')
sage: eq_p = p == n*R*T/V


At some point, I would like to differentiate this equation for an open system, i.e. where p, n, T and V are functions of time. I re-defined the respective variables as functions of and time tried taking the derivative, but:

sage: function('p',t)
sage: function('n', t)
sage: function('T', t)
sage: function('V', t)
sage: diff(eq_p,t)
p == R*T*n/V
0 == 0


I also didn't find a way to get FderivativeOperator? to do the trick:

sage: D = sage.symbolic.operators.FDerivativeOperator
sage: D(eq_p,[0])
D[0](p == R*T*n/V)


The only way to get the desired outcome seems to be to re-write the whole equation:

sage: eq_p = p(t) == n(t)*R*T(t)/V(t)
sage: diff(eq_p,t)
D[0](p)(t) == R*n(t)*D[0](T)(t)/V(t) - R*T(t)*n(t)*D[0](V)(t)/V(t)^2 +
R*T(t)*D[0](n)(t)/V(t)


Alright, I thought, what about a system with constant volume?

sage: eq_p = p(t) == n(t)*R*T(t)/V
sage: diff(eq_p,t)
Boom!


Pity, would have been too easy. Interestingly, this works instead:

sage: eq_p = p(t) == n(t)*R*T(t)/V(x)
sage: diff(eq_p,t)
D[0](p)(t) == R*n(t)*D[0](T)(t)/V(x) + R*T(t)*D[0](n)(t)/V(x)


I'm still confused. What I want to express by function('V', t) is that V is a function of t and hence needs to be treated as such when taking the derivative with respect to t. By writing V(t) above, I turn the function into an expression again, which does lead to the desired functionality, but you mentioned earlier that V(t) means "Function V evaluated at t", which to me means something different. I don't see the utility of defining function('V', t) in the above at all. I could have equally defined function('V', x), right?

What would be the correct way to do the above consistently?

comment:21 in reply to: ↑ 20 ; follow-up: ↓ 22 Changed 5 years ago by nbruin

sage: function('p',t)
sage: function('n', t)
sage: function('T', t)
sage: function('V', t)
sage: diff(eq_p,t)
p == R*T*n/V
0 == 0


Just by rebinding the names in the global scope, you do not change the identity of the consituents in eq_p. Those are still variables. Pointwise operations aren't supported for functions:

sage: function('f')
f
sage: function('g')
g
sage: f*g
TypeError: unsupported operand type(s) for *: 'NewSymbolicFunction' and 'NewSymbolicFunction'


Alright, I thought, what about a system with constant volume?

sage: eq_p = p(t) == n(t)*R*T(t)/V


This already fails for me, because V at this point is a function and dividing a symbolic expression by a function isn't supported.

Pity, would have been too easy. Interestingly, this works instead:

sage: eq_p = p(t) == n(t)*R*T(t)/V(x)
sage: diff(eq_p,t)
D[0](p)(t) == R*n(t)*D[0](T)(t)/V(x) + R*T(t)*D[0](n)(t)/V(x)


I'm still confused. What I want to express by function('V', t) is that V is a function of t and hence needs to be treated as such when taking the derivative with respect to t.

I don't think you can, because in sage, symbolic functions have variable *positions*, not *names*.

By writing V(t) above, I turn the function into an expression again, which does lead to the desired functionality, but you mentioned earlier that V(t) means "Function V evaluated at t", which to me means something different. I don't see the utility of defining function('V', t) in the above at all. I could have equally defined function('V', x), right?

Or as function('V') for that matter. It seems misguided to me that function admits an argument list. It has no meaning other than that function('V',x) == function('V')(x). I think the RHS syntax is much clearer.

What would be the correct way to do the above consistently?

A "symbolic function" in sage is simply something that can occur in the "operator" slot of a symbolic expression. I don't think there is much support for algebra on such objects. Hence the need to talk about V(t) and V(x) (but better not in the same expression! Then you should use V(x,t) and be consistent about the order in which x,t occur). If you absolutely need to make the thing into a "function" again, you could turn it into a "callable symbolic expression":

sage: A = n(t)*R*T(t)/V(x)
sage: p=diff(A,t).function(t)
sage: p
t |--> R*n(t)*D[0](T)(t)/V(x) + R*T(t)*D[0](n)(t)/V(x)
sage: parent(p)
Callable function ring with arguments (t,)


You do have to decide beforehand if V is going to be a function of x or of t or of both. Also, the expression above should probably be

sage: p=diff(A,t).function(x,t)
sage: p
(x, t) |--> R*n(t)*D[0](T)(t)/V(x) + R*T(t)*D[0](n)(t)/V(x)
sage: parent(p)
Callable function ring with arguments (x, t)


The root cause of this is the following: sin is a function, right? You know the meaning of sin(x) and of sin(y), right? So is sin a function of x or of y? What should diff(sin,x) and diff(sin,y) be?

The answer is of course that sin by itself isn't a function of x or y. It simply is a function. It depends on the context what you put into it. In any case D[0](sin) is its derivative,

You may wish that sage would treat V differently, but it doesn't. Otherwise, if you do function('V',t), what should it do if you call V(x)? raise an error? What about V(t+1)? It really has no choice other than to ignore the name of the parameter and only look at its position in the argument list. Indeed, going back to the topic of the ticket, I recommend that the whole function('f',x) syntax gets deprecated or at least gets advised against in the documentation. It pretends that sage can do something with it that it can't.

comment:22 in reply to: ↑ 21 Changed 5 years ago by schymans

Or as function('V') for that matter. It seems misguided to me that function admits an argument list. It has no meaning other than that function('V',x) == function('V')(x). I think the RHS syntax is much clearer.
A "symbolic function" in sage is simply something that can occur in the "operator" slot of a symbolic expression. I don't think there is much support for algebra on such objects. Hence the need to talk about V(t) and V(x) (but better not in the same expression! Then you should use V(x,t) and be consistent about the order in which x,t occur). Indeed, going back to the topic of the ticket, I recommend that the whole function('f',x) syntax gets deprecated or at least gets advised against in the documentation. It pretends that sage can do something with it that it can't.
Thanks, this clarifies a lot for me! I didn't realise I have to think of 'function' as of an operator. I have been thinking about using vars for independent variables and functions for dependent variables, but as you clarified, this was misguided. My thumbs up to deprecate the function('f',x) syntax, then. Is there another way to write an expression with dependent and independent variables and then transparently differentiate it according to assumptions which of the dependent variables are kept constant?