Understanding why NIntegrate requires explicit substitution of variables in argument

The problem that leads me here begins with a quantity I have previously defined, let's call it test, that has many other quantities in its definition. When evaluated, test is an expression that includes two variables, let's call them k and x. A MWE would be

test = kx;


I wish to create a function of k that includes a NIntegration of test over x with limits that involve k. An example would be

testint[k_] := NIntegrate[test, {x, k, 2k}];


Evaluating this for some k , say k = .1, returns an error:

>>testint[.1]

NIntegrate::inumr: The integrand k x has evaluated to non-numerical values for all sampling points in the region with boundaries {{0.1,0.2}}.

NIntegrate[k x, {x, 0.1, 0.2}]


However, if I define testint using a temporary variable and perform a replacement in the argument of NIntegrate , then it computes fine:

>>testint[k1_] := NIntegrate[test/.k->k1, {x, k1, 2k1}];
>>testint[.1]
0.0015


I found this answer, which led me to try the explicit substitution: Replace variable with value prior to evaluating NIntegrate
Another answer addresses the order of NIntegrate with the help of the ?NumericQ pattern check: How do I prevent NIntegrate::inumr errors within other functions?

My question is Why does NIntegrate require this explicit substitution in order to compute?

As a test, I even tried removing NIntegrates HoldAll attribute thinking that would force the evaluation of test before the integration. It did, but not soon enough to help.

>>test = k x;
>>ClearAttributes[NIntegrate, HoldAll]
>>testint[k_] := NIntegrate[testin, {x, k, 2 k}];
>>testint[.1]//Trace

NIntegrate::inumr: The integrand k x has evaluated to non-numerical values for all sampling points in the region with boundaries {{0.1,0.2}}.

{testint[0.1], NIntegrate[test, {x, 0.1, 2 0.1}], {test, k x}, {{2 0.1, 0.2}, {x, 0.1, 0.2}}, NIntegrate[k x, {x, 0.1, 0.2}], {{x} =., {x =.}, {x =., Null}, {Null}}, {x =., Null}, ...


• (1) kx is not the same as k*x or k x (note space). (2) The evaluation might be less noisy if you define testint[k_?NumericQ] := ... Alternatively could do NIntegrate[...,Method->{"SymbolicProcessing"->None}] in case that's the cause of the messages (I have not tested this) – Daniel Lichtblau Nov 21 '19 at 18:12

The definition testint[k_] := NIntegrate[test, {x, k, 2k}]; replaces only the k that appear in the literal code. Thus testint[0.1] returns the expression

NIntegrate[test, {x, 0.1, 2*0.1}]


Only then is test evaluated, at which point the integrand becomes k x, after testint[0.1] is done and all the instances of k have been replaced (that are going to be replaced). Thus the trouble is in how testint and test are defined, not with NIntegrate per se.

This can be observed in the output of Trace[]:

testint[0.1] // Trace

• Another way to look at it is that the argument k to testint is localized and the k in test is Global , but that is rather a half-explanation, since the localization mechanism makes a difference. NIntegrate localizes x effectively with Block, a different mechanism. – Michael E2 Nov 23 '19 at 21:29
• I'm not sure what the purpose of the OP's code is. One fix is to make dependencies explicit: ClearAll[test]; test[k_, x_] := k*x`. – Michael E2 Nov 23 '19 at 21:30