Package: pulseplot
pulseplotTired of opening Inkspace or (gasp) Illustrator to make
simple pulse-timing diagrams? Want to combine spectra and
pulse-timing diagrams on a single plot without having to manually
piece them together? Want an easy way to make shaped pulses, hatches, indicates phases, FIDs, and animation? How about something like this:importpulseplotaspplotspin_echo=r"""p1 ph1 fc=blackd10 tx=$\tau$p2 ph2d10 tx=$\tau$p10 sp=fid phrec"""fig,ax=pplot.subplots(figsize=(7,2))ax.pseq(spin_echo)fig.savefig("spin_echo.png",dpi=150)Or, maybe something more complicated?See SourcePerhaps some simple animation?See SourceRequirementsPython 3.7 or higherMatplotlib 3.3 or higherNumpyInstallationUsing pipGet the stable version from PyPi usingpython -m pip install pulseplotElse, you can also get the latest version from GitHub usingpython -m pip install git+https://github.com/kaustubhmote/pulseplotInstall from sourcegit clone https://github.com/kaustubhmote/pulseplot
cd pulseplot
[activate your virtual environment]
python -m pip install -r requirements.txt
python -m pip install .UsageThe basicsStart with importing the module.>>>importpulseplotaspplot>>>fig,ax=pplot.subplots(nrows=2,ncols=2)>>>fig,ax=pplot.subplot_mosiac("AB\nCD")pulseplotpiggy-backs on matplotlib's object-oriented interface. Just likeplt.subplots(pltis short formatplotlib.pyplot), the functionpplot.subplotsreturnsFigureandAxesobjects. It is in fact just a thin wrapper around theplt.subplots()call, and passes along all arguments given to it toplt.subplots. The only additional thing it does is define some methods onAxesthat is returned (ax), so that it can, among other things, add pulses, delays, track their positions on the horizontal axis, etc. Theax.pulseandax.delaymethods "apply" pulses and delays with either strings, keyword arguments, or both. The methodpseqcan apply a sequence of these delays and pulses. In addition the wonderfulplt.subplot_mosaicfunction (available in matplotlib 3.3 and higher) is available aspplot.subplot_mosaic. You also get applot.show()call for convenience (It just calls plt.show()).If you instead want to manually add anAxestoFigureusingfig.add_subplot(...), you will first need to register the "PulseProgram" projection.>>>pplot.register_projection(pplot.PulseProgram)>>>fig=plt.figure()>>>ax=fig.add_subplot(111,projection="PulseProgram")Pulses>>>fig,ax=pplot.subplots()>>># the following statements are equivalent>>>ax.pulse(r"p1 pl1 f1")>>>ax.pulse(plen=1,power=1,channel=1)>>>ax.pseq(r"p1 pl1 f1")>>>ax.pseq(r"p=1 pl=1 f=1")This syntax is "inspired" by the syntax used on Bruker spectrometers. The above statement draws a rectangle (pulse) with length=1 (p1), height=1 (pl1) starting at x-position that is automatically determined and y-position=1 (f1). Optionally, you can declare all variables with "=" for clarity, or as keyword arguments. The following sections just used strings and theax.pseqmethod.Note: Always use raw strings (r"...") to avoid issues with escape characters.Pulse annotations>>>ax.pseq(r"p1 pl1 f1 ph1")>>>ax.pseq(r"p1 pl1 f1 ph_x")>>>ax.pseq(r"p1 pl1 f1 ph1 pdy=0.1 pdx=-0.2 pfs=10")>>>ax.pseq(r"p1 pl1 f1 tx=$\tau$")>>>ax.pseq(r"p1 pl1 f1 tx=CP tdx=0.1 tdy=-0.1 tfs=20")Theph1declaration (equivalent toph=1) adds a text $\phi_1$ at an automatically calculated location at the top of the pulse. If instead, you want to put in only a labelx, simply start the text to be put in with an underscore. If you are not happy with the exact location of this label, usepdxandpdyto move the label from its automatically calculated position, andpfsto change its font size. Similarly, a text can be specified bytx, and moved around usingtdxandtdyfrom its default position (which is somewhere in the center of the pulse itself), andtfsspecifies the font size. In matplotlib, all text inside$...$is interpreted as a LATEX block, so you can use it to add Greek letters and mathematical symbols.The default power for a pulse is 1 and the default channel is 0. These will be used if none are specified.Pulse length can be zero or negative. Ap0declaration just adds a vertical line, which can be useful to mark positions. A negative pulse aligns the pulse so that it ends at the current ax.time.Delays>>># The following statements are equivalent>>>ax.delay(r"d2 tx$\tau$ tdy0.4 f1")>>>ax.pseq(r"d2 tx$\tau$ tdy0.4 f1")A delay is just a pulse with nofacecolor, noedgecolor, and the default power level (1.0). You can annotate delays withphandtxare declarations, just like that with pulses. Delays can be negative. A negative delay will simply decreaseax.timeinstead of increasing it.Pulse sequence>>>pH90=r"p1 pl1 f1 fc=k">>>pN180=r"p2 pl1 f0">>>seq=fr"""# NOESY with pi pulse on 15N{pH90}ph1d2{pN180}# refocusd2{pH90}ph2d5{pH90}ph3p10 sp=fid phrec""">>>ax.pseq(seq)ax.pseqapplies a sequence of pulses/delays, each separated on a new line. Python's multi-line strings (r""" ...  """) can be used to construct these. You can predefine pulses that you want to reuse as strings, and usef-stringsto construct the pulse-sequence, which can then be fed toax.pseq. Comments can be indicated by a#. Anything appearing after a#on a line will be ignored.Simultaneous and centered pulses>>>ax.pseq(r"""p1 pl1 ph1 f1 wp1 pl1 ph2 f2 # aligned with the previous pulsep1 pl1 ph1 f1 c""")By default,ax.timemoves to the end of the pulse/delay after it is applied. Simply add awin the declaration (stands for "wait") to not do so. This is convenient when left aligning two pulses.Pulses start at the currentax.timeby default. Instead, if you want to center a pulse at the current value ofax.time, add the declarationc.Shaped pulses>>>ax.pseq(r"p1 pl1 f1 sp=fid")>>>ax.pulse(r"p1 pl1 ph1 f1",shape=lambdax:x**3)This would not be very useful library without the ability to add shaped pulses. Simply use thesp=declaration to give the shape. Currently, the library has these shapes:NameParametersExample/DefaultCommentfidfrequency, decayfid_10_5Free Induction Decayfid2frequency, decayfid2_10_5Noisy FIDgausscenter, sigmagauss_0.5_6Gaussian Pulseramppercentramp_40Linear Ramp Pulsetanpercent, curvaturetan_50_0.1Tangential Ramp Pulsegradrisetimegrad_8Gradientsine-sineSinusoid (0 to 2$\pi$)grad2gives backsinegrad2Sinusoid (0 to 2$\pi$)These shapes are customizable. For example,r"p1 pl1 sp=ramp_-30"gives a shape with a linear ramp from 100% to 70% of the specified power level. The parameters for the shapes are given above, and are set in the pulse sequence strings using underscores to separate them. The defaults and allowed parameters for each shape are given above.If you need a customized shape, you will need to define it as a function of a single variable (time), and pass the function to theshapeargument inax.pulse. Python'slambda functionscomes in very handy here, as shown in the example above, which puts in a 'cube' function as a shape. You can also pass a custom function or a numpy array directly to the pulse sequence string as shown below:>>>defmyshape(x):returnx**2>>>defmyshape_array():returnnp.linspace(0,1,100)>>>ps=r"""p1 pl1 fckd1p2 sp2d1p3 sp3""">>>ax.params={"sp2":myshape,"sp3":myshape_array}>>>ax.pseq(ps)All pulses are implemented as shapes using thePolygonpatch in matplotlib. Only the top part of the pulse is the actual shape and vertical lines are drawn by default to the channel so that this looks like a pulse. The vertical lines are not added iftroffis specified.  AllPolygonpatches, by default, are closed, i.e. the first and last points are joined. This behaviour is changed by the declarationo(stands for "open"). The casesp=fidandsp=fid2is a bit special, in thattroffandoare automatically specified.Colors, hatches, and transparency.There are several more declarations than can be passed.>>># The two statements below are equivalent>>>ax.pulse(r"p1 pl1 ph1 f1 fc=black ec=red al=0.5 h=///")>>>ax.pulse(r"p1 pl1 ph1 f1",facecolor="black",edgecolor="red",alpha=0.5,hatch="///",)AlignmentsIf you want your diagrams to look like they appear on an oscilloscope, set thecenter_alignattribute on yourAxesto True.See sourceAnimationsWe again piggy-back on Matplotlib'sArtistAnimationclass to make simple animated gifs or mp4 files. This requires that you collect all artists for each frame of your animation separately in a list, and pass this list of lists to the ArtistAnimation class. The simplest way to generate this is use f-strings to make a function to generate a string for your sequence (which is quite handy even if you don't want to make an animation). At each step, collect all the artists (inpulseplot, these artists are either patches or text objects, and then replot). Dont forget to resetax.timeeach time.defsequence(time):pulse=r"p1 fc=black"seq=fr"""{pulse}d{time}{pulse}d2 tx=$\tau${pulse}p15 pl1 sp=fid fc=none"""returnseqfig,ax=pplot.subplots(figsize=(7,1),dpi=300)ax.spacing=0.1parts=[]foriinrange(10):ax.time=0ax.pseq(sequence(i))ax.draw_channels(0)ifi==0:npatches,ntexts=len(ax.patches),len(ax.texts)parts.append(ax.patches[-npatches:]+ax.texts[-ntexts:])two_dim=pplot.animation(fig,parts,interval=1000,)two_dim.save("two_d.gif")The above example assumes that the number of patches and texts don't change in any of the frames. If this is not correct, you need to track the number of patches/text in each iteration, and only add the ones you want to.Bugs and requestsJust open an issue or a pull-request.