pozitron57 / plotnik

Matplotlib-based library for drawing thermodynamical cycles and more

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

plotnik is a Python library designed for creating simple graphs using matplotlib in Cartesian coordinates, mirroring the style of 'school' graphs traditionally used in Russian physics and mathematics education. It was developed for convenient drawing of thermodynamic cycles, including nonlinear processes, without need to perform calculations.

The library is currently usable. The code is poorly designed. Full documentation is not available, but you can refer to the examples provided below to understand its functionality.

The library utilizes syntax inspired by the SchemDraw library.

The code has been mostly written by Chat-GPT.

Basic usage

The default font is 'STIX Two Text'. To switch to Computer Modern Roman, use d.set_config(font='serif'), noting that this requires LaTeX to be installed on your machine.

To draw the curves, use processes: class Process() with its subclasses:

  • Linear() Draw a straight line from .at() to .to().

  • Power() Connects initial and final points with the equation y=k*x^n + b

  • Adiabatic() Draw adiabatic process pV^gamma = const for p(V) coordinates. Set the gamma value using Adiabatic(gamma=7/5), with the default being 5/3.

  • Iso_t() Isothermal process pV=const in p(V) coordinates.

  • Bezier(x,y) Draw quadratic or cubic Bezier curve.

    d += Bezier(x=2,y=2).at(1,1).to(3,1)
    

    draws quadratic Bezier curve from (1,1) to (3,1) with a single control point at (2,2). Similarly,

    d += Bezier(x1=3,y1=7, x2=5,y2=3).at(1,5).to(7,5)
    

    this code plots a cubic Bezier curve, resembling a sine wave, with two control points at (x1, y1) and (x2, y2). Note that d += is usually optional.

Additionally, standard matplotlib syntax can be used to add text and lines to the plot, for example, d.ax.plot(x, y).

Examples

1. V=const, adiabatic, isothermal

This example illustrates well the purpose behind the creation of the library. It is necessary to draw a cycle in pV-coordinates, consisting of an isochore, adiabat, and isotherm. The goal was to free the user from the need to perform calculations and to provide a simple interface for constructing such graphs.

import plotnik
from plotnik.processes import *

v1 = 3
v2 = 9
v3 = v1

p1 = 9

with plotnik.Drawing() as d:
    d.set_config(
        xname='$V$',
        yname='$p$',
        zero_x=0.5,
        axes_arrow_width=0.23,
    )

    A1 = Adiabatic().at(v1,p1).to(v2, 'volume').arrow().dot()
    p2 = A1.end[1] # A1.end returns coordinates (x,y) for the last point of A1 process

    # Process T1 has no .at(), so it takes the last point from the previous
    # process A1 as an initial point
    T1 = Iso_t().to(v1, 'volume').arrow().dot().label(2, dy=0)
    p3 = T1.end[1]

    Linear().to(v1,p1).arrow().dot().label(3,1)

    d.show()
    # `crop=True` is only compatible with SVG files and requires the installation of `Inkscape` on your machine.
    # This feature removes paths named `patch_1` and `patch_2`, which, in my case, do not contain any paths
    # but add a whitespace margin.
    d.save('filename.svg', crop=True)

2. Linear() and grid()

import plotnik
from plotnik.processes import *

u1 = 2
u2 = 4

v1 = 3
v2 = 6

with plotnik.Drawing() as d:
    d.set_config(
        yname='$U$',
        xname='$V$',
        zero_x=0.4,
        ylim=[0,6],
        xlim=[0,8],
    )

    Linear().at(v1,u1).to(v2,u2).arrow().dot('both').label(1,2)
    d.grid(y_end=5)
    d.show()

3. Carnot cycle in PV coordinates. Adiabatic(), Iso_t().

import plotnik
from plotnik.processes import *

p1 = 10
v1 = 3
v2 = 6
v3 = 10

with plotnik.Drawing() as d:
    d.set_config(
        fontsize=30,
        yname='$p$',
        xname='$V$',
        aspect=0.7,
        xlim=[0,11],
        center=[5.5, 4.5],
    )

    # When the second argument in the .to() method is 'volume', the function draws a line up to
    # volume v2 and calculates the required pressure. To retrieve this pressure value, use end_p(process)
    T1 = Iso_t().at(v1, p1).to(v2, 'volume').dot('both').label(1,2)
    p2 = T1.end[1]

    A1 = Adiabatic().to(v3, 'volume')
    p3 = A1.end[1]

    # common_pv calculates the volume (v) and pressure (p) at the intersection of an isothermal process
    # passing through the start point and an adiabatic process passing through the end point.
    v4, p4 = common_pv(v1,p1, v3,p3)
    Iso_t().to(v4, 'volume').dot('both').label(3,4)

    Adiabatic().to(v1,'volume')
    Power(15).at(v2, p2).to(v4,p4)

    d.ax.text(4.75, 4.8, '$A_1$', fontsize=24)
    d.ax.text(5.65, 3.9, '$A_2$', fontsize=24)

    d.show()

4. Cubic Bezier curve with dots on it

import plotnik
from plotnik.processes import *

with plotnik.Drawing() as d:
    d.set_config(yname=r'$x$',
                 xname=r'$t$',
                 xlim=[0,12],
                 center_x=5,
                 )

    B = Bezier(x1=5,y1=15, x2=6.8,y2=-4).at(1,7).to(11,3).lw(2.4)

    # B.get_point(index) returns a tuple (x, y).
    # Use an asterisk to unpack this tuple into x and y.
    # The allowed index range is from 0 to 100. 
    State().at(*B.get_point( 4)).dot().label('A')
    State().at(*B.get_point(18)).dot().label('B', dx=0)
    State().at(*B.get_point(48)).dot().label('C')
    State().at(*B.get_point(91)).dot().label('D')

    d.show()

5. Power() to create shifted hyperbola y=k/x+b

import plotnik
from plotnik.processes import *

x1 = 3
y1 = 2

x2 = 2*x1
y2 = y1

x3 = x1
y3 = 3*y1

with plotnik.Drawing() as d:
    d.set_config(
        fontsize=31,
        yname='$p$',
        xname=r'$\rho$',
        xlim=[0,7.5],
        ylim=[0,7.5],
        axes_arrow_width=0.16,
        zero_x=0.4, # add zero as a xtick label shifted to x=-0.4
        )

    Linear().at(x1, y1).to(x2,y2).arrow().dot().tox().label(1,2, dy=-0.65)
    Power(power=-0.5).at(x2, y2).to(x3, y3).arrow().label('',3)
    Linear().to(x1,y1).arrow().dot('both').toy()

    d.ax.set_yticks([y1, y3], ['$p_0$', '$3p_0$'])
    d.ax.set_xticks([x1, x2], [r'$\rho_0$', r'$2\rho_0$'])

    d.show()

6. Two Adiabatic() & two Linear()

import plotnik
from plotnik.processes import *

v1  = 2
v2  = 5
p12 = 8
p34 = 3

with plotnik.Drawing() as d:
    d.set_config(
         yname='$p$',
         xname='$V$',
         zero_x=0.5,
         fontsize=28,
         ylim=[0,10.7],
         axes_arrow_scale=0.7,
         center_x=4,
     )

    a=22
    Linear().at(v1,p12).to(v2,p12).dot('both').arrow(size=a,pos=0.61).label(1,2)
    Q1 = Adiabatic().at(v1,p12).to(p34, 'pressure').arrow(size=a,reverse=True)
    Q2 = Adiabatic().at(v2,p12).to(p34, 'pressure').arrow(size=a)
    v3 = Q1.end[0]
    v4 = Q2.end[0]
    Linear().at(v4,p34).to(v3,p34).dot('both').arrow(size=a).label(3,4, dy=-0.8)

    d.show()

7. Bezier().connect() method

This method is used to create a smooth curve that must pass through the specified point, in this case, (3,60).

import plotnik
from plotnik.processes import *

with plotnik.Drawing() as d:
    d.set_config(
        aspect=1/20,
        yname=r'$\alpha, \%$',
        yname_y=103,
        xname=r'$T,10^3 \rm{К}$',
        xlim=[0,6],
        ylim=[0,106],
        zero_ofst=[0.2, 11.8]
    )

    d += (P1:= Power(2).at(0,0).to(3,60).lw(3) )
    d += (L1:= Linear().at(5,90).to(6,90).lw(0) ) # This process is used solely to complete the Bezier curve with a tangent, hence 'lw=0' is specified.
    Bezier().connect(P1,L1).lw(3)

    d.ax.set_xticks([2,4])
    d.ax.set_yticks([40,80])
        
    d.grid(step_x=0.5, step_y=10, x_end=5, y_end=90)

    d.show()

8. Bezier().get_coordinates()

When plotting a complex curve as two separate processes (thus requiring two calls to ax.plot()), using a large linewidth may result in poor connections between the segments. To resolve this, you can use Bezier() to calculate the coordinates without plotting them. Then, append these coordinates to the other process. Matplotlib will seamlessly join these segments when plotting them in a single ax.plot() call.

import plotnik
from plotnik.processes import *

with plotnik.Drawing() as d:
    d.set_config(
        yname=r'$V_{\rm погр},\rm{см}^3$',
        xname=r'$\rho,\rm{г}/\rm{см}^3$',
        ylim=[0,12],
        xlim=[0,4.8],
        aspect=1/4,
        fontsize=18,
        axes_arrow_width=0.2,
        )

    d += (B1:= Bezier(x=1.8,y=2.8).at(1, 10).to(4, 2.5).lw(0) )
    x,y = B1.get_coordinates()
    # Append straight line to x,y
    x = np.append([0,1],x)
    y = np.append([10,10],y)

    d.ax.plot(x, y, lw=2.5, color='k')

    d.ax.tick_params(length=0)
    d.ax.set_yticks(np.arange(1,11,1))
    d.ax.set_xticks(np.arange(0.5,4.5,0.5),
                    ['0,5','1,0','1,5','2,0','2,5','3,0','3,5','4,0'])
    d.grid(step_x=0.25, step_y=1, y_end=10, x_end=4)

    d.show()

9. Power()

import plotnik
from plotnik.processes import *

v1 = 8
u1 = 6
v2 = 3.5

with plotnik.Drawing() as d:
    d.set_config(
        fontsize=31,
        yname='$U$', 
        xname='$V$',
        ylim=[0,7.4],
        axes_arrow_length=1.1,
        center=[10,0],
    )

    P1 = Power().at(v1, u1).to(v2, 'x').arrow().label(1,2).dot('both').tox().toy()
    y2 = P1.end[1]

    Power().to(0, 0).ls('--')

    d.ax.set_xticks([v1, v2], ['$V_1$', '$V_2$'])
    d.ax.set_yticks([u1, y2], ['$U_1$', '$U_2$'])

    d.show()

10. Arrows and labels positioning

import plotnik
from plotnik.processes import *

v1 = 2
v2 = v1
v3 = 6
v4 = v3

t1 = 4
t2 = 2
t3 = 6
t4 = 8

with plotnik.Drawing() as d:
    d.set_config(
        yname='$V$',
        lw=3.2,
        xname='$T$',
        ylim=[0,7],
        xlim=[0,10],
        zero_x=0.5,
        axes_arrow_scale=1.5,
    )

    Linear().at(t1, v1).to(t2, v2).arrow().dot('both').tox()\
       .label(1,2, start_ofst=[0.5,0.2], end_ofst=[-0.8, 0.2])
    Linear().to(t3, v3).arrow().tozero('start')
    Linear().to(t4, v4).arrow(pos=0.7).tox().dot('both')\
       .label(3,4, start_dx=-0.5)

    d.ax.set_xticks([2,4,6,8], ['$T_0$','$2T_0$','$3T_0$','$4T_0$'])

    d.show()

11. Customize grid()

import plotnik
from plotnik.processes import *

B = [0, 0.2, 0]
t = [0, 2,   4]

with plotnik.Drawing() as d:
    d.set_config(yname='$B,$Тл', xname='$t,$с',
                 xlim=[0,6.3],
                 xname_x=5,
                 yname_y=0.26,
                 ylim=[0,0.27],
                 zero_x=0.3,
                 axes_arrow_scale=1.5,
                 aspect=20,
                 )

    d.ax.plot(t,B,'k-', lw=2.5)

    d.ax.set_yticks([0.1,0.2], ['0,1','0,2'])
    d.ax.set_xticks([1,2,3,4])

    d.grid(step_x=1, step_y=0.05, x_end=4.2, y_end=0.21, lw=2, color='#333333')

    d.show()

12. Tangent red isotherm

import plotnik
from plotnik.processes import *

p1=3
v1=1
p2=1
v2=4
a = (p1-p2) / (v1-v2)
b = p1 - a*v1
vm = -b/(2*a)
pm = a*vm + b

with plotnik.Drawing() as d:
    d.set_config(
        fontsize=24,
        yname='$p$',
        xname='$V$',
        xlim=[0,5],
        ylim=[0,4],
        zero_ofst=[0.2, 0.38],
    )

    Linear().at(v1,p1).to(v2,p2).arrow(pos=0.3).dot('both').label(1,2).toy().tox()
    State().at(vm,pm).dot().tox().toy()
    Iso_t().at(vm,pm).to(v1*1.35,'volume').lw(1.4).col('#EE3344')
    Iso_t().at(vm,pm).to(v2*1.16,'volume').lw(1.4).col('#EE3344')

    d.ax.set_yticks([p1, p2, pm], ['$p_1$', '$p_2$', r'$p_\text{м}$'])
    d.ax.set_xticks([v1, v2, vm], ['$V_1$', '$V_2$', r'$V\!_\text{м}$'])

    d.grid(step=.5, y_end=3.5, x_end=4.5, color='#dddddd')

    d.show()

Some options

Consider the followig syntax:

Linear().at().to().arrow().dot().label().toy().tox().tozero().col().lw().ls().zord().

.at(x1,y1): set starting point. Uses previous process last point if not set.

.to(x2,y2): set end point.

.arrow(size=None, pos=0.54, color='black', reverse=False, filled=True, zorder=3, head_length=0.6, head_width=0.2)

  • pos sets position of the arrow on the line (from 0 to 1).
  • reverse=True rotates the arrow on 180 degrees.
  • filled=False doesn't look well but produces not filled arrow.

.dot(pos='end', size=8, color='black', zorder=5, marker='o')

  • .dot() or .dot('end') or .dot(pos='end') adds only last point;
  • .dot('start') adds only start point;
  • .dot('both') adds two points.
  • .dot(size=25)
  • marker are standart matplotlib markers, see full list
  • zorder can change the order it appears relative to other elements (useful to plot marker above or below grid or process etc.).

.label() add 1 or two labels.

.tox(), .toy(), .tozero() draw lines to, correspondingly, horizontal axis, vertical axis and zero. Default linestyle is dashed line, can be changed like .tox(ls='-'). By default, draw lines both for start and end of the process. Can be changed like .tox('end') or .tox('start').

.col('red') set color for the line.

.lw(1) set linewidth for the process.

.ls('--') set linestyle for the process.

.zord(5) set zorder for the process.

TODO

  • Repair examples 7 and 8 so d += won't be required.

  • Revise the arrow positioning logic to ensure they are accurately centered.

  • In the set_config() function, add the capability to globally modify arrowsize, dotsize, and lw (line width) for processes.

    Introduce options in the set_config() to globally adjust the size of arrows, dots, and line width for processes. For instance, include settings like dots_all=True, dots_size=10 and arrows_all=True, arrows_size=23.

  • Integrate the feature to select different coordinates. For instance, if all processes are initially plotted in x, y coordinates, there should be an option to view them in transformed coordinates like 1/x, y^2. Example syntax could be: d.transform_coordinates(newx = 1/x, newy = y**2).

  • Address the issue where d.save() generates erroneous results when used without a prior call to d.show(), ensuring reliable save functionality.

  • .xtick() and d.add_xticks() use different codes for tick positioning.

  • make .xtick() use matplotlib ax.set_xticks() method

  • Improve the algorithm for automatic determination of positions and sizes for labels, ticks, and arrows.

  • When need Bezier() only to calculate coordinates, you have to add it to Drawing() like so:

    d += (B1:= Bezier(x=1.8,y=2.8).at(1, 10).to(4, 2.5).lw(0) )
    x,y = B1.get_coordinates()
    

    Rewrite the code so one can use

    B1 = Bezier(x=1.8,y=2.8).at(1, 10).to(4, 2.5).hide()
    x,y = B1.get_coordinates()
    

    without adding it an actual figure.

About

Matplotlib-based library for drawing thermodynamical cycles and more

License:BSD 3-Clause "New" or "Revised" License


Languages

Language:Python 97.4%Language:Shell 2.6%