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 !

Torbjørn T.
  • 206,688
  • 1
    Takes about 2 s here (user 0m2.102s) . I think the code is ok, rand binding the most resources. – AlexG Feb 07 '18 at 14:26
  • 1
    Instead of white, I would use draw=none for the outer shape. – AlexG Feb 07 '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). –  Feb 07 '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 07 '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 07 '18 at 15:09
  • Maybe you could refine the code such as to make sure that a random coordinate is inside the allowed shape and redo computation if not. So that you get the same number of atoms in each picture. – AlexG Feb 07 '18 at 15:11
  • AlexG: That sounds interesting. I didn't find how to fill a specific area with a \foreach command, so as to have a random pattern... so I used a clip and a huge number of points so that it looks OK. Any idea? @TorbjørnT.: yes I would! That was part of my asking here, I feel there's a better way to do that. What do you have in mind? – François NICOLAS Feb 07 '18 at 15:24

1 Answers1

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}
Torbjørn T.
  • 206,688
  • 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 07 '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 07 '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 07 '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 07 '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 07 '18 at 17:07
  • Thank you for this solution. It's quite elegant that way ; though it was not a main concern it fits with what I imagined first! – François NICOLAS Feb 07 '18 at 17:18