5

I was trying to draw the well-know Joule expansion experiment in Tikz, and I managed to get through it thanks to many (many) other topics.

However, it seems to by quite slow, as it takes 10 seconds to compile and show the final image. I'm thinking that the way I designed the diagram is not efficient enough, but I'm having a hard time thinking how to do it differently.

Does anyone have an idea about why it takes so long?

Here's the code:

\documentclass[tikz]{standalone}

\usepackage{pgfplots} \pgfplotsset{compat=newest}
\usetikzlibrary{patterns}
\usetikzlibrary{calc}
\usepgfplotslibrary{fillbetween}

\begin{document}
    \begin{tikzpicture}[scale = 2]

\begin{scope}[xshift = -2cm]

    \draw[name path = one] (-.2,.2) node (a){} -- (-.2,1) node (b){} -- (-1.5,1) node (c){} -- (-1.5,-1) node (d){} -- (-.2,-1) node (e){} -- (-.2,-.2) node (f){} -- (.2,-.2) node (g){} -- (.2,-1) node (h){} -- (1.5,-1) node (i){} -- (1.5,1) node (j){} -- (.2,1) node (k){} -- (.2,.2) node (l){} -- cycle;

    \draw[white, name path = two] ($(a)+(.1,.1)$) -- ($(b)+(.1,.1)$) -- ($(c) + (-.1,.1)$) -- ($(d) + (-.1,-.1)$) -- ($(e) + (.1,-.1)$) -- ($(f) + (.1,-.1)$) -- ($(g) + (-.1,-.1)$) -- ($(h) + (-.1,-.1)$) -- ($(i) + (.1,-.1) $)-- ($(j) + (.1,.1)$) -- ($(k) + (-.1,.1)$) -- ($(l) + (-.1,.1)$) -- cycle;

    \tikzfillbetween[of=two and one]{pattern=north east lines};

    \draw (0,-.35) -- (0,.35) (-.1,.35) -- (.1,.35);

        \begin{scope}
            \clip (-.2,.2) -- (-.2,1) -- (-1.5,1) -- (-1.5,-1) -- (-.2,-1) -- (-.2,-.2) -- (0,-.2) -- (0,.2) -- cycle;
            \foreach \i in {1,...,1000} \fill[red] (rand-.5, rand) circle (.25pt);
        \end{scope}

\end{scope}

    \draw[black, -stealth, line width = 1pt] (-.3,0) -- (.3,0);

\begin{scope}[xshift = 2cm]

    \draw[name path = one] (-.2,.2) node (a){} -- (-.2,1) node (b){} -- (-1.5,1) node (c){} -- (-1.5,-1) node (d){} -- (-.2,-1) node (e){} -- (-.2,-.2) node (f){} -- (.2,-.2) node (g){} -- (.2,-1) node (h){} -- (1.5,-1) node (i){} -- (1.5,1) node (j){} -- (.2,1) node (k){} -- (.2,.2) node (l){} -- cycle;

    \draw[white, name path = two] ($(a)+(.1,.1)$) -- ($(b)+(.1,.1)$) -- ($(c) + (-.1,.1)$) -- ($(d) + (-.1,-.1)$) -- ($(e) + (.1,-.1)$) -- ($(f) + (.1,-.1)$) -- ($(g) + (-.1,-.1)$) -- ($(h) + (-.1,-.1)$) -- ($(i) + (.1,-.1) $)-- ($(j) + (.1,.1)$) -- ($(k) + (-.1,.1)$) -- ($(l) + (-.1,.1)$) -- cycle;

    \tikzfillbetween[of=two and one]{pattern=north east lines};

    \draw[densely dashed] (0,-.35) -- (0,.35) (-.1,.35) -- (.1,.35);

        \begin{scope}
            \clip (-.2,.2) -- (-.2,1) -- (-1.5,1) -- (-1.5,-1) -- (-.2,-1) -- (-.2,-.2) -- (.2,-.2)  -- (.2,-1) -- (1.5,-1) -- (1.5,1) -- (.2,1) -- (.2,.2) -- cycle;
            \foreach \i in {1,...,1000} \fill[red] (2*rand, rand) circle (.25pt);
        \end{scope}

\end{scope}

    \end{tikzpicture}
\end{document}

Joule expansion diagram

As you can see, the way I did the shape works well, but making it line by line seems... childish, even if it allows me to define the white path that I used to make the north east lines "quite" easily. I thought of using 3 rectangles to make the connected boxes, but then the patterning seems more difficult. Still I'm sure there's a less difficult way to do that.

Thanks for your help !

  • 1
    Takes about 2 s here (user 0m2.102s) . I think the code is ok, rand binding the most resources. – AlexG Feb 7 '18 at 14:26
  • 1
    Instead of white, I would use draw=none for the outer shape. – AlexG Feb 7 '18 at 14:29
  • ... and if you use externalize, you do not have to redo the computation every time you compile your document. I think 10s for this nice picture is quite good (but on my machine it only takes about 2). – user121799 Feb 7 '18 at 14:57
  • I looked into it, but I was actually loading a *.pdf created by another *.tex, so it wasn't supposed to take more time. It would seem that the internal PDF viewer of TeXMaker was the issue here. Using an external viewer works very well, although it's not as convenient to make small changes in the figure. Thanks for the draw=none @AlexG, it makes more sense that way. – François NICOLAS Feb 7 '18 at 15:01
  • 1
    It doesn't really answer your question about compilation time, but would you be interested in an answer showing a slightly more concise, and parametrized, method for drawing the frame? – Torbjørn T. Feb 7 '18 at 15:09
5

You can make use of perpendicular coordinates (see TikZ: What EXACTLY does the the |- notation for arrows do?) to make the code a bit more concise. I also took a different approach, defining some constants describing the size of the chambers, and using relative coordinates instead of absolute coordinates. Note I used rnd instead of rand, which generates numbers between 0 and 1, instead of between -1 and 1.

See comments in code, ask if anything is mysterious.

output of first code block

\documentclass[tikz,border=5mm]{standalone}

\usepackage{pgfplots}
\pgfplotsset{compat=newest}
\usetikzlibrary{patterns}
\usetikzlibrary{calc}
\usepgfplotslibrary{fillbetween}

\begin{document}
\begin{tikzpicture}[
  % define some constants used below
  declare function={
    H=4; % width of chamber
    W=2; % height of chamber
    sep=0.7; % distance between chambers
    tochannel=1.5; % vertical distance from chamber edge to "channel"
    pw=0.1; % width of hatched region
}]

% draw outline
\draw[name path = one]
    (0,0)             coordinate (a) |-
  ++(W,H)             coordinate (c) |-
  ++(sep,-tochannel)  coordinate (e) |-
  ++(W,tochannel)     coordinate (g) |-
  ++(-W,-H)           coordinate (i) |-
  ++(-sep,tochannel)  coordinate (k) |-
  cycle
;

% define midpoints in channel
\path
(c|-e) -- coordinate (mid1) (e)
(k-|e) -- coordinate (mid2) (k)
;

% make path around outer edge of hatched region
% don't need to draw the path, hence \path
\path[name path = two] ($(a) + (-pw,-pw)$) |- ($(c) + (pw, pw)$) |- 
                       ($(e) + (-pw, pw)$) |- ($(g) + (pw, pw)$) |- 
                       ($(i) + (-pw,-pw)$) |- ($(k) + (pw,-pw)$) |-
                       cycle;

\tikzfillbetween[of=two and one]{pattern=north east lines};

% draw "gate" (or whatever it's called)
\draw ([yshift=-0.2cm*tochannel]mid2) -- ([yshift=0.2cm*tochannel]mid1)
          ++(sep/2-pw,0) -- ++(-sep+2*pw,0);

\begin{scope}
    \clip (a) |- (c) |- (e) |- (mid1) |- (k) |- cycle;
    \foreach \i in {1,...,1000}
        \fill[red] ({rnd*(W+0.5*sep)}, H*rnd) circle[radius=0.25pt];
\end{scope}


% second part
\draw[name path = one]
    % modify only the starting coordinate
    (2*W+sep+1,0)    coordinate (a)  |-
  ++(W,H)             coordinate (c) |-
  ++(sep,-tochannel)  coordinate (e) |-
  ++(W,tochannel)     coordinate (g) |-
  ++(-W,-H)           coordinate (i) |-
  ++(-sep,tochannel)  coordinate (k) |-
  cycle
;

% define midpoints in channel
\path
(c|-e) -- coordinate (mid1) (e)
(k-|e) -- coordinate (mid2) (k)
;

\path[name path = two] ($(a) + (-pw,-pw)$) |- ($(c) + (pw, pw)$) |- 
                       ($(e) + (-pw, pw)$) |- ($(g) + (pw, pw)$) |- 
                       ($(i) + (-pw,-pw)$) |- ($(k) + (pw,-pw)$) |-
                       cycle;

\tikzfillbetween[of=two and one]{pattern=north east lines};

\draw [densely dashed] ([yshift=-0.2cm*tochannel]mid2) -- ([yshift=0.2cm*tochannel]mid1)
          ++(sep/2-pw,0) -- ++(-sep+2*pw,0);

\begin{scope}
    \clip (a) |- (c) |- (e) |- (g) |- (i) |- (k) |- cycle;
    \foreach \i in {1,...,1000}
       \fill[red] ($(a)+({rnd*(2*W+sep)}, H*rnd)$) circle[radius=.25pt];
\end{scope}


% arrow in middle
\draw [-stealth, line width = 1pt] (a) ++(-0.75,H/2) -- ++(0.5,0);
\end{tikzpicture}
\end{document}

No clipping

If one wants to avoid the clipping, this might be an option. What I've done is to split the point generation over two (or three) loops, one for each chamber, one for the channel between them. To get the same density, the number of points generated in the channel is calculated based on the number of points in the chamber, and the ratio of the areas the points cover. To make this a bit more convenient I added a function calculating the height of the channel, as channelheight=H-2*tochannel;.

The point generation for the first part is then:

% set number of points in chamber
\pgfmathsetmacro{\pointsInChamber}{1000}
% - area in channel covered by points is 0.5*sep*channelheight
% - area of chamber is H/W
\pgfmathsetmacro{\pointsInChannel}{\pointsInChamber*0.5*sep*channelheight/(H*W)}

% loop for chamber
\foreach \i in {1,...,\pointsInChamber}
    \fill[red] (rnd*W, rnd*H) circle[radius=0.25pt];
% loop for channel
\foreach \i in {1,...,\pointsInChannel}
    \fill[red] (W+rnd*sep/2, tochannel+rnd*channelheight) circle[radius=0.25pt];

output of second code block

\documentclass[tikz,border=5mm]{standalone}

\usepackage{pgfplots}
\pgfplotsset{compat=newest}
\usetikzlibrary{patterns}
\usetikzlibrary{calc}
\usepgfplotslibrary{fillbetween}

\begin{document}
\begin{tikzpicture}[
  % define some constants used below
  declare function={
    H=4; % width of chamber
    W=2; % height of chamber
    sep=0.7; % distance between chambers
    tochannel=1.5; % vertical distance from chamber edge to "channel"
    channelheight=H-2*tochannel;
    pw=0.1; % width of hatched region
}]


% draw outline
\draw[name path = one]
    (0,0)             coordinate (a) |-
  ++(W,H)             coordinate (c) |-
  ++(sep,-tochannel)  coordinate (e) |-
  ++(W,tochannel)     coordinate (g) |-
  ++(-W,-H)           coordinate (i) |-
  ++(-sep,tochannel)  coordinate (k) |-
  cycle
;

% define midpoints in channel
\path
(c|-e) -- coordinate (mid1) (e)
(k-|e) -- coordinate (mid2) (k)
;

% make path around outer edge of hatched region
% don't need to draw the path
\path[name path = two] ($(a) + (-pw,-pw)$) |- ($(c) + (pw, pw)$) |- 
                       ($(e) + (-pw, pw)$) |- ($(g) + (pw, pw)$) |- 
                       ($(i) + (-pw,-pw)$) |- ($(k) + (pw,-pw)$) |-
                       cycle;

\tikzfillbetween[of=two and one]{pattern=north east lines};

% draw "gate" (or whatever it's called)
\draw ([yshift=-0.2cm*tochannel]mid2) -- ([yshift=0.2cm*tochannel]mid1)
          ++(sep/2-pw,0) -- ++(-sep+2*pw,0);


\pgfmathsetmacro{\pointsInChamber}{1000}
\pgfmathsetmacro{\pointsInChannel}{\pointsInChamber*0.5*sep*channelheight/(H*W)}

\foreach \i in {1,...,\pointsInChamber}
    \fill[red] (rnd*W, rnd*H) circle[radius=0.25pt];
\foreach \i in {1,...,\pointsInChannel}
    \fill[red] (W+rnd*sep/2, tochannel+rnd*channelheight) circle[radius=0.25pt];



% second part
\draw[name path = one]
    % modify only the starting coordinate
    (2*W+sep+1,0)    coordinate (a)  |-
  ++(W,H)             coordinate (c) |-
  ++(sep,-tochannel)  coordinate (e) |-
  ++(W,tochannel)     coordinate (g) |-
  ++(-W,-H)           coordinate (i) |-
  ++(-sep,tochannel)  coordinate (k) |-
  cycle
;

% define midpoints in channel
\path
(c|-e) -- coordinate (mid1) (e)
(k-|e) -- coordinate (mid2) (k)
;

\path[name path = two] ($(a) + (-pw,-pw)$) |- ($(c) + (pw, pw)$) |- 
                       ($(e) + (-pw, pw)$) |- ($(g) + (pw, pw)$) |- 
                       ($(i) + (-pw,-pw)$) |- ($(k) + (pw,-pw)$) |-
                       cycle;

\tikzfillbetween[of=two and one]{pattern=north east lines};

\draw [densely dashed] ([yshift=-0.2cm*tochannel]mid2) -- ([yshift=0.2cm*tochannel]mid1)
          ++(sep/2-pw,0) -- ++(-sep+2*pw,0);


\pgfmathsetmacro{\pointsInChamber}{\pointsInChamber/2}
\pgfmathsetmacro{\pointsInChannel}{\pointsInChamber*sep*channelheight/(H*W)}

\foreach \i in {1,...,\pointsInChamber}
    \fill[red] ($(a)+(rnd*W, rnd*H)$) circle[radius=0.25pt];
\foreach \i in {1,...,\pointsInChannel}
    \fill[red] ($(a)+(W+rnd*sep, tochannel+rnd*channelheight)$) circle[radius=0.25pt];
\foreach \i in {1,...,\pointsInChamber}
    \fill[red] ($(i)+(rnd*W, rnd*H)$) circle[radius=0.25pt];

% arrow in middle
\draw [-stealth, line width = 1pt] (a) ++(-0.75,H/2) -- ++(0.5,0);
\end{tikzpicture}
\end{document}
  • Wow, it's in fact way more beautifully put that way! I didn't know about the declare function command... that changes everything :-). I still don't get how you manage to define mid1and mid2 with your command. I get that mid1 is the middle of "(d)" and (e), but I don't understand the notation (c|-e)... – François NICOLAS Feb 7 '18 at 16:29
  • 1
    @FrançoisNICOLAS That is described in the question I linked to. (c |- e) is the coordinate with the x-component of c and y-component of e. – Torbjørn T. Feb 7 '18 at 16:37
  • Indeed I had forgotten to read your link. That is great! Thanks a lot, I am very satisfied with this answer! – François NICOLAS Feb 7 '18 at 16:45
  • I tried to put a number of atoms as a parameter in the declare function command and switching the 1000 by N in \foreach \i in {1,...,N}, but somehow I have 100 atoms at most, even though I declared N =1000;. I checked online, the \foreach command does take a letter or parameter as an argument (meaning that \def \N {1000} works). Would you happen to know why a declared function doesn't work for foreach? – François NICOLAS Feb 7 '18 at 16:58
  • 1
    @FrançoisNICOLAS Don't know for sure, but that it doesn't work probably means that the end value in the loop (N) isn't passed through \pgfmathparse, so it is never evaluated as a function. I just updated my answer by the way. – Torbjørn T. Feb 7 '18 at 17:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.