Extending EngineeringPaper.xyz with Python

Extending EngineeringPaper.xyz with Python

A live version of this calculation is available at EngineeringPaper.xyz.

Introduction

EngineeringPaper.xyz is designed to make make it easy to build, document, and share powerful calculations without ever needing to write a line of code. However, you may run into situations where you would like to utilize some of Python’s powerful computational libraries, such as NumPy, SciPy, or scikit-learn, to augment your calculations. Since EP already uses the SymPy Python library to perform your calculations, it is natural to extend its capabilities by writing your own Python functions. EP uses the Pyodide project to run Python in your browser, so you don’t need to install anything in order to make use of the Python libraries and all of the calculations run locally on your computer. You can click the info icon ⓘ at the top right of the code cells below to see a list of all of the available Python modules and their versions. Code cells allow you to define custom numerical functions that accept one or more scalar and/or matrix inputs and output a scalar or matrix result. Additionally, you can have your custom Python function output text, HTML, or SVG images to create custom visualizations based on the values in your EP sheet. This code cell tutorial will demonstrate the capabilities of EP code cells through several examples.

Code cells are created by clicking the Insert Code Cell button </> at the bottom of the sheet (or between cells). When you first create a code cell, it will have a basic Python function called _calculate _with one input argument and a return value. There will also be a code cell function definition expression at the top of the code cell that will default to $\mathrm{CodeFunc1}\left(\left\lbrack any\right\rbrack\right)=\left\lbrack none\right\rbrack$. The function definition tells EP how many inputs your function will have and how to handle the units for the inputs and outputs of your function. $\left\lbrack any\right\rbrack$ indicates that the function accepts any units for the input argument. If the input argument needs to have specific units, they can be specified directly in the function definition. For example, use $\mathrm{CodeFunc1}\left(\left\lbrack m\right\rbrack\right)=\left\lbrack none\right\rbrack$ to indicate that the input units need to be a distance. If the input or output arguments must have no units, $\left\lbrack none\right\rbrack$ is used. If multiple inputs are required for a code function, their unit specifications are separated by commas as in $\mathrm{CodeFunc1}\left(\left\lbrack m\right\rbrack, \left\lbrack s\right\rbrack\right)=\left\lbrack m\cdot s\right\rbrack$, which defines a function where the first argument is distance, the second argument is time, and the result is distance times time.

You may notice in the code cells below that there is a check box labeled Use SymPy Mode. The default is to have this box unchecked, in this case the inputs to your function will be floats for scalar values and 2D NumPy arrays for matrix or vector values. Your function will return a number (int or float) for a scalar result or a NumPy array, a list, or a nested list for a vector or matrix result. In SymPy mode, your function will operate on SymPy expressions directly, as will be discussed in the examples below. For matrix or vector inputs and/or outputs, the units specified in the function definition will be applied to all elements of the matrix. You can also specify the units specification as a matrix to allow different values in the matrix to have different units as in $\mathrm{CodeFunc1}\left(\left\lbrack none\right\rbrack\right)=\begin{bmatrix}\left\lbrack m\right\rbrack & \left\lbrack s\right\rbrack\ \left\lbrack kg\right\rbrack & \left\lbrack none\right\rbrack\end{bmatrix}$. Instead of individually specifying the units of each element of the array, the units can be specified on a per row or per column basis. This example specifies the units for each column of the result matrix where the result matrix can have any number of rows: $\mathrm{CodeFunc1}\left(\left\lbrack none\right\rbrack\right)=\begin{bmatrix}\left\lbrack m\right\rbrack & \left\lbrack s\right\rbrack & \left\lbrack none\right\rbrack\end{bmatrix}$. This is useful when the number of rows or columns in the result is not known in advance or for cases with a large number of rows or columns.

These concepts, and more, will be illustrated through a series of examples. Each of the examples aims to use code cells to expand the capabilities of EP beyond what is currently possible to illustrate the types of situations in which you would consider extending EP with Python.

Example 1: Scalar Input and Output

In the first example, we implement the Gamma function, which is not included as a built-in function in EP. Python provides a Gamma function in the standard library through the math module. The first thing we do in our script is import the math module so that it can be used in our calculate function. For this case, the calculate function is about as simple as it can get, it just outputs the result of the Gamma function applied to the input value. See the code cell below for the implementation. Note that in the function definition, we can give the function any valid variable name. In this case, we name the function with the Greek letter for Gamma, $\Gamma$. Note that the name of the actual Python function is always _calculate _since this is the function that EP is looking for within each code cell. Below the code cell, we see the result of calling the Gamma function for an input of 10.

import math

def calculate(value):
    return math.gamma(value)

$$ \mathrm{\Gamma}\left(10\right)= 3.6288\times 10^{5} $$

Example 2: Scalar Input and Output in SymPy Mode

Whenever SymPy provides a function or capability, such as the Gamma function, it’s usually best to use SymPy’s implementation of that function or capability and to define our code cell function in SymPy mode. When using SymPy Mode, the values passed into our function are maintained as SymPy expressions or matrices and we return SymPy expressions or matrices as results. Internally, EP effectively uses SymPy mode for everything. This has many benefits including being able to work with symbolic expressions in addition to numeric expressions. Additionally, SymPy uses arbitrary precision math when converting expressions into numbers, so more precision is maintained in calculations performed in SymPy mode. The following code cell reimplements the Gamma function in SymPy mode using SymPy’s built in Gamma function implementation:

from sympy import gamma

def calculate(value):
    return gamma(value)

$$ \mathrm{\Gamma^{\prime}}\left(10\right)= 3.6288\times 10^{5} $$

One benefit of this approach is that the Gamma function is now able to handle larger input values without causing an error. For example, the following Gamma function call will cause an error if used with the non-SymPy version:

$$ \mathrm{\Gamma^{\prime}}\left(10000\right)= 2.84625968091705\times 10^{35655} $$

In addition to implementing the SymPy versions of functions, Sympy Mode also opens up SymPy’s powerful tools for symbolic expression manipulation. For example, polynomial factoring can be performed with the code cell function below. Note that the Automatically Simplify Symbolic Expressions sheet setting needs to be disabled for such manipulations to not be lost by EP’s default simplification strategy. Additionally, note that we need to import SymPy in every code cell where SymPy is needed since each code cell runs in its own isolated namespace. However, because of how Python imports modules, there is no performance impact to this since Python only ever imports a module once and keeps a reference to it for any future imports of the same module.

import sympy

def calculate(value):
    return sympy.factor(value)

$$ x^2+5\cdot x+6= x^{2} + 5 \cdot x + 6 $$

$$ \mathrm{factor}\left(x^2+5\cdot x+6\right)= \left(x + 2\right) \cdot \left(x + 3\right) $$

Example 3: Inputs and Output with Units

Now let’s explore the case where our inputs and outputs have units. Another function that is not included in EP by default is atan2, which takes two inputs and returns an angle (note that EP currently provides $angle\left(x+y\cdot i\right)$ which is equivalent to $atan2\left(y,x\right)$). If we would like to provide $x$ and $y$ in length units, we define the code cell function below. Note that since there are two inputs, two units specifications need to be provided to the function, both as $\left\lbrack m\right\rbrack$. The output units are set to $\left\lbrack rad\right\rbrack$ to force the results to have angle units. Not that, in general, it’s best to use SI units for inputs and outputs for code cell functions since the user’s values will be converted to whatever units are specified in the code cell function definition and the output will be converted from whatever units are specified in the function definition ($\left\lbrack rad\right\rbrack$ in this case) to the user’s units. In general, it’s best to do your calculations directly in SI units and let EP convert the user’s values to SI for the calculation and to convert the result back to the user’s units after the calculation. Since SymPy has a built-in atan2 implementation, this code cell is also be defined in SymPy mode.

import sympy

def calculate(y, x):
    return sympy.atan2(y, x)

$$ \mathrm{atan2}\left(\frac{\sqrt3}{2}\cdot1\left\lbrack m\right\rbrack,\frac12\cdot1\left\lbrack m\right\rbrack\right)=\left\lbrack deg\right\rbrack =60 \left\lbrack deg\right\rbrack $$

Example 4: Defining a Custom Units Function

One obvious limitation of the atan2 implementation above is that it it only allows distance units for the $x$ and $y$ inputs. However, it should work for any set of consistent units, or inputs without any units at all. We could specify both of the input units as $\left\lbrack any\right\rbrack$, and it will work with either distance units, or no units for the inputs. However, one limitation of this approach is that it would also work if $x$ has distance units and $y$ has time units. Cases like this, where more than one set of units needs to be handled by the same code cell function, can be handled by defining a custom_dims function that takes as many input arguments as the _calculate _function and returns the result dimensions. Using a custom_dims function requires that the unit specification of all of the inputs and outputs be set to $\left\lbrack any\right\rbrack$. Note that two utility functions are provided to the code cell environment, ensure_all_equivalent and ensure_all_unitless. Both of these functions take any number of inputs and return the units of the first argument if the unit check passes, either all equivalent or all unitless. If the unit check fails, a TypeError exception is raised, which a signal to the EP units system that there is a dimension error for the calculation. Additionally, note that the custom dimensions function makes use of the dimension_definitions object from sympy.physics.units.definitions to access the angle dimension. dimension_definitions also includes the mass, length, time, current, temperature, luminous_intensity, amount_of_substance, angle, and information dimensions which can be multiplied, divided, and raised to powers to construct whatever dimensions are needed for the output of your custom_dims function.

from sympy.physics.units.definitions import dimension_definitions
import sympy

def calculate(y, x):
    return sympy.atan2(y, x)

def custom_dims(y_dims, x_dims):
    ensure_all_equivalent(y_dims, x_dims)
    return dimension_definitions.angle

$$ \mathrm{atan2^{\prime}}\left(\frac{\sqrt3}{2},\frac12\right)=\left\lbrack deg\right\rbrack =60 \left\lbrack deg\right\rbrack $$

The following example implements a product function that works for any units by defining a custom_dims function that multiplies the dimensions of all of the input arguments. This example also shows how to handle the case of a variable number of inputs to code cell functions by placing a asterisk in front of the input parameter name, which represents a variable number of inputs as a list. This is the standard syntax for a variable number of input arguments in Python.

import sympy as sp

def calculate(*values):
    return sp.Mul(*values)

def custom_dims(*dims):
    return sp.Mul(*dims)

$$ \mathrm{prod}\left(1\left\lbrack s\right\rbrack,:2\left\lbrack m\right\rbrack,:3\left\lbrack kg\right\rbrack\right)= 6 \left\lbrack kg\cdot m\cdot s\right\rbrack $$

Occasionally, the actual numerical values of the inputs and/or outputs to the calculate function are needed by the custom_dims function. To facilitate this, the custom_dims function can accept a final input argument called dim_values, which is a dictionary with two entries. dim_values[“args”] is a list containing the input argument numerical values and dim_values[“result”] is the numerical result value. This is an advanced feature and is not needed in most situations.

Example 5: Accepting and Returning Vectors and Matrices

Python code cell functions are designed to transparently handle passing in and/or returning vectors and matrices. When not in SymPy Mode, matrices are represented as 2D NumPy arrays. When in SymPy mode, SymPy Matrix objects are used to represent vectors and matrices.

Let’s first look at an example of a code cell function that returns a matrix. The following function creates a random matrix without using SymPy mode. Note that when calling NumPy’s rand function, the number of rows and columns need to be integer values. When not in SymPy mode, all of the input arguments will be float or NumPy float arrays. Python’s int function is used to convert the rows and cols values to integers as required by NumPy’s rand function.

import numpy as np

def calculate(rows, cols):
    return np.random.rand(int(rows), int(cols))

$$ \mathrm{rand}\left(2,3\right)= \begin{bmatrix} 0.949597451656205 & 0.910302904864407 & 0.15221420625253 \ 0.279087293386282 & 0.835501623321928 & 0.744244383789574 \end{bmatrix} $$

A similar function can be made using SymPy mode. In SymPy mode, the input arguments are not converted to float so the int function does not need to be used:

import sympy as sp

def calculate(rows, cols):
    return sp.randMatrix(rows, cols)

$$ \mathrm{rand^{\prime}}\left(2,3\right)= \begin{bmatrix} 85 & 96 & 21 \ 52 & 61 & 85 \end{bmatrix} $$

Code cell functions can also accept one or more matrices as inputs. For example, let’s create a function that returns the mean, median, and mode of an input vector. Note that if a list or tuple is returned, it will be automatically converted into a column vector. Additionally, note the use of Python’s _print _function within the _calculate _function. The printed values are displayed below code cell and can be useful when debugging a code cell.

import statistics

def calculate(vector):
    print(vector)
    mean = statistics.mean(*vector)
    median = statistics.median(*vector)
    mode = statistics.mode(*vector)

    return mean, median, mode

$$ \mathrm{stats}\left(\begin{bmatrix}1 & 1 & 2 & 5 & 6 & 9\end{bmatrix}\right)= \begin{bmatrix} 4 \ 3.5 \ 1 \end{bmatrix} $$

Example 6: Using Code Cell Functions to Display Custom Text and Graphics

A powerful feature of code cells is the ability to display results as plain text, HTML, or markdown. HTML is a very powerful option since it can include text formatting, such as colors, and images and graphics, such as SVG vector images. To make use of this functionality, you need to tell EP what format your result is in by defining the output as $\left\lbrack text\right\rbrack$, $\left\lbrack html\right\rbrack$, or $\left\lbrack markdown\right\rbrack$ and your calculate function needs to return a string the defines the desired output. The following example uses the $\left\lbrack text\right\rbrack$ output type:

def calculate(value, threshold):
    if value > threshold:
        return f"\n🚫Fail: Stress of {value} MPa exceeds the allowable stress of {threshold} MPa"
    else:
        return f"\n✅Pass: Stress of {value} MPa is below the allowable stress of {threshold} MPa"

✅Pass: Stress of 10.0 MPa is below the allowable stress of 20.0 MPa

The following example uses the $\left\lbrack html\right\rbrack$ output type which enables the definition of more expressive rendering results and also allows the inclusion of figures and images. Note that, for security reasons, the HTML content is sanitized before rendering using the DOMPurify library which strips out any script or style tags, amongst other things, to prevent the rendered HTML from impacting the rest of the page.

def calculate(value, threshold):
    fail_text_style = '"color: red; font-weight: bold;"'
    fail_div_style ='"border: 3px solid red; border-radius: 5px; width: fit-content; padding: 10px"'
    pass_text_style = '"color: green; font-weight: bold;"'
    pass_div_style ='"border: 3px solid green; border-radius: 5px; width: fit-content; padding: 10px"'
    if value > threshold:
        return f"""
<h4>Result Summary</h4>
<div style={fail_div_style}>
  🚫<span style={fail_text_style}>Fail:</span> Stress of 
  <span style={fail_text_style}>{value} MPa</span> exceeds allowable stress of {threshold} MPa
</div>
"""
    else:
        return f"""
<h4>Result Summary</h4>
<div style={pass_div_style}>
  ✅<span style={pass_text_style}>Pass:</span> Stress of 
  <span style={pass_text_style}>{value} MPa</span> is below the allowable stress of {threshold} MPa
</div>
"""

Result Summary

🚫Fail: Stress of 30.0 MPa exceeds allowable stress of 20.0 MPa

Result Summary

Pass: Stress of 15.0 MPa is below the allowable stress of 20.0 MPa

The ability to render to HTML opens up many possibilities, including the ability to include images and graphics. In particular, the ability to include SVG vector images can be used to create figures that are driven by values in the sheet. The following example uses the drawsvg library to draw the work envelope for a SCARA robot based on its arm lengths and joint angle ranges:

import drawsvg as dw
import numpy as np
from numpy import pi

def calculate(lengths, min_angles, max_angles):
    l1, l2 = lengths[:,0]
    theta1_min, theta2_min = min_angles[:,0]
    theta1_max, theta2_max = max_angles[:,0]
    
    d = dw.Drawing(2.2*(l1+l2), 2.2*(l1+l2), origin='center')

    # assumes symmetric range for outer arm and that outer arm is shorter than the inner arm
    inner_radius = np.sqrt(l1**2+l2**2-2*l1*l2*np.cos((180-theta2_max)*pi/180))

    alpha = np.asin((l2*np.sin((180-theta2_max)*pi/180))/inner_radius)*180/pi + \
            theta1_max
    
    path = dw.Path(stroke='royalblue', fill='lightblue', fill_rule='nonzero')
    
    path.arc(0, 0, l1+l2, theta1_min, theta1_max)
    
    path.arc(l1*np.cos(theta1_min*pi/180), -l1*np.sin(theta1_min*pi/180),
             l2, theta1_max, theta1_max+theta2_max, include_m=False)
    
    path.arc(0, 0, inner_radius, alpha, -alpha, cw=False, include_m=False)

    path.arc(l1*np.cos(theta1_min*pi/180), l1*np.sin(theta1_min*pi/180),
         l2, theta1_min+theta2_min, theta1_min, include_m=False)
    
    group = dw.Group(transform='scale(1,-1)')
    group.append(path)
    
    group.append(dw.Rectangle(-.2*l1,-.2*l1, l1+.4*l1,l1*.4, rx=.2*l1, ry=.2*l1, fill='grey', stroke='black'))
    group.append(dw.Circle(0, 0,.05*l1))

    group.append(dw.Rectangle(-.1*l1+l1,-.1*l1, l2+.3*l2, l1*.2, rx=.1*l1, ry=.1*l1, fill='grey', stroke='black'))
    
    group.append(dw.Circle(l1, 0,.05*l1))
    group.append(dw.Circle(l1+l2, 0,.05*l1))

    group1 = dw.Group(transform=f'rotate({theta1_max})', opacity=0.3)

    group1.append(dw.Rectangle(-.2*l1,-.2*l1, l1+.4*l1,l1*.4, rx=.2*l1, ry=.2*l1, fill='grey', stroke='black'))
    group1.append(dw.Circle(0, 0,.05*l1))

    group1.append(dw.Rectangle(-.1*l1+l1,-.1*l1, l2+.3*l2, l1*.2, rx=.1*l1, ry=.1*l1, fill='grey', stroke='black'))
    
    group1.append(dw.Circle(l1, 0,.05*l1))
    group1.append(dw.Circle(l1+l2, 0,.05*l1))

    group2 = dw.Group(transform=f'rotate({theta2_max}, {l1}, 0)')
    group2.append(dw.Rectangle(-.1*l1+l1,-.1*l1, l2+.3*l2, l1*.2, rx=.1*l1, ry=.1*l1, fill='grey', stroke='black'))
    group2.append(dw.Circle(l1, 0,.05*l1))
    group2.append(dw.Circle(l1+l2, 0,.05*l1))
    
    group1.append(group2)
    
    group.append(group1)
    
    d.append(group)
    
    return f"<h4>SCARA Robot Work Envelope</h4>{d.as_svg()}"

SCARA Robot Work Envelope

Example 7: Creating Matplotlib Plots

There are two ways to include a Matplotlib plots. Both cases require a few extra steps to convert the plot into HTML. The first example renders the plot as a PNG image. Both examples create a polar plot since EP doesn’t natively support polar plots. Note that it is required to set the Matplotlib rendering backend to “AGG” or “SVG” using the matplotlib.use() function call. Additionally, it’s important to call plot.close(fig) before your function exits to avoid excessive memory usage since Matplotlib doesn’t free up the memory for these plots automatically.

import matplotlib.pyplot as plt
import matplotlib
matplotlib.use("AGG") # must choose an image backend to avoid error

import numpy as np

import io
import base64

from typing import Tuple

def calculate(num_turns):
    r = np.arange(0, num_turns, num_turns/200)
    theta = 2 * np.pi * r
    
    # Create a figure
    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
    ax.plot(theta, r)
    ax.set_rmax(2)
    ax.set_rticks([0.5, 1, 1.5, 2])  # Less radial ticks
    ax.set_rlabel_position(-22.5)  # Move radial labels away from plotted line
    ax.grid(True)
    
    ax.set_title("A line plot on a polar axis", va='bottom')
    
    # Save to a BytesIO buffer
    buf = io.BytesIO()
    fig.savefig(buf, format='png')
    buf.seek(0)
    
    # Encode to base64 (required to embed into HTML)
    img_base64 = base64.b64encode(buf.read()).decode('utf-8')
    
    # Embed in HTML
    html_img = f'<img src="data:image/png;base64,{img_base64}"/>'

    # Important to call plt.close to avoid a memory leak
    plt.close(fig)
    
    return html_img

Finally, Matplotlib figures can be rendered as SVG images as well:

import matplotlib.pyplot as plt
import matplotlib
matplotlib.use("AGG") # must choose an image backend to avoid error

import numpy as np

import io
import base64

from typing import Tuple

def calculate(num_turns):
    r = np.arange(0, num_turns, num_turns/200)
    theta = 2 * np.pi * r
    
    # Create a figure
    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
    ax.plot(theta, r)
    ax.set_rmax(2)
    ax.set_rticks([0.5, 1, 1.5, 2])  # Less radial ticks
    ax.set_rlabel_position(-22.5)  # Move radial labels away from plotted line
    ax.grid(True)
    
    ax.set_title("A line plot on a polar axis", va='bottom')
    
    # Save to an in-memory string buffer as SVG
    svg_buffer = io.StringIO()
    fig.savefig(svg_buffer, format='svg')
    svg_string = svg_buffer.getvalue()
    svg_buffer.close()

    plt.close(fig)
    
    return svg_string
2025-07-02T15:59:04.965000
image/svg+xml

 
  Matplotlib v3.8.4, https://matplotlib.org/
 
  <g transform="translate(377.53475 177.287375) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-30" d="M 2034 4250 

Q 1547 4250 1301 3770 Q 1056 3291 1056 2328 Q 1056 1369 1301 889 Q 1547 409 2034 409 Q 2525 409 2770 889 Q 3016 1369 3016 2328 Q 3016 3291 2770 3770 Q 2525 4250 2034 4250 z M 2034 4750 Q 2819 4750 3233 4129 Q 3647 3509 3647 2328 Q 3647 1150 3233 529 Q 2819 -91 2034 -91 Q 1250 -91 836 529 Q 422 1150 422 2328 Q 422 3509 836 4129 Q 1250 4750 2034 4750 z” transform=”scale(0.015625)”>

  <g transform="translate(331.281795 73.30308) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-34" d="M 2419 4116 

L 825 1625 L 2419 1625 L 2419 4116 z M 2253 4666 L 3047 4666 L 3047 1625 L 3713 1625 L 3713 1100 L 3047 1100 L 3047 0 L 2419 0 L 2419 1100 L 313 1100 L 313 1709 L 2253 4666 z” transform=”scale(0.015625)”>

  <g transform="translate(227.2975 30.231375) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-39" d="M 703 97 

L 703 672 Q 941 559 1184 500 Q 1428 441 1663 441 Q 2288 441 2617 861 Q 2947 1281 2994 2138 Q 2813 1869 2534 1725 Q 2256 1581 1919 1581 Q 1219 1581 811 2004 Q 403 2428 403 3163 Q 403 3881 828 4315 Q 1253 4750 1959 4750 Q 2769 4750 3195 4129 Q 3622 3509 3622 2328 Q 3622 1225 3098 567 Q 2575 -91 1691 -91 Q 1453 -91 1209 -44 Q 966 3 703 97 z M 1959 2075 Q 2384 2075 2632 2365 Q 2881 2656 2881 3163 Q 2881 3666 2632 3958 Q 2384 4250 1959 4250 Q 1534 4250 1286 3958 Q 1038 3666 1038 3163 Q 1038 2656 1286 2365 Q 1534 2075 1959 2075 z” transform=”scale(0.015625)”>

  <g transform="translate(120.131955 73.30308) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-31" d="M 794 531 

L 1825 531 L 1825 4091 L 703 3866 L 703 4441 L 1819 4666 L 2450 4666 L 2450 531 L 3481 531 L 3481 0 L 794 0 L 794 531 z” transform=”scale(0.015625)”>

  <g transform="translate(77.06025 177.287375) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-38" d="M 2034 2216 

Q 1584 2216 1326 1975 Q 1069 1734 1069 1313 Q 1069 891 1326 650 Q 1584 409 2034 409 Q 2484 409 2743 651 Q 3003 894 3003 1313 Q 3003 1734 2745 1975 Q 2488 2216 2034 2216 z M 1403 2484 Q 997 2584 770 2862 Q 544 3141 544 3541 Q 544 4100 942 4425 Q 1341 4750 2034 4750 Q 2731 4750 3128 4425 Q 3525 4100 3525 3541 Q 3525 3141 3298 2862 Q 3072 2584 2669 2484 Q 3125 2378 3379 2068 Q 3634 1759 3634 1313 Q 3634 634 3220 271 Q 2806 -91 2034 -91 Q 1263 -91 848 271 Q 434 634 434 1313 Q 434 1759 690 2068 Q 947 2378 1403 2484 z M 1172 3481 Q 1172 3119 1398 2916 Q 1625 2713 2034 2713 Q 2441 2713 2670 2916 Q 2900 3119 2900 3481 Q 2900 3844 2670 4047 Q 2441 4250 2034 4250 Q 1625 4250 1398 4047 Q 1172 3844 1172 3481 z” transform=”scale(0.015625)”>

  <g transform="translate(120.131955 281.27167) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-32" d="M 1228 531 

L 3431 531 L 3431 0 L 469 0 L 469 531 Q 828 903 1448 1529 Q 2069 2156 2228 2338 Q 2531 2678 2651 2914 Q 2772 3150 2772 3378 Q 2772 3750 2511 3984 Q 2250 4219 1831 4219 Q 1534 4219 1204 4116 Q 875 4013 500 3803 L 500 4441 Q 881 4594 1212 4672 Q 1544 4750 1819 4750 Q 2544 4750 2975 4387 Q 3406 4025 3406 3419 Q 3406 3131 3298 2873 Q 3191 2616 2906 2266 Q 2828 2175 2409 1742 Q 1991 1309 1228 531 z” transform=”scale(0.015625)”>

  <g transform="translate(224.11625 324.343375) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-37" d="M 525 4666 

L 3525 4666 L 3525 4397 L 1831 0 L 1172 0 L 2766 4134 L 525 4134 L 525 4666 z” transform=”scale(0.015625)”>

  <g transform="translate(328.100545 281.27167) scale(0.1 -0.1)">
   <use xlink:href="#DejaVuSans-33"></use>
   <use xlink:href="#DejaVuSans-31" x="63.623047"></use>
   <use xlink:href="#DejaVuSans-35" x="127.246094"></use>
   <use xlink:href="#DejaVuSans-b0" x="190.869141"></use>
  </g>
 </g>
</g>
  <g transform="translate(266.891929 185.177894) scale(0.1 -0.1)">
   <defs>
    <path id="DejaVuSans-2e" d="M 684 794 

L 1344 794 L 1344 0 L 684 0 L 684 794 z” transform=”scale(0.015625)”>

  <g transform="translate(297.623858 197.907476) scale(0.1 -0.1)">
   <use xlink:href="#DejaVuSans-31"></use>
   <use xlink:href="#DejaVuSans-2e" x="63.623047"></use>
   <use xlink:href="#DejaVuSans-30" x="95.410156"></use>
  </g>
 </g>
</g>
<g id="ytick_3">
 <g id="line2d_11">
  <path d="M 335.952 174.528 

C 335.952 161.423435 333.370686 148.446295 328.355786 136.339255 C 323.340886 124.232215 315.989927 113.230727 306.7236 103.9644 C 297.457273 94.698073 286.455785 87.347114 274.348745 82.332214 C 262.241705 77.317314 249.264565 74.736 236.16 74.736 C 223.055435 74.736 210.078295 77.317314 197.971255 82.332214 C 185.864215 87.347114 174.862727 94.698073 165.5964 103.9644 C 156.330073 113.230727 148.979114 124.232215 143.964214 136.339255 C 138.949314 148.446295 136.368 161.423435 136.368 174.528 C 136.368 187.632565 138.949314 200.609705 143.964214 212.716745 C 148.979114 224.823785 156.330073 235.825273 165.5964 245.0916 C 174.862727 254.357927 185.864215 261.708886 197.971255 266.723786 C 210.078295 271.738686 223.055435 274.32 236.16 274.32 C 249.264565 274.32 262.241705 271.738686 274.348745 266.723786 C 286.455785 261.708886 297.457273 254.357927 306.7236 245.0916 C 315.989927 235.825273 323.340886 224.823785 328.355786 212.716745 C 333.370686 200.609705 335.952 187.632565 335.952 174.528” clip-path=”url(#p89d372a357)” style=”fill: none; stroke: #b0b0b0; stroke-width: 0.8; stroke-linecap: square”>

  <g transform="translate(328.355786 210.637058) scale(0.1 -0.1)">
   <use xlink:href="#DejaVuSans-31"></use>
   <use xlink:href="#DejaVuSans-2e" x="63.623047"></use>
   <use xlink:href="#DejaVuSans-35" x="95.410156"></use>
  </g>
 </g>
</g>
<g id="ytick_4">
 <g id="line2d_12">
  <path d="M 369.216 174.528 

C 369.216 157.055246 365.774248 139.752393 359.087715 123.609673 C 352.401182 107.466954 342.599903 92.798303 330.2448 80.4432 C 317.889697 68.088097 303.221046 58.286818 287.078327 51.600285 C 270.935607 44.913752 253.632754 41.472 236.16 41.472 C 218.687246 41.472 201.384393 44.913752 185.241673 51.600285 C 169.098954 58.286818 154.430303 68.088097 142.0752 80.4432 C 129.720097 92.798303 119.918818 107.466954 113.232285 123.609673 C 106.545752 139.752393 103.104 157.055246 103.104 174.528 C 103.104 192.000754 106.545752 209.303607 113.232285 225.446327 C 119.918818 241.589046 129.720097 256.257697 142.0752 268.6128 C 154.430303 280.967903 169.098954 290.769182 185.241673 297.455715 C 201.384393 304.142248 218.687246 307.584 236.16 307.584 C 253.632754 307.584 270.935607 304.142248 287.078327 297.455715 C 303.221046 290.769182 317.889697 280.967903 330.2448 268.6128 C 342.599903 256.257697 352.401182 241.589046 359.087715 225.446327 C 365.774248 209.303607 369.216 192.000754 369.216 174.528” clip-path=”url(#p89d372a357)” style=”fill: none; stroke: #b0b0b0; stroke-width: 0.8; stroke-linecap: square”>

  <g transform="translate(359.087715 223.366639) scale(0.1 -0.1)">
   <use xlink:href="#DejaVuSans-32"></use>
   <use xlink:href="#DejaVuSans-2e" x="63.623047"></use>
   <use xlink:href="#DejaVuSans-30" x="95.410156"></use>
  </g>
 </g>
</g>
<g transform="translate(159.825 14.137313) scale(0.12 -0.12)">
 <defs>
  <path id="DejaVuSans-41" d="M 2188 4044 

L 1331 1722 L 3047 1722 L 2188 4044 z M 1831 4666 L 2547 4666 L 4325 0 L 3669 0 L 3244 1197 L 1141 1197 L 716 0 L 50 0 L 1831 4666 z” transform=”scale(0.015625)”>