Python: HandCalcs#

HandCalcs renders calculations from any Python program. Create it with a renderer, then call it with source code.

from handcalcs import HandCalcs
from handcalcs.renderers import MarkdownRenderer

hc = HandCalcs(renderer=MarkdownRenderer())
markdown = hc("""
## Beam moment
w = 5  # kN/m
L = 6  # m
M = w * L**2 / 8
""")

The return value is whatever the renderer produces: a string for the Markdown, LaTeX, HTML and ASCII renderers, a document for the Word renderer, a workbook for the Excel renderer, a nested list for the base renderer. Pass destination= to write a file instead. One HandCalcs object can be called any number of times.

Creating it#

HandCalcs(renderer=None)

renderer is a renderer instance. If it is omitted, the renderer named by the default_renderer option is used (the ASCII renderer unless you change it).

The global configuration is copied when the object is created, so set options first.

Calling it#

hc(source_or_path, supplied_globals=None, supplied_locals=None, *, inputs=None, strict=True, allow_computed=False, destination=None)

source_or_path

Either a string of Python source, or a pathlib.Path to a .py file.

Important

A plain string is always treated as source code, even if it looks like a file name. Wrap paths in pathlib.Path.

supplied_globals

Where values come from.

  • If omitted, the source is run (with exec, in a fresh namespace) and the resulting variables are used.

  • If given, the source is not run: values are looked up in this dictionary. Use this when you have already run the code, or want to render code with values from somewhere else.

import pathlib

# Run a file and render it
latex = HandCalcs(LatexRenderer())(pathlib.Path("beam.py"))

# Render with values you already have (the code is not run)
values = {"w": 5, "L": 6, "M": 22.5}
text = HandCalcs(ASCIIRenderer())("M = w * L**2 / 8", values)

If a name has no value in supplied_globals, it is shown as empty in the substituted step. The exception is a name used in an if condition: the condition is evaluated to decide which branch to show, so a missing name raises NameError.

inputs, strict, allow_computed

Render the source with different given values. See Rendering many input sets. inputs cannot be combined with supplied_globals.

destination

A file path (a string or a pathlib.Path). If given, the render is written to that file and its Path is returned instead of the render. See Writing a file.

Writing a file#

Give a destination and handcalcs writes a file that is ready to open:

import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import LatexRenderer, HTMLRenderer, WordRenderer

calc = pathlib.Path("calcs/beam.py")
HandCalcs(LatexRenderer())(calc, destination="report/beam.tex")
HandCalcs(HTMLRenderer())(calc, destination="report/beam.html")
HandCalcs(WordRenderer())(calc, destination="report/beam.docx")

Renderer

The file

LaTeX

A complete document: \documentclass{article}, the packages the output needs (amsmath, amssymb, xcolor), then the calculation between \begin{document} and \end{document}. It compiles with pdflatex as it is.

HTML

A complete page: <!DOCTYPE html>, a <head> with the character set and a title, and the calculation in the <body>.

Markdown, ASCII

The output as it is: neither format needs anything around it.

Word, Excel

The .docx or .xlsx file, saved.

Text is written as UTF-8. The parent folder must already exist, and an existing file is replaced.

Without a destination, the LaTeX and HTML renderers return a fragment, ready to \input into your own document or insert into your own page. Use that when the calculation is one part of a larger document.

The text before and after the output comes from the renderer’s preamble and postamble attributes. Set them on a renderer to use your own document class or page template:

renderer = LatexRenderer()
renderer.preamble = (
    "\\documentclass{report}\n"
    "\\usepackage{amsmath,amssymb,xcolor}\n"
    "\\begin{document}\n"
)
HandCalcs(renderer)(calc, destination="report/beam.tex")

The renderer’s to_file() method does the writing; see Overridable methods to change it in your own renderer.

Inspecting the parsed tree#

After a call, hc.hc_ast holds the parsed calculation (an HcSequence). This is mostly useful when building a renderer.

Rendering many input sets#

Write a calculation once, as an ordinary script with sample values, then render it with different inputs, one document per input set:

import json
import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import LatexRenderer

hc = HandCalcs(LatexRenderer())
for case in json.loads(pathlib.Path("beams.json").read_text()):  # [{"w": 6.2, "L": 8}, ...]
    doc = hc(pathlib.Path("beam.py"), inputs=case)

In a notebook, %%render inputs=case does the same for a cell. A @handcalc function needs nothing extra: its parameters are already its inputs.

beam.py stays a normal script that runs and renders on its own:

import forallpeople as si
si.environment("structural")
from forallpeople import *

w = 5 * kN/m  # load
L = 6 * m
M = w * L**2 / 8

With inputs={"w": 7.5, "L": 8}, the first assignment of each input name is given the input value and the script is then run and rendered as usual, so everything computed from the inputs is recomputed:

w = 7.5 * kN / m = 7.5 * 1 kN / 1 m = 7.5 kN/m (load)
L = 8 * m = 8 * 1 m = 8 m
M = w * L² / 8 = 7.5 kN/m * 8 m² / 8 = 60 kN·m

Units are kept. When the input is a number and the script assigns a number times units (L = 6 * m), only the number is replaced, so the line becomes L = 8 * m. Any other input replaces the whole right-hand side.

Which assignment is bound. The first top-level name = value assignment of each input name, including one statement of an a = 1; b = 2 line. Later reassignments still run. A name assigned only inside a block or function, by +=, or by unpacking cannot be bound and raises ValueError.

Input values are usually what JSON holds: numbers, strings, True/False, None, and lists and dicts of these. They are written into the script as typed. Numbers of other types, such as numpy.float64 from a pandas row, are written as plain numbers.

Quantities are converted to the units of their line. With inputs={"w": 6200 * ureg.N / ureg.m}, the line w = 4.5 * kN / m runs and renders as w = 6.2 * kN / m, so the document keeps the script’s units. A quantity whose units do not convert (a length for a load) raises ValueError. pint quantities from another UnitRegistry are converted into the script’s registry.

Other objects, such as arrays, sections or a quantity on a line with no units (A = 100), are assigned as they are and shown as the line’s value. Keep pint quantities on such lines in the script’s registry, since pint cannot calculate with quantities from two registries.

Checks#

strict=True

An input the script never assigns raises KeyError, which catches misspelled names. With strict=False such inputs are ignored, so one input file can serve several scripts.

allow_computed=False

An input for a computed line (M = w * L**2 / 8) raises ValueError, so a calculation is not replaced by accident. With allow_computed=True the line is replaced by the input value.

Finding a script’s inputs#

find_inputs lists the names whose first assignment is a literal or a number times units, with their values in the script (the number only, for a value with units). Use it as a template for an input file:

from handcalcs import find_inputs

find_inputs(pathlib.Path("beam.py"))  # {'w': 5, 'L': 6}

The demo#

hc.demo(destination=None) renders a built-in example calculation (headings, prose, parameter grids, long equations and nested conditionals) with the object’s renderer:

print(HandCalcs(ASCIIRenderer()).demo())

It is a quick way to compare renderers, or to check a renderer you are writing; with a destination, it writes the demo to a file as above. A renderer that cannot render the general demo can supply its own source in a demo_source attribute (the Excel renderer does).

Examples#

Write a LaTeX fragment for \input#

import pathlib
from handcalcs import HandCalcs
from handcalcs.renderers import LatexRenderer

body = HandCalcs(LatexRenderer())(pathlib.Path("calcs/beam.py"))
pathlib.Path("report/beam.tex").write_text(body)

Without destination=, the output is a fragment for the body of your own document; see the LaTeX renderer for the packages it needs.

Serve HTML from a web app#

from handcalcs import HandCalcs
from handcalcs.renderers import HTMLRenderer

hc = HandCalcs(HTMLRenderer())

def beam_page(w: float, L: float) -> str:
    return hc("M = w * L**2 / 8", {"w": w, "L": L, "M": w * L**2 / 8})

One calculation per row of a table#

source = """
w = 5
L = 6
M = w * L**2 / 8
"""
hc = HandCalcs(ASCIIRenderer())
for w, L in [(5, 6), (7.5, 4)]:
    print(hc(source, inputs={"w": w, "L": L}))

Source#

src/handcalcs/interfaces/object_oriented.py