2

I want to plot a lognormal function:

\documentclass{article}
\usepackage{pgfplots}

\begin{document}    
\pgfmathdeclarefunction{lognormal}{2}{%
%  \pgfkeys{/pgf/fpu=true}
  \pgfmathparse{1/(x*#2*sqrt(2*pi))*exp(-((ln(x)-#1)^2)/(2*#2^2))}%
%  \pgfmathresult
%  \pgfkeys{/pgf/fpu=false}
}
\begin{tikzpicture}
\begin{axis}[ domain=0.01:10 ]
  \addplot {lognormal(ln(5),0.02)};
\end{axis}
\end{tikzpicture}
\end{document}

It produces "Dimension too large" even that the function has moderate values. I guess that the problem lies in intermediate values within exp(). I tried /pgf/fpu=true (commented out in the source) as suggested in this answer but it did not help and even additionally confused PGF. The latter can be verified by replacing lognormal(ln(5),0.02) with lognormal(ln(5),1).

How to draw the lognormal function in pgf? I tried to replace the problematic parts of the domain with y ~ 0:

\pgfmathparse{1/(x*#2*sqrt(2*pi))*exp(ifthenelse((ln(x)-#1)/sqrt(#2) < -10, -100, -((ln(x)-#1)^2)/(2*#2^2)))}

As seen, I do not use a square in the condition in order to avoid an overflow. However, for certain parameters, processing a single chart with that equation takes several seconds, and of the whole Tex document, with many charts, more than a minute. Is there a faster, more precise and more elegant solution?

1 Answer 1

3

Edit2:

The small wrapper made its way to CTAN. So the below code with an up to date LaTeX distribution is now just:

\documentclass{article}
\usepackage{pgfplots}
\usepackage{pgfmath-xfp}

\pgfmxfpdeclarefunction{lognormal}{3}
  {exp(-((ln(#1) - #2)^2) / (2 * (#3)^2)) / (#1 * #3 * sqrt(2 * pi))}

\begin{document}    
\begin{tikzpicture}
\begin{axis}[ domain=0.01:10, samples=100 ]
  \addplot {lognormal(x,ln(5),0.02)};
\end{axis}
\end{tikzpicture}
\end{document}

Edit:

I've created a small wrapper to define pgfmath-functions using l3fp first posted here. With it the MWE here boils down to the following (the things between \ExplSyntaxOn and \ExplSyntaxOff is the wrapper):

\documentclass{article}
\usepackage{pgfplots}

\ExplSyntaxOn
\tl_new:N \l_pgffpeval_function_body_tl
\tl_new:N \l_pgffpeval_function_definition_tl
\int_new:N \l_pgffpeval_tmp_int
\cs_new_protected:Npn \pgffpeval_declare_function:nnn #1#2#3
  {
    \__pgffpeval_initialize_body:
    \int_step_inline:nn {#2}
      {
        \tl_put_right:Nx \l_pgffpeval_function_body_tl
          {
            \exp_not:n { \pgfmathsetmacro } \exp_not:c { __pgffpeval_arg##1 }
              { \exp_not:n {####} ##1 }
          }
      }
    \__pgffpeval_define_function:nnnn {#2} {#1} {#2} {#3}
  }
\cs_new_protected:Npn \pgffpeval_declare_function_processed_args:nnnn #1#2#3#4
  {
    \__pgffpeval_initialize_body:
    \int_zero:N \l_pgffpeval_tmp_int
    \clist_map_inline:nn {#3}
      {
        \int_incr:N \l_pgffpeval_tmp_int
        \tl_put_right:Nx \l_pgffpeval_function_body_tl
          {
            \exp_not:n { \pgfmathsetmacro }
              \exp_not:c { __pgffpeval_arg \int_use:N \l_pgffpeval_tmp_int }
              { \exp_not:n {##1} }
          }
      }
    \exp_args:NV
    \__pgffpeval_define_function:nnnn \l_pgffpeval_tmp_int {#1} {#2} {#4}
  }
\cs_new_protected:Npn \__pgffpeval_initialize_body:
  {
    \tl_set:Nn \l_pgffpeval_function_body_tl
      {
        \group_begin:
        \pgfkeys{/pgf/fpu=true, /pgf/fpu/output~format=sci}%
      }
  }
\cs_new:Npn \__pgffpeval_process_function_aux:n #1 { \exp_not:n {## #1} }
\cs_new_protected:Npn \__pgffpeval_process_function:nnn #1#2#3
  {
    \exp_last_unbraced:Nx
    \cs_set_protected:cpn
      {
        { __pgffpeval_function_ #2 _cmd }
        \int_step_function:nN {#1} \__pgffpeval_process_function_aux:n
      }
      { \group_end: \exp_args:Nf \pgfmathparse { \fp_eval:n {#3} } }
  }
\cs_new_protected:Npn \__pgffpeval_define_function:nnnn #1#2#3#4
  {
    \__pgffpeval_process_function:nnn {#1} {#2} {#4}
    \tl_put_right:Nx \l_pgffpeval_function_body_tl
      {
        \use:x
          { 
            \exp_not:c { __pgffpeval_function_ #2 _cmd }
            \int_step_function:nN {#1} \__pgffpeval_define_function_aux:n
          }
      }
    \exp_args:Nnno
    \pgfmathdeclarefunction {#2} {#3} \l_pgffpeval_function_body_tl
  }
\cs_new:Npn \__pgffpeval_define_function_aux:n #1
  { { \exp_not:c { __pgffpeval_arg#1 } } }
\NewDocumentCommand \pgfmathdeclarefpevalfunction { m m o m }
  {
    \IfValueTF {#3}
      { \pgffpeval_declare_function_processed_args:nnnn {#1} {#2} {#3} }
      { \pgffpeval_declare_function:nnn {#1} {#2} }
      {#4}
  }
\ExplSyntaxOff

\pgfmathdeclarefpevalfunction{lognormal}{3}
  {exp(-((ln(#1) - #2)^2) / (2 * (#3)^2)) / (#1 * #3 * sqrt(2 * pi))}

\begin{document}    
\begin{tikzpicture}
\begin{axis}[ domain=0.01:10, samples=100 ]
  \addplot {lognormal(x,ln(5),0.02)};
\end{axis}
\end{tikzpicture}
\end{document}

Output and explanation like below.


The following uses xfp for the actual calculation, and \pgfmathsetmacro with the options /pgf/fpu=true and /pgf/fpu/output format=sci applied locally to ensure the input number format is understandable for xfp (as pgfmath uses a custom internal number representation when the FPU is used, which isn't understood by xfp).

The \romannumeral is used to ensure that \fpeval is fully done when the group is closed and therefore \argA, \argB, and \argC don't have the correct meaning anymore. The result is then again parsed by \pgfmathparse to make sure that \pgfmathresult holds the correct format for pgf outside of the function.

\documentclass{article}
\usepackage{pgfplots}

\usepackage[]{xfp}

\begin{document}    
\pgfmathdeclarefunction{lognormal}{3}{%
  \begingroup
    \pgfkeys{/pgf/fpu=true, /pgf/fpu/output format=sci}%
    \pgfmathsetmacro\argA{#1}%
    \pgfmathsetmacro\argB{#2}%
    \pgfmathsetmacro\argC{#3}%
    \expandafter
  \endgroup
  \expandafter\pgfmathparse\expandafter
    {%
      \romannumeral`\^^@%
      \fpeval{exp(-((ln(\argA)-\argB)^2)/(2*\argC^2))/(\argA*\argC*sqrt(2*pi))}%
    }%
}
\begin{tikzpicture}
\begin{axis}[ domain=0.01:10, samples=100 ]
  \addplot {lognormal(x,ln(5),0.02)};
\end{axis}
\end{tikzpicture}
\end{document}

Is it fast? No. Does it work? Yes:

enter image description here

3
  • It is not fast for lognormal(x,ln(5),0.02) but it is fast for lognormal(x,ln(5),0.2), as if larger values made the computation slower. If fpu is used, where is exactly the bottleneck?
    – scriptfoo
    Feb 26, 2021 at 14:47
  • @scriptfoo I have no idea, as I don't know much about the internals of both, pgfmath's FPU and l3fp.
    – Skillmon
    Feb 26, 2021 at 16:15
  • 2
    @scriptfoo I did some benchmarking and l3fp (the thing behind xfp) is only marginally affected by the bigger value. An entire sweep with 100 values from 0.01 to 10 takes about 2.6% more time in the calculation for the smaller value, whereas a single assignment using \pgfmathsetmacro\tmp{<num>} takes 13.6% more time for the smaller value. So the choke is pgfmath's number parsing.
    – Skillmon
    Feb 26, 2021 at 16:38

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .