%mathpiper,def="Plot2D"

//Retract("Plot2D", *);

//////////////////////////////////////////////////
/// Plot2D --- adaptive two-dimensional plotting
//////////////////////////////////////////////////

/// definitions of backends
//LoadScriptOnce("plots.rep/backends_2d.mpi");

/*
	Plot2D is an interface for various backends (Plot2D'...). It calls
Plot2D'get'data to obtain the list of points and values, and then it calls
Plot2D'<backend> on that data.

	Algorithm for Plot2D'get'data:
	1) Split the given interval into Quotient(points+3, 4) subintervals, and split each subinterval into 4 parts.
	2) For each of the parts: evaluate function values and call Plot2D'adaptive
	3) concatenate resulting lists and return
*/

LocalSymbols(var, func, range, option, options'list, delta, options'hash, c, fc, all'values, dummy)
[

// declaration of Plot2D with variable number of arguments
Function() Plot2D(func);
Function() Plot2D(func, range);
Function() Plot2D(func, range, options, ...);

/// interface routines
1 # Plot2D(_func) <-- ("Plot2D" @ {func, -5:5});
2 # Plot2D(_func, _range) <-- ("Plot2D" @ {func, range, {}});
3 # Plot2D(_func, _range, option_IsFunction) _ (Type(option) = "->" )  <-- ("Plot2D" @ {func, range, {option}});

/// Plot a single function
5 # Plot2D(_func, _range, options'list_IsList)_(Not IsList(func)) <-- ("Plot2D" @ {{func}, range, options'list});

/// Top-level 2D plotting routine:
/// plot several functions sharing the same xrange and other options
4 # Plot2D(func'list_IsList, _range, options'list_IsList) <--
[
	Local(var, func, delta, options'hash, c, fc, all'values, dummy);
	all'values := {};
	options'hash := "OptionsListToHash" @ {options'list};
    
    
	// this will be a string - name of independent variable
	options'hash["xname"] := "";
	// this will be a list of strings - printed forms of functions being plotted
	options'hash["yname"] := {};
	// parse range
	If (
		Type(range) = "->",	// variable also specified -- ignore for now, store in options
		[
			// store alternative variable name
			options'hash["xname"] := ToString(range[1]);
			range := range[2];
		]
	);
	If(
		Type(range) = ":",	// simple range
		range := N(Eval({range[1], range[2]}))
	);
	// set default option values
	If(
		options'hash["points"] = Empty,
		options'hash["points"] := 23
	);
	If(
		options'hash["depth"] = Empty,
		options'hash["depth"] := 5
	);
	If(
		options'hash["precision"] = Empty,
		options'hash["precision"] := 0.0001
	);
	If(
		options'hash["output"] = Empty Or IsString(options'hash["output"]) And Plot2DOutputs()[options'hash["output"]] = Empty,
		options'hash["output"] := Plot2DOutputs()["default"]
	);
	// a "filename" parameter is required when using data file
	If(
		options'hash["output"] = "datafile" And options'hash["filename"] = Empty,
		options'hash["filename"] := "output.data"
	);
    
	// we will divide each subinterval in 4 parts, so divide number of points by 4 now
	options'hash["points"] := N(Eval(Quotient(options'hash["points"]+3, 4)));
    
	// in case it is not a simple number but an unevaluated expression
	options'hash["precision"] := N(Eval(options'hash["precision"]));
    
	// store range in options
	options'hash["xrange"] := {range[1], range[2]};
    
	// compute the separation between grid points
	delta := N(Eval( (range[2] - range[1]) / (options'hash["points"]) ));
    
	// check that the input parameters are valid (all numbers)
	Check(IsNumber(range[1]) And IsNumber(range[2]) And IsNumber(options'hash["points"]) And IsNumber(options'hash["precision"]),
                "Argument",
		"Plot2D: Error: plotting range '"
		:(PipeToString()Write(range))
		:"' and/or the number of points '"
		:(PipeToString()Write(options'hash["points"]))
		:"' and/or precision '"
		:(PipeToString()Write(options'hash["precision"]))
		:"' is not numeric"
	);
	// loop over functions in the list
	ForEach(func, func'list)
	[
		// obtain name of variable
		var := VarList(func);        // variable name in a one-element list
                Check(Length(var)<=1, "Argument", "Plot2D: Error: expression is not a function of one variable: "
			:(PipeToString()Write(func))
		);
		// Allow plotting of constant functions
		If(Length(var)=0, var:={dummy});
		// store variable name if not already done so
		If(
			options'hash["xname"] = "",
			options'hash["xname"] := ToString(VarList(var)[1])
		);
		// store function name in options
		DestructiveAppend(options'hash["yname"], PipeToString()Write(func));
		// compute the first point to see if it's okay
		c := range[1];
		fc := N(Eval(Apply({var, func}, {c})));
		Check(IsNumber(fc) Or fc=Infinity Or fc= -Infinity Or fc=Undefined,
                        "Argument", 
			"Plot2D: Error: cannot evaluate function '"
			:(PipeToString()Write(func))
			:"' at point '"
			:(PipeToString()Write(c))
			:"' to a number, instead got '"
			:(PipeToString()Write(fc))
			:"'"
		);
		// compute all other data points
		DestructiveAppend(all'values,  Plot2D'get'data(func, var, c, fc, delta, options'hash) );
        
		If(InVerboseMode(), Echo({"Plot2D: using ", Length(all'values[Length(all'values)]), " points for function ", func}), True);
	];
    
	// call the specified output backend
	Plot2DOutputs()[options'hash["output"]] @ {all'values, options'hash};
];

//HoldArgument("Plot2D", range);
//HoldArgument("Plot2D", options);
HoldArgumentNumber("Plot2D", 2, 2);
HoldArgumentNumber("Plot2D", 3, 2);
HoldArgumentNumber("Plot2D", 3, 3);



//Retract("Plot2D'get'data", *);
/// this is the middle-level plotting routine; it generates the initial
/// grid, calls the adaptive routine, and gathers data points.
/// func must be just one function (not a list)
Plot2D'get'data(_func, _var, _x'init, _y'init, _delta'x, _options'hash) <--
[
	Local(i, a, fa, b, fb, c, fc, result);
	// initialize list by first points (later will always use Rest() to exclude first points of subintervals)
	result := { {c,fc} := {x'init, y'init} };
	For(i:=0, i<options'hash["points"], i++)
	[
		{a,fa} := {c, fc};	// this is to save time but here a = x'init + i*delta'x
		// build subintervals
		{b, c} := N(Eval({x'init + (i+1/2)*delta'x, x'init + (i+1)*delta'x}));	// this is not computed using "a" to reduce roundoff error
		{fb, fc} := N(Eval(MapSingle({var, func}, {b, c})));
		result := Concat(result,
			Rest(Plot2D'adaptive(func, var, {a,b,c}, {fa, fb, fc}, options'hash["depth"],
				// since we are dividing into "points" subintervals, we need to relax precision
				options'hash["precision"]*options'hash["points"] )));
	];
    
	result;
];

//////////////////////////////////////////////////
/// Plot2D'adaptive --- core routine to collect data
//////////////////////////////////////////////////
/*
	Plot2D'adaptive returns a list of pairs of coordinates { {x1,y1}, {x2,y2},...}
	All arguments except f() and var must be numbers. var is a one-element list containing the independent variable. The "a,b,c" and "fa, fb, fc" arguments are values of the function that are already computed -- we don't want to recompute them once more.
	See documentation (Algorithms.chapt.txt) for the description of the algorithm.
*/

//Retract("Plot2D'adaptive", *);
Plot2D'adaptive(_func, _var, {_a,_b,_c}, {_fa, _fb, _fc}, _depth, _epsilon) <--
[
	Local(a1, b1, fa1, fb1);

	a1 := N(Eval((a+b)/2));
	b1 := N(Eval((b+c)/2));
	{fa1, fb1} := N(Eval(MapSingle({var, func}, {a1, b1})));
	If(
		depth<=0 Or
		(
		  // condition for the values not to oscillate too rapidly
		  sign'change(fa, fa1, fb) + sign'change(fa1, fb, fb1) + sign'change(fb, fb1, fc) <= 2
		  And
		  // condition for the values not to change too rapidly
		  N(Eval(Abs( (fa-5*fa1+9*fb-7*fb1+2*fc)/24 ) ))	// this is the Simpson quadrature for the (fb,fb1) subinterval (using points b,b1,c), subtracted from the 4-point Newton-Cotes quadrature for the (fb,fb1) subinterval (using points a, a1, b, b1)
		  	<= N(Eval( epsilon*(	// the expression here will be nonnegative because we subtract the minimum value
		    //(fa+fc+2*fb+4*(fa1+fb1))/12	// this is 1/4 of the Simpson quadrature on the whole interval
			(5*fb+8*fb1-fc)/12	// this is the Simpson quadrature for the (fb,f1) subinterval
			- Minimum({fa,fa1,fb,fb1,fc}) ) ) )
		),
		// okay, do not refine any more
		{{a,fa}, {a1,fa1}, {b,fb}, {b1,fb1}, {c,fc}},
		// not okay, need to refine more
		Concat(
			// recursive call on two halves of the interval; relax precision by factor of 2
			Plot2D'adaptive(func, var, {a, a1, b}, {fa, fa1, fb}, depth-1, epsilon*2),	// Rest() omits the pair {b, fb}
			Rest(Plot2D'adaptive(func, var, {b, b1, c}, {fb, fb1, fc}, depth-1, epsilon*2))
		)
	);
];

];	// LocalSymbols()


%/mathpiper

    %output,preserve="false"
      Result: True
.   %/output




%mathpiper_docs,name="Plot2D",categories="User Functions;Visualization"
*CMD Plot2D --- adaptive two-dimensional plotting
*STD
*CALL
	Plot2D(f(x))
	Plot2D(f(x), a:b)
	Plot2D(f(x), a:b, option -> value)
	Plot2D(f(x), a:b, option -> value, ...)
	Plot2D(list, ...)

*PARMS

{f(x)} -- unevaluated expression containing one variables (function to be plotted)

{list} -- list of functions to plot

{a}, {b} -- numbers, plotting range in the $x$ coordinate

{option} -- atom, option name

{value} -- atom, number or string (value of option)

*DESC
The routine {Plot2D} performs adaptive plotting of one or several functions
of one variable in the specified range.
The result is presented as a line given by the equation $y=f(x)$.
Several functions can be plotted at once.
Various plotting options can be specified.
Output can be directed to a plotting program (the default is to use
{data}) to a list of values.

The function parameter {f(x)} must evaluate to a MathPiper expression containing
at most one variable. (The variable does not have to be called {x}.)
Also, {N(f(x))} must evaluate to a real (not complex) numerical value when given a numerical value of the argument {x}.
If the function {f(x)} does not satisfy these requirements, an error is raised.

Several functions may be specified as a list and they do not have to depend on the same variable, for example, {{f(x), g(y)}}.
The functions will be plotted on the same graph using the same coordinate ranges.

If you have defined a function which accepts a number but does not
accept an undefined variable, {Plot2D} will fail to plot it.
Use {NFunction} to overcome this difficulty.

Data files are created in a temporary directory {/tmp/plot.tmp/} unless otherwise requested.
File names
and other information is printed if {InVerboseMode()} returns {True} on using {V()}.

The current algorithm uses Newton-Cotes quadratures and some heuristics for error estimation (see <*mathpiperdoc://Algo/3/1/*>).
The initial grid of {points+1} points is refined between any grid points $a$, $b$ if the integral
$Integrate(x,a,b)f(x)$ is not approximated to the given precision by
the existing grid.

Default plotting range is {-5:5}. Range can also be specified as {x= -5:5} (note the mandatory space separating "{=}" and "{-}");
currently the variable name {x} is ignored in this case.

Options are of the form {option -> value}. Currently supported option names
are: "points", "precision", "depth", "output", "filename", "yrange". Option values
are either numbers or special unevaluated atoms such as {data}.
If you need to use the names of these atoms
in your script, strings can be used. Several option/value pairs may be specified (the function {Plot2D} has a variable number of arguments).

*	{yrange}: the range of ordinates to use for plotting, e.g.
{yrange=0:20}. If no range is specified, the default is usually to
leave the choice to the plotting backend. 
*	{points}: initial number of points (default 23) -- at least that
many points will be plotted. The initial grid of this many points will be
adaptively refined. 
*	{precision}: graphing precision (default $10^(-6)$). This is interpreted as the relative precision of computing the integral of $f(x)-Minimum(f(x))$ using the grid points. For a smooth, non-oscillating function this value should be roughly 1/(number of screen pixels in the plot).
*	{depth}: max. refinement depth, logarithmic (default 5) -- means there will be at most $2^depth$ extra points per initial grid point.
*	{output}: name of the plotting backend. Supported names: {data} (default).
The {data} backend will return the data as a list of pairs such as {{{x1,y1}, {x2,y2}, ...}}.
*	{filename}: specify name of the created data file. For example: {filename="data1.txt"}.
The default is the name {"output.data"}.
Note that if several functions are plotted, the data files will have a number appended to the given name, for example {data.txt1}, {data.txt2}.

Other options may be supported in the future.

The current implementation can deal with a singularity within the plotting range only if the function {f(x)} returns {Infinity}, {-Infinity} or
{Undefined} at the singularity.
If the function {f(x)} generates a numerical error and fails at a
singularity, {Plot2D} will fail if one of the grid points falls on the
singularity.
(All grid points are generated by bisection so in principle the
endpoints and the {points} parameter could be chosen to avoid numerical
singularities.)

*WIN32



*SEE V, NFunction, Plot3DS
%/mathpiper_docs

    %output,preserve="false"
      
.   %/output


