Examples

The following examples illustrate practical applications of nomographs. Each example includes background and the underlying math for the nomograph construction. Source code shows the implementation.

Gasoline Price comparison

Background

Comparing the unit price of a commodity with different units of measure and currencies is greatly simplified with a nomograph. In this example. cross border travellers between the United States and Canada can easily compare the cost of gasoline on both sides of the border. Moreover, by drawing an isopleth thorough the currency rate at par (1.0000) they can directly convert dollars per litre to dollars per USG. This nomograph implements the following formula:

\(\frac{CAD}{L} = \frac{CAD}{USD} \times \frac{USD}{US Gal} \div \frac{L}{US Gal}\)

This equation follows the form of a Type 2 nomograph where:

\(F_1(u_1) = \frac{CAD}{L}\),

\(F_2(u_2) = \frac{CAD}{USD}\)

and

\(F_3(u_3) = \frac{USD}{US Gal} \div 3.78541 \frac{L}{US Gal}\)

Generated nomograph

../_images/ex_gasoline_pricing.png

Source code

 1"""
 2    ex_gasoline_pricing.py
 3
 4    Gasoline price converter
 5"""
 6
 7from pynomo.nomographer import *
 8import sys
 9from pyx import *
10
11sys.path.insert(0, "..")
12
13# allows use of latex commands in PyX such as \frac{a}{b} and \par
14pyx.text.set(text.LatexEngine)
15
16N_params_1 = {
17    "u_min": 1.1,
18    "u_max": 1.6,
19    "function": lambda u: u,
20    "title": r"$\frac{CAD}{L}$",
21    "tick_levels": 4,
22    "tick_text_levels": 3,
23    "text_format": r"$\$%3.3f$",
24    "scale_type": "linear smart",
25    "tick_side": "left",
26}
27
28N_params_2 = {
29    "u_min": 1.0,
30    "u_max": 1.5,
31    "function": lambda u: u,
32    "title": r"$\frac{CAD}{USD}$",
33    "tick_levels": 4,
34    "tick_text_levels": 3,
35    "text_format": r"$%3.4f$",
36    "scale_type": "linear smart",
37    "title_x_shift": 0.5,
38    "title_rotate_text": True,
39}
40
41N_params_3 = {
42    "u_min": 3.0,
43    "u_max": 5.0,
44    "function": lambda u: u / 3.78541,
45    "title": r"$\frac{USD}{US Gal}$",
46    "tick_levels": 4,
47    "tick_text_levels": 2,
48    "scale_type": "linear smart",
49    "text_format": r"$\$%3.3f$",
50    "scale_type": "linear smart",
51}
52
53
54block_1_params = {
55    "block_type": "type_2",
56    "f1_params": N_params_1,
57    "f2_params": N_params_2,
58    "f3_params": N_params_3,
59    "isopleth_values": [[1.3, 1.4, "x"]],
60}
61
62main_params = {
63    "filename": "ex_gasoline_pricing.pdf",
64    "paper_height": 11.0 * 2.54 / 2.0,
65    "paper_width": 8.5 * 2.54 / 2.0,
66    "block_params": [block_1_params],
67    "transformations": [("rotate", 0.01), ("scale paper",)],
68    "title_str": r"\huge \textbf{Gas Price Converter}",
69    "title_y": 13.50,
70    "title_box_width": 15.0,
71    "extra_texts": [
72        {
73            "x": 1.0,
74            "y": 12.5,
75            "text": r"\noindent Is gasoline cheaper \
76                south of the 49\textsuperscript{th}? Use this gas price \
77                converter to be sure. In the example \
78                shown, \$1.300 $\frac{CAD}{L}$ is the same price as \$3.52 $\frac{USD}{US Gal}$ if the exchange rate is 1.40  $\frac{CAD}{USD}$.",
79            "width": 8.0,
80        },
81        {
82            "text": r"\copyright Daniel Boulet (2019-2021)",
83            "x": 3.0,
84            "y": -0.0,
85        },
86    ],
87    # 'make_grid': True
88}
89Nomographer(main_params)

Voltage Divider

Theory and background

In electronics, resistive voltage dividers are used for a variety of purposes. The formula for a resistive voltage divider is [Wik21b]:

\(\frac{V_{out}}{V_{in}} = \frac{R_b}{(R_a + R_b)}\)

../_images/ex_voltage_divider_schematic.png

When designing voltage dividers, constraints may demand engineers choose resistors from a set of “preferred values” [Wik21a]. These values are discrete and engineers must select the best combination of resistors based on tolerance and the available preferred values. Similar voltage ratios can be obtained with different combinations of resistor values.

The voltage divider nomograph links input voltage (\(V_{in}\)), output voltage (\(V_{out}\)) and a pair of resistor values into a single nomograph. Pynomo’s Type 5 blocks are well suited for plotting relationships between pairs of discrete values. A vertical line dropped from the intersection of \(R_a\) values and \(R_b\) values reveals the \(V_{out}\) / \(V_{in}\) voltage ratio. Alignment with a Type 2 block allows the engineer to determine \(V_{out}\) given \(V_{in}\) (or vice versa).

Of greater benefit is the ability to quickly determine the optimum pair of resistor values for a given application. For example, given an input voltage (9V) and desired output voltage (3.3V), the engineer draws a straight line from the \(V_{out}\) axis, through the \(V_{in}\) axis to the base of the voltage ratio graph. A perpendicular line is then drawn from the base to the top of the graph. The vertical line’s nearest approach to the intersection of \(R_a\) and \(R_b\) values represents the best combination of resistor values. It can be quickly shown that one combination of values (\(R_a\) = 6.2 and \(R_b\) = 3.6) will produce an output voltage very close to the desired value (3.3061V).

Generated nomograph

../_images/ex_voltage_divider.png

Source code

  1"""
  2    voltdiv_E24_resistors.py
  3
  4    Nomogram to calculate resistor values for simple voltage divider.  This
  5    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  6"""
  7
  8from pynomo.nomographer import *
  9import sys
 10sys.path.insert(0, "..")
 11
 12from pyx import *
 13pyx.text.set(text.LatexEngine)
 14
 15import numpy as np
 16
 17resistors = [
 18	1.0,	1.1,	1.2,
 19	1.3,	1.5,	1.6,
 20	1.8,	2.0,	2.2,
 21	2.4,	2.7,	3.0,
 22	3.3,	3.6,	3.9,
 23	4.3,	4.7,	5.1,
 24	5.6,	6.2,	6.8,
 25	7.5,	8.2,	9.1
 26]
 27
 28# Type 5 contour
 29def f1(x, u):
 30    return np.log10(u * (1 - x) / x)
 31
 32
 33block_1_params = {
 34    'width': 12.0,
 35    'height': 25.0,
 36    'block_type': 'type_5',
 37
 38    'u_func': lambda u: np.log10(u),
 39    'u_values': resistors,
 40    'u_axis_color': pyx.color.cmyk.Red,
 41    'u_title': r'\Large{$R_a$}',
 42    'u_text_format': r"\normalsize{$%3.1f$}",
 43
 44    'v_func': f1,
 45    'v_values': resistors,
 46    'v_axis_color': pyx.color.cmyk.Red,
 47    'v_title': r'\Large{$R_b$}',
 48    'v_text_format': r"\normalsize{$%3.1f$}",
 49
 50    'wd_tag': 'A',
 51    'wd_tick_side': 'right',
 52	'wd_title':r'\Large $\frac{V_{out}}{V_{in}}$',
 53    'wd_tick_levels': 5,
 54    'wd_tick_text_levels': 2,
 55    'wd_title_opposite_tick': True,
 56    'wd_axis_color': pyx.color.cmyk.Gray,
 57    'isopleth_values': [
 58        [6.2, 'x', 'x'],
 59    ],
 60    'vertical_guide_nr': 10,
 61	'manual_x_scale': True,		# trick to "decompress" Ra scale
 62
 63}
 64
 65# this is non-obvious trick to find bottom edge coordinates of the grid in order
 66# to align it with N nomogram
 67block1_dummy = Nomo_Block_Type_5(mirror_x=False)
 68block1_dummy.define_block(block_1_params)
 69block1_dummy.set_block()
 70
 71# Let's define the N-nomogram
 72N_params_3 = {
 73    'u_min': block1_dummy.grid_box.params_wd['u_min'],
 74    'u_max': block1_dummy.grid_box.params_wd['u_max'],
 75    'function': lambda u: u,
 76    'title': '',
 77    'tag': 'A',
 78    'tick_side': 'right',
 79    'tick_levels': 2,
 80    'tick_text_levels': 2,
 81    'reference': False,
 82    'tick_levels': 0,
 83    'tick_text_levels': 0,
 84    'title_draw_center': True
 85}
 86N_params_2 = {
 87    'u_min': 6.0,
 88    'u_max': 24.0,
 89    'function': lambda u: u,
 90    'title': r'$V_{in}$',
 91    'tag': 'none',
 92    'tick_side': 'left',
 93    'tick_levels': 4,
 94    'tick_text_levels': 3,
 95    'title_draw_center': True,
 96    'scale_type': 'linear smart',
 97}
 98N_params_1 = {
 99    'u_min': 1.0,
100    'u_max': 10.0,
101    'function': lambda u: u,
102    'title': r'$V_{out}$',
103    'tag': 'none',
104    'scale_type': 'linear smart',
105    'tick_side': 'right',
106    'tick_levels': 3,
107    'tick_text_levels': 3,
108    'title_draw_center': True
109}
110
111block_2_params = {
112    'block_type': 'type_2',
113    'f1_params': N_params_1,
114    'f2_params': N_params_2,
115    'f3_params': N_params_3,
116    'isopleth_values': [
117        # Vout, Vin, ratio
118        [3.3, 9.0, 'x'],
119    ]
120}
121
122main_params = {
123    'filename': 'ex_voltage_divider.pdf',
124    'paper_height': 8.5*2.54,
125    'paper_width': 11.0*2.54,
126    'block_params': [block_1_params, block_2_params],
127    'transformations': [('rotate', 0.01), ('scale paper',)],
128    'title_str': r'\Large Voltage Divider Nomograph \par \
129        \normalsize (For E24 series values) \par \bigskip \
130        \large $V_{out}=V_{in} \cdot \frac{R_b}{R_a+R_b}$ \
131        \par \bigskip   \normalsize \copyright    Daniel Boulet  2018-2021',
132    'title_x': 2.0,
133    'title_y': 4.0,
134    'isopleth_params': [
135        {
136            'color': 'blue',
137            'linewidth': 'thick',
138            'linestyle': 'dashed',
139            'circle_size': 0.10,
140        },
141    ],
142}
143
144Nomographer(main_params)

Bicycle Cadence

Theory and background

../_images/gear_image_1.pdf

Choosing the correct gears on a bicycle allows a cyclist to maintain a comfortable cadence. A higher cadence helps reduce muscle fatigue [tra21] though it does put more stress on heart and lungs. However a lower cadence for the same power output puts more stress on the rider’s knees, hips and back. [the21] Furthermore, cycling cadence will vary widely with beginning cyclists peddling more slowly (60-85 rpm) and professionals exceeding 100 rpm under certain conditions. Generally, a good cadence in cycling is between 80-100 rpm. [Hur21] The correct gear ratio can help the rider maximize their speed with a comfortable cadence.

A bicycle’s speed is the product of the wheel diameter (e.g. 700mm), the wheel’s rotation rate (in rpm) and \(\pi\). The wheel’s rotation rate is a function of the rider’s cadence and the front to rear gear ratio. As in the previous example, pairs of discrete values such as the number of teeth on the front and rear sprockets are easily represented on a Type 5 block. This nomograph combines a Type 5 block (to calculate gearing ratio) with a pair of Type 2 blocks to calculate the rider’s speed given their cadence and gear settings.

Generated nomograph

../_images/ex_bicycle_cadence.png

Source code

  1"""
  2    ex_bicycle_cadence.py
  3
  4    Bicycle gearing cadence and speed calculator
  5"""
  6from pynomo.nomographer import *
  7from pyx import *
  8import sys
  9sys.path.insert(0, "..")
 10pyx.text.set(text.LatexEngine)
 11
 12
 13gearing = {
 14    'block_type': 'type_5',
 15    'wd_tag': 'ratio',
 16
 17    'u_func': lambda u: u,
 18    'v_func': lambda x, v: v/x,
 19
 20	# teeth on rear cage
 21    'u_values': [12.0, 14.0, 16.0, 18.0, 21.0, 24.0, 28.0],
 22    'u_scale_type': 'manual point',
 23    'u_manual_axis_data': {12.0: '7', 14.0: '6', 16.0: '5', 18.0: '4', 21.0: '3', 24.0: '2', 28.0: '1'},
 24    'u_title': 'Rear cage',
 25
 26	# teeth on front derailer
 27    'v_values': [28.0, 38.0, 48.0],
 28    'v_scale_type': 'manual point',
 29    'v_manual_axis_data': {28.0: 'Small', 38.0: 'Medium', 48.0: 'Large'},
 30
 31    'wd_tick_levels': 2,
 32    'wd_tick_text_levels': 1,
 33    'wd_tick_side': 'right',
 34    'wd_title_opposite_tick': True,
 35    'isopleth_values': [[14.0, 38.0, 'x']],
 36
 37}
 38
 39
 40wheelrpm = {
 41    'tag': 'wheelrpm',
 42    'u_min': 90.0,
 43    'u_max': 360.0,
 44    'scale_type': 'manual point',
 45    'function': lambda u: u,
 46}
 47
 48crankrpm = {
 49    'u_min': 60.0,
 50    'u_max': 120.0,
 51    'function': lambda u: u,
 52    'title': r'\large \slshape Cadence (RPM)',
 53    'tick_levels': 3,
 54    'tick_text_levels': 2,
 55    'scale_type': 'linear smart',
 56	'tick_side':'left',
 57    'title_draw_center': True,
 58    # 'title_distance_center': -0.5,
 59}
 60
 61ratio = {
 62    'scale_type': 'manual point',
 63    'tag': 'ratio',
 64    'u_min': 1.0,
 65    'u_max': 4.0,
 66    'function': lambda u: u,
 67    'tick_levels': 3,
 68    'tick_text_levels': 1,
 69}
 70
 71
 72rotation = {
 73    'block_type': 'type_2',
 74    'f1_params': wheelrpm,
 75    'f2_params': crankrpm,
 76    'f3_params': ratio,
 77    'isopleth_values': [['x', 75, 'x']],
 78}
 79
 80
 81speed = {
 82    'u_min': 15.0,
 83    'u_max': 45.0,
 84    'function': lambda u: u,
 85    'title': r'\large \slshape Speed (km/hour)',
 86    'tick_levels': 5,
 87    'tick_text_levels': 2,
 88    'scale_type': 'linear smart',
 89    'title_draw_center': True,
 90    'title_distance_center': -0.5,
 91}
 92
 93diameter = {
 94    'u_min': 600.0,
 95    'u_max': 800.0,
 96    'function': lambda u: u*3.1415927*60.0/1000000.0,
 97    'title': r'Wheel Diameter (mm)',
 98    'tick_levels': 2,
 99    'tick_text_levels': 1,
100    'scale_type': 'linear smart',
101    'title_draw_center': True,
102    'title_distance_center': -0.5,
103
104}
105
106wheelrpm2 = {
107    'tag': 'wheelrpm',
108    'u_min': 90.0,
109    'u_max': 360.0,
110    'function': lambda u: u,
111    'scale_type': 'linear smart',
112
113    'title': r'\large \slshape Wheel RPM',
114    'title_x_shift': -18.5,
115    'tick_levels': 5,
116    'tick_text_levels': 3,
117}
118
119
120speedblock = {
121    'block_type': 'type_2',
122    'f1_params': speed,
123    'f2_params': diameter,
124    'f3_params': wheelrpm2,
125    'mirror_x': True,
126    'isopleth_values': [['x', 750.0, 'x']],
127}
128
129
130main_params = {
131    'filename': 'ex_bicycle_cadence.pdf',
132    'block_params': [gearing, rotation, speedblock],
133    'transformations': [('rotate', 0.01), ('scale paper',)],
134    'title_str': r'\Large \textbf{Bicycle Cadence Calculator}',
135    'title_x': 3.5,
136    'title_y': 3.5,
137
138    'extra_texts': [
139        {
140            'x': -0.5,
141            'y': 11.3,
142            'text': r'\large \slshape{Gearing Ratio}',
143        },
144        {
145            'x': 7.0,
146            'y': 20.5,
147            'text': r'\large \slshape{Front derailer setting}',
148        },
149        {
150            'text': r'\copyright Daniel Boulet (2019-2021)',
151            'x': -0.5,
152            'y': 3.0,
153        },
154
155
156    ],
157	# 'make_grid':True,
158
159}
160
161Nomographer(main_params)

Z Score

Theory and background

This example extends Pynomo’s versatility by using external libraries. Python’s scipy library is the engine behind this nomograph which calculates the area under a normal distribution curve between two Z scores (one negative, the other positive).

To calculate the area between two Z scores (\(Z_{upper}\), \(Z_{lower}\)) of a normal distribution one must compute the difference between the respective probability density functions \(\mathrm{PDF}(Z_{upper})\) and \(\mathrm{PDF}(Z_{lower})\). [wik07]

../_images/The_Normal_Distribution.svg

Recall that the functional relatinship for a Type 1 block is:

\(F1(u_1)+F2(u_2)+F3(u_3)=0\)

and

\(\mathrm{Area} = \mathrm{PDF}(Z_{upper}) - \mathrm{PDF}(Z_{lower})\)

therefore

\(\mathrm{PDF}(Z_{upper}) - Area - \mathrm{PDF}(Z_{lower})=0\).

Two Type 8 axes are aligned with \(\mathrm{PDF}(Z_{upper})\) and \(\mathrm{PDF}(Z_{lower})\) to align a Z score with its associated PDF.

Generated nomograph

../_images/ex_zscore.png

Source code

  1"""
  2    ex_zscore.py
  3
  4    Nomograph to calculate area under normal curve from z-score.
  5"""
  6
  7import scipy.stats as stats
  8from pynomo.nomographer import *
  9import sys
 10
 11sys.path.insert(0, "..")
 12# allows use of latex commands in PyX such as \frac{a}{b} and \par
 13from pyx import *
 14
 15pyx.text.set(text.LatexEngine)
 16
 17
 18def cdf(u):
 19    return stats.norm.cdf(u)
 20
 21
 22def ppf(u):
 23    return stats.norm.ppf(u)
 24
 25
 26lmin = 0.0001
 27lmax = 0.9999
 28
 29sd1 = cdf(1.0) - cdf(-1.0)
 30sd2 = cdf(2.0) - cdf(-2.0)
 31sd3 = cdf(3.0) - cdf(-3.0)
 32
 33
 34leftpdf = {
 35    "tag": "a",
 36    "u_min": lmin,
 37    "u_max": 0.5,
 38    "function": lambda u: (u),
 39    "scale_type": "manual point",
 40    # extra parameters
 41    "extra_params": [
 42        {
 43            "scale_type": "manual arrow",
 44            "manual_axis_data": {
 45                cdf(-1.0): r"%4.4f" % cdf(-1.0),
 46                cdf(-2.0): r"%4.4f" % cdf(-2.0),
 47                cdf(-3.0): r"%4.4f" % cdf(-3.0),
 48                cdf(-1.25): r"%4.4f" % cdf(-1.25),
 49            },
 50            "arrow_length": 2.0,
 51        },
 52    ],
 53}
 54
 55
 56leftz = {
 57    "tag": "a",
 58    "u_min": ppf(lmin),
 59    "u_max": ppf(0.5),
 60    "function": lambda u: cdf(u),
 61    "align_func": lambda u: cdf(u),
 62    "title": "Lower tail $X_1$",
 63    "tick_levels": 5,
 64    "tick_text_levels": 4,
 65    "scale_type": "linear smart",
 66    "tick_side": "left",
 67}
 68
 69
 70rightpdf = {
 71    "tag": "c",
 72    "u_min": 0.5,
 73    "u_max": lmax,
 74    "function": lambda u: -(u),
 75    "scale_type": "manual point",
 76    # extra parameters
 77    "extra_params": [
 78        {
 79            "scale_type": "manual arrow",
 80            "tick_side": "left",
 81            "manual_axis_data": {
 82                cdf(3.0): r"%4.4f" % cdf(3.0),
 83                cdf(2.0): r"%4.4f" % cdf(2.0),
 84                cdf(1.0): r"%4.4f" % cdf(1.0),
 85                cdf(0.25): r"%4.4f" % cdf(0.25),
 86            },
 87            "arrow_length": 2.0,
 88        },
 89    ],
 90}
 91
 92
 93rightz = {
 94    "tag": "c",
 95    "u_min": ppf(0.5),
 96    "u_max": ppf(lmax),
 97    "function": lambda u: cdf(u),
 98    "align_func": lambda u: cdf(u),
 99    "title": "Upper tail $X_2$",
100    "tick_levels": 5,
101    "tick_text_levels": 4,
102    "scale_type": "linear smart",
103}
104
105leftblock2 = {"block_type": "type_8", "f_params": leftz, "isopleth_values": [["x"]]}
106
107
108rightblock2 = {"block_type": "type_8", "f_params": rightz, "isopleth_values": [["x"]]}
109
110
111delta = {
112    "u_min": 0.0,
113    "u_max": 1.0,
114    "function": lambda u: u,
115    "scale_type": "linear smart",
116    "title": "Area",
117    "tick_levels": 5,
118    "tick_text_levels": 4,
119    "extra_params": [
120        {
121            "tick_side": "left",
122            "scale_type": "manual arrow",
123            "manual_axis_data": {
124                sd1: r"$\pm 1 \sigma$ %4.4f" % sd1,
125                sd2: r"$\pm 2 \sigma$ %4.4f" % sd2,
126                sd3: r"$\pm 3 \sigma$ %4.4f" % sd3,
127            },
128            "arrow_length": 2.0,
129        },
130    ],
131}
132
133block_diff = {
134    "block_type": "type_1",
135    "f1_params": leftpdf,
136    "f2_params": delta,
137    "f3_params": rightpdf,
138    "proportion": 1.5,
139    "isopleth_values": [[cdf(-1.25), "x", cdf(0.25)]],
140}
141
142
143main_params = {
144    "filename": "ex_zscore.pdf",
145    "paper_height": 11.0 * 2.54,
146    "paper_width": 8.5 * 2.54,
147    "block_params": [block_diff, leftblock2, rightblock2],
148    "cdfations": [("rotate", 0.01), ("scale paper",), ("polygon",)],
149    "title_x": 11.0,
150    "title_y": 3.0,
151    "title_box_width": 15.0,
152    "title_str": r"\Huge Z Score Nomograph \par \medskip \large ($\mu = 0$, $\sigma = 1$) \par \medskip  \small \copyright Daniel Boulet  2021",
153    # 'make_grid':True,
154    "extra_texts": [
155        {
156            "x": 2.5,
157            "y": 16.0,
158            "text": r"\noindent Area under normal curve between lower tail $X_1$ and upper tail $X_2$ is read on center axis. \
159                For example, area under normal distribution curve from $%g \sigma$ to $%g \sigma$ is $%4.4f$."
160            % (-1.25, 0.25, cdf(0.25) - cdf(-1.25)),
161            "width": 8.0,
162        },
163    ],
164}
165
166Nomographer(main_params)

Frequency response

Contribution: Add Lamorille

Theory and background

For a single degree of freedom system (driven harmonic oscillator), the force frequency response (transmissibility) \(T\) is defined as the ratio between the magnitude of the transmitted force and the magnitude of the applied force. The frequency factor (dimensionless frequency) \(r\) is the ratio between the driving frequency and the natural frequency of the system. The damping ratio \(\zeta\) is a measure describing how rapidly the oscillations decay from one bounce to the next, it is specific to the system. Vibration isolation is possible only when the frequency factor is greater than \(\sqrt{2}\). Relation is according equation:

\(T = \sqrt{\frac{1 + (2\zeta r)^2}{(1-r^2) + (2\zeta r)^2}}.\)

Generated nomograph

../_images/ex_transmissibility.png

Source code

  1# T=sqrt[(1+(2zr)**2)/((1-r**2)**2+(2zr)**2]
  2# Copyright Add LaMorille 2023
  3# merging of two type 5 blocks, as pynomo does not draw the whole function in one
  4
  5
  6from pynomo.nomographer import *
  7from pyx import *
  8import numpy as np
  9
 10u_min = 0.1  # T
 11u_max = 6.0
 12
 13wd_min_left = 0.05  # r
 14wd_max_left = 1.0
 15
 16wd_min_right = 0.5  # r
 17wd_max_right = 10.0
 18
 19
 20def fT(r, z):  # u(x,v)
 21    r = np.exp(r)  # log x scale
 22    return ((1 + 4 * r * r * z * z) / (((1 - r * r) ** 2) + 4 * r * r * z * z)) ** 0.5
 23
 24
 25block_params_left = {
 26    'block_type': 'type_5',
 27    'u_func': lambda u: np.log10(u),  # log y scale
 28    'v_func': lambda x, v: np.log10(fT(x, v)),  # log y scale
 29    'u_values': [u_min, u_max],
 30    'scale_type_u': 'log smart',
 31    'u_tick_levels': 4,
 32    'u_tick_text_levels': 2,
 33    'u_title': '$T$',
 34    'u_title_draw_center': False,
 35    'u_tick_side': 'left',
 36    'v_values': [0.0, 0.2, 0.5, 1.0],
 37    'v_manual_axis_data': {0.0: r'', 0.2: r'', 0.5: r'', 1.0: r''},
 38    'v_title': '',
 39    'manual_x_scale': True,
 40    'scale_type_wd': 'log smart',
 41    'x_min': np.log(wd_min_left),  # log x scale
 42    'x_max': np.log(wd_max_left),  # log x scale
 43    'wd_func': lambda x: np.log(x),  # log x scale
 44    'wd_func_inv': lambda x: np.exp(x),  # log x scale
 45    'wd_tick_levels': 0,
 46    'wd_tick_text_levels': 0,
 47    'wd_title': '',
 48    'wd_tag': 'axis_left',
 49    'vertical_guides': False,
 50}
 51
 52block_params_right = {
 53    'block_type': 'type_5',
 54    'u_func': lambda u: np.log10(u),
 55    'v_func': lambda x, v: np.log10(fT(x, v)),
 56    'u_values': [u_min, u_max],
 57    'scale_type_u': 'log smart',
 58    'u_tick_levels': 0,
 59    'u_tick_text_levels': 0,
 60    'u_title': '',
 61    'v_values': [0.0, 0.2, 0.5, 1.0],
 62    'v_manual_axis_data': {0.0: ['$\zeta=0.0$', {'x_corr': 2.25, 'y_corr': -14, 'draw_line': False}],
 63                           0.2: ['$\zeta=0.2$', {'x_corr': 4.5, 'y_corr': -9.5, 'draw_line': False}],
 64                           0.5: ['$\zeta=0.5$', {'x_corr': 5.75, 'y_corr': -6, 'draw_line': False}],
 65                           1.0: ['$\zeta=1.0$', {'x_corr': 6.9, 'y_corr': -3.75, 'draw_line': False}], },
 66    'v_title': '',
 67    'manual_x_scale': True,
 68    'scale_type_wd': 'log smart',
 69    'x_min': np.log(wd_min_right),
 70    'x_max': np.log(wd_max_right),
 71    'wd_func': lambda x: np.log(x),
 72    'wd_func_inv': lambda x: np.exp(x),
 73    'wd_tick_levels': 0,
 74    'wd_tick_text_levels': 0,
 75    'wd_title': '',
 76    'wd_tag': 'axis_right',
 77    'vertical_guides': False,
 78}
 79
 80axis_params = {  # for proper axis management
 81    'tag': 'axis_left',
 82    'dtag': 'axis_right',
 83    'u_min': wd_min_left,
 84    'u_max': wd_max_right,
 85    'function': lambda u: np.log(u),
 86    'scale_type': 'log smart',
 87    'title': r'r',
 88    'tick_levels': 3,
 89    'tick_text_levels': 2,
 90    'title_x_shift': 0.0,
 91    'title_y_shift': -1.2,
 92    'tick_side': 'left',
 93    'extra_params': [{'tick_side': 'left', 'scale_type': 'manual line',
 94                      'manual_axis_data': {1.414: r'$\sqrt 2$'}, }, ],
 95}
 96
 97block_axis_params = {
 98    'block_type': 'type_8',
 99    'f_params': axis_params,
100}
101
102
103def post(c):  # hiding outlines
104    c.stroke(path.line(8.515, 0.01, 8.515, 14.99) + path.line(6.565, 0.01, 6.565, 14.99),
105             [style.linewidth.thick, color.cmyk.White])
106
107
108main_params = {
109    'filename': "ex_transmissibility.pdf",
110    'paper_height': 15,
111    'paper_width': 15,
112    'block_params': [block_params_left, block_params_right, block_axis_params],
113    'transformations': [('scale paper',)],
114    'make_grid': False,
115    'draw_lines': True,
116    'line_params': [{'coords': [[0, 8.435, 9.49, 8.435], [9.495, 8.435, 9.495, 0]],  # manual isopleth
117                     'line_style': [color.cmyk.Black, style.linewidth.thick, style.linestyle.dashed],
118                     'circle_size': 0.0, }],
119    'title_x': 12.75,
120    'title_y': 14.0,
121    'title_box_width': 4.0,
122    'title_str': r'\large Pynomo Type 5 \par \
123                   \par $T=\sqrt{{1+(2 \zeta r)^2}\over{(1-r^2)^2+(2 \zeta r)^2}}$ \par \
124                   \par \normalsize $T=$ transmissibility \par $\zeta=$ damping ratio \par $r=$ frequency ratio',
125    'post_func': post,
126}
127
128Nomographer(main_params)

Example: Amortized loan calculator

Theory and background

This approach of constructing an amortized loan calculator is similar to one in Ref. [dOcagne99]

Equation for amortized loan [wik21a] is:

\(\frac{a}{A} = \frac{\frac{p}{100\times 12}}{1-\frac{1}{(1+\frac{p}{100\times 12})^{12n}}},\)

where \(A\) is the amount of loan, \(a\) is monthly payment amount, \(p\) interest rate per year (monthly interest rate is taken as \(p/12\)) [wik21b] and \(n\) is number of years for payment.

This equation of four variables is probably impossible to present with line and grid nomographs. For this reason a “Type 5” contour nomogram is constructed of the right hand side of the equation and left hand equation is just N-nomogram (Type 2). The two equations for nomogram construction are:

\(x = \frac{a}{A}\)

and

\(x = \frac{\frac{p}{100\times 12}}{1-\frac{1}{(1+\frac{p}{100\times 12})^{12n}}}.\)

In practice \(x\) is the x-coordinate of the canvas where nomogram is constructed.

Right hand side of equation

By defining coordinates \(x\,\) and \(y\,\):

\(x = \frac{\frac{p}{100\times 12}}{1-\frac{1}{(1+\frac{p}{100\times 12})^{12n}}},\)

\(y = 12n, \,\) we may solve \(y\,\) in terms of \(x\,\) and \(n\,\):

\(y = \frac{\log (\frac{x}{x-\frac{p}{100\times 12}})}{\log (1+\frac{p}{100 \times 12})} \,\)

The previous two equations are of correct form

\(y = f_1(v) \,\)

and

\(y = f_2(x,u) \,\)

for type 5 nomogram. For compressing time axis (\(y\)-axis), we transform \(y \rightarrow \log y\) and find

\(y = \log \left( \frac{\log (\frac{x}{x-\frac{p}{100\times 12}})}{\log (1+\frac{p}{100 \times 12})} \right)\,\)

\(y = \log( 12n ). \,\)

Left hand side of equation

Left hand side of equation

\(x = \frac{a}{A}\)

is just N-nomogram

\(F_1(u_1) = F_2(u_2)F_3(u_3) \,\)

References

Generated nomograph

../_images/amortized_loan.png

Source code

 1"""
 2    ex_amortized_loan.py
 3
 4    Amortized loan calculator
 5"""
 6import sys
 7sys.path.insert(0, "..")
 8from pynomo.nomographer import *
 9
10# Type 5 contour
11def f1(x,u):
12    return log(log(x/(x-u/(100.0*12.0)))/log(1+u/(100.0*12.0)))
13
14block_1_params={
15            'width':10.0,
16           'height':5.0,
17           'block_type':'type_5',
18           'u_func':lambda u:log(u*12.0),
19           'v_func':f1,
20           'u_values':[10.0,11.0,12.0,13.0,14.0,15.0,20.0,25.0,30.0,40.0,50.0,60.0],
21           'v_values':[1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,9.0,10.0],
22           'wd_tag':'A',
23           'u_title':'years',
24           'v_title':r'interest rate = ',
25           'u_text_format':r"$%3.0f$ ",
26           'v_text_format':r"$%3.0f$ \%% ",
27           'isopleth_values':[[21,5,'x']]
28             }
29
30# this is non-obvious trick to find bottom edge coordinates of the grid in order
31# to align it with N nomogram
32block1_dummy=Nomo_Block_Type_5(mirror_x=False)
33block1_dummy.define_block(block_1_params)
34block1_dummy.set_block()
35
36# Let's define the N-nomogram
37N_params_3={
38        'u_min':block1_dummy.grid_box.params_wd['u_min'],
39        'u_max':block1_dummy.grid_box.params_wd['u_max'],
40        'function':lambda u:u,
41        'title':'',
42        'tag':'A',
43        'tick_side':'right',
44        'tick_levels':2,
45        'tick_text_levels':2,
46        'reference':False,
47        'tick_levels':0,
48        'tick_text_levels':0,
49        'title_draw_center':True
50                }
51N_params_2={
52        'u_min':30.0,
53        'u_max':1000.0,
54        'function':lambda u:u,
55        'title':'Loan',
56        'tag':'none',
57        'tick_side':'left',
58        'tick_levels':4,
59        'tick_text_levels':3,
60        'title_draw_center':True,
61        #'text_format':r"$%3.0f$ ",
62        'scale_type':'linear smart',
63                }
64N_params_1={
65        'u_min':0.2,
66        'u_max':3.0,
67        'function':lambda u:u,
68        'title':'monthly payment',
69        'tag':'none',
70        'tick_side':'right',
71        'tick_levels':3,
72        'tick_text_levels':2,
73        'title_draw_center':True
74                }
75
76block_2_params={
77             'block_type':'type_2',
78             'width':10.0,
79             'height':20.0,
80             'f1_params':N_params_1,
81             'f2_params':N_params_2,
82             'f3_params':N_params_3,
83             'isopleth_values':[['x',200,'x']]
84             }
85
86main_params={
87              'filename':'amortized_loan.pdf',
88              'paper_height':20.0,
89              'paper_width':20.0,
90              'block_params':[block_1_params,block_2_params],
91              'transformations':[('rotate',0.01),('scale paper',)],
92                'title_str':r'Amortized loan calculator    \copyright    Leif Roschier  2009',
93                'title_x': 17,
94                'title_y': 21,
95                'title_box_width': 5
96              }
97Nomographer(main_params)

Example Photography exposure

Theory and background

This example illustrates how exposure in photography depends on factors: latitude, time of day, day of year, weather, composition. It relates these to camera settings: film speed (e.g. ISO 100), aperture and shutter speed. The mathematical approach and model is taken from book written by V. Setälä [Set40]. This book illustrates the approach as nomographs but they are different compared with the one generatated here. Book uses shadow length, but we break shadow length into time, date and latitude via solar zenith angle.

The basic equation in Setälä (pp.492-494) can be extracted and written as

(1)\[FS-L-A-W+C+T=0 \,\]

where parameters of (1) are listed below:

\(FS\,\)

Film speed

DIN value that equals \(10 \log (S) +1 \,\),where S is ISO FILM speed

\(T\,\)

shutter time

\(10 \log \left( \frac{t}{1/10}\right)\)

\(A\,\)

aperture

\(10 \log \left(\frac{N^2}{3.2^2}\right)\)

\(L\,\)

shadow length (in steps)

two times (shadow length)/(person length) \(= 2 \arctan ( \phi) \,\), where \(\phi \,\) is solar zenith angle.

\(W\,\)

weather

Clear sky, Cumulus clouds: 0, Clear sky: 1, Sun through clouds: 3, Sky light gray: 6, Sky dark gray: 9, Thunder-clouds cover sky: 12

\(C\,\)

Composition

Person under trees: -6, Inside forest : -4, Person in shadow of wall : -1, Person at open place; alley under trees : 2, Buildings; street : 5, Landscape and front matter : 7, Open landscape : 9, Snow landscape and front matter; beach : 11,Snow field; open sea : 13, Clouds : 15

It is to be noted that Setälä has stops ten times base-10 logarithmic. Today we think stops in base-2 logarithmic.

Shadow lenght

Calculation of shadow length as a function of day of year, time of day and latitude is according to [sol]. Following equations are used. For fractional year (without time information) we take

\(\gamma = (day-1+0.5)2\pi /365.\)

For time offset (eqtime) we use equation (in minutes)

\(TO = 229.18(0.000075+0.001868\cos(\gamma)-0.032077\sin(\gamma)-0.014615\cos(2\gamma)-0.040849\sin(2\gamma))\)

to calculate that error is below 17 minutes for time axis. We assume that sun is at heightest point at noon and this is the error and approximation. We calculate stops in logarithmic scale and in this case we do not need very accurate equations for time. For declination we use equation

and for hour angle

\(ha=(60h+\overline{TO})/4-180. \,\)

Solar zenith angle (\(\phi \,\)), latitude (LAT), declination (D) and hour angle (ha) are connected with equation:

\(\cos (\phi ) = \sin(LAT)sin(D)+\cos(LAT)\cos(D)\cos(ha). \,\)

This is in our desired form as a function of hour (h), day (day), latitude (LAT), solar zenith angle (\(\phi \,\)):

\(\cos (\phi ) = \sin(LAT)sin(D(\gamma(day)))+\cos(LAT)\cos(D(\gamma(day)))\cos(ha(h)) \,\).

In practice illuminance of flat surface on earth depends on solar zenith angle as \(\cos(\phi)\,\). Setälä uses shadow length that is easily measurable, but scales incorrectly, as value is proportional to \(\tan(\phi)\,\). Also Setälä sums linear value with logarithmic ones as a practical approximation. To correct these assumptions, here we assume that values for shadow length 1 and 10 for Setälä are reasonable, and an equation that scales logarithmically is found:

\(L = 0.33766 - 13.656 \log10 (\cos(\phi)) \,\)

that gives \(L=1\,\) for \(\phi = 26.565 =\arctan(1/2)\,\) and \(L=10\,\) for \(\phi = 78.69 =\arctan(10/2).\,\)

Construction of the nomograph

The presented equation is the following:

\begin{eqnarray*} FS - \{0.33766 - 13.656 \log_{10}[ \sin (LAT)\sin (D(\gamma(day)))+\cos (LAT)\cos (D(\gamma(day)))\cos (ha(h))]\}\\ - A - W + C + T = 0. \end{eqnarray*}

In order to construct the nomograph, we split the equation into four blocks and an additional block to present values as EV100.

Main equation split into blocks for the nomograph.

Explanation

Type

\[x_1 \equiv \cos(\phi)=\sin(LAT)sin(D(\gamma(day)))+\cos(LAT)\cos(D(\gamma(day)))\cos(ha(h))\,\]

formed into determinant:

\[\begin{split}{{ \begin{vmatrix} 0 & \cos(\phi) & 1 \\ \frac{\cos(LAT)\cos(D(\gamma(day)))}{1+(\cos(LAT)\cos(D(\gamma(day))))} & \frac{\sin(LAT)\sin(D(\gamma(day)))}{1+(\cos(LAT)\cos(D(\gamma(day))))} & 1 \\ 1 & -\cos(ha(h)) & 1 \\ \end{vmatrix} = 0 }}\end{split}\]

Type 9

\[C_1 \equiv L+W = 0.006918-13.656 \log_{10}(x_1)+W\]

split into two equations for contour construction:

\[y_1 = C_1 \,\]
\[y_1 = 0.006918-13.656 \log_{10}(x_1)+W\]

Type 5

\[C_2 \equiv L+W+C = C_1+C\]

split into two equations for contour construction:

\[y_2 = C_2\]
\[y_2 = C_1+C\]

Type 5

\[C_2 = FS-A+T\]

equals

\[C_2 -(10 \log_{10}(S)+1.0)+10 \log_{10}\left(\frac{N^2}{3.2^2} \right)-10 \log_{10}\left( \frac{1/t_i}{1/10}\right)=0,\]

where

\[t_i\equiv 1/t\]

is inverse shutter time.

Type 3

Additional EV100 scale by using relation

\[C_2 =(-EV_{100}+13.654)/0.3322\]

Type 8

Maximum focal length calculator according to equation

\[t_i / f = FL\]

written as

\[-10 \log_{10}\left( \frac{1/t_i}{1/10}\right) - 10 \log_{10}\left( \frac{f}{10} \right) -10 \log_{10}\left( FL \right) = 0\]

in order to align correctly with previous equation. The values for the factor f are: DSLR (3/2), 35mm (1), DSLR image stabilization (3/8) and 35mm image stabilization (1/8).

Type 1

Generated nomograph

../_images/ex_photo_exposure.png

Source code

  1"""
  2    ex_photo_exposure.py
  3
  4    Photgraph exposure.
  5"""
  6import sys
  7sys.path.insert(0, "..")
  8from pynomo.nomographer import *
  9"""
 10functions for solartime taken from solareqns.pdf from
 11http://www.srrb.noaa.gov/highlights/sunrise/solareqns.PDF
 12"""
 13
 14
 15# fractional year
 16def gamma(day):
 17    return 2 * pi / 365.0 * (day - 1 + 0.5)
 18# equation of time
 19
 20
 21def eq_time(day):
 22    gamma0 = gamma(day)
 23    return 229.18 * (0.000075 + 0.001868 * cos(gamma0) - 0.032077 * sin(gamma0)\
 24                   - 0.014615 * cos(2 * gamma0) - 0.040849 * sin(2 * gamma0))
 25
 26# mean correction, with constant correction we make less than 17 minutes  error
 27# in time axis
 28temp_a = arange(0, 365.0, 0.1)
 29temp_b = eq_time(temp_a)
 30correction = mean(temp_b) # this is 0.0171885 minutes
 31
 32
 33# declination
 34def eq_declination(day):
 35    g0 = gamma(day)
 36    return 0.006918 - 0.399912 * cos(g0) + 0.070257 * sin(g0) - 0.006758 * cos(2 * g0)\
 37            + 0.000907 * sin(2 * g0) - 0.002697 * cos(3 * g0) + 0.00148 * sin(3 * g0)
 38
 39
 40def f1(dummy):
 41    return 0.0
 42
 43
 44def g1(fii):
 45    return cos(fii*pi/180.0)
 46
 47
 48def f2(lat, day):
 49    dec = eq_declination(day)
 50    return (cos(lat * pi / 180.0) * cos(dec)) / (1.0 + (cos(lat * pi / 180.0) * cos(dec)))
 51
 52
 53def g2(lat, day):
 54    dec = eq_declination(day)  # in radians
 55    return (sin(lat * pi / 180.0) * sin(dec)) / (1.0 + (cos(lat * pi / 180.0) * cos(dec)))
 56
 57
 58def f3(dummy):
 59    return 1
 60
 61
 62def g3(h):
 63    hr = (h * 60.0 + correction) / 4.0 - 180.0
 64    return -1.0 * cos(hr * pi / 180.0)
 65
 66days_in_month = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
 67times1=[]
 68for idx in range(0, 12):
 69    times1.append(sum(days_in_month[0:idx])+1)
 70
 71time_titles = ['January', 'February', 'March', 'April', 'May', 'June',
 72               'July', 'August', 'September', 'October', 'November', 'December']
 73
 74phi_params = {'u_min': 0.0,
 75              'u_max': 90.0,
 76              'u_min_trafo': 0.0,
 77              'u_max_trafo': 90.0,
 78              'f': f1,
 79              'g': g1,
 80              'h': lambda u: 1.0,
 81              'title': r'Solar zenith angle $\phi$',
 82              'title_x_shift': 0.0,
 83              'title_y_shift': 0.25,
 84              'scale_type': 'linear smart',
 85              'tick_levels': 4,
 86              'tick_text_levels': 2,
 87              'tick_side': 'right',
 88              'tag': 'phi',
 89              'grid': False,
 90              }
 91time_params = {'u_min': 0.0,
 92               'u_max': 23.0,
 93               'u_min_trafo': 0.0,
 94               'u_max_trafo': 12.0,
 95               'f': f3,
 96               'g': g3,
 97               'h':lambda u: 1.0,
 98               'title': r'Hour (h)',
 99               'title_x_shift': 0.0,
100               'title_y_shift': 0.25,
101               'scale_type': 'linear',
102               'tick_levels': 2,
103               'tick_text_levels': 1,
104               'tick_side': 'right',
105               'tag': 'none',
106               'grid': False,
107               }
108lat_day_params = {'ID': 'none',  # to identify the axis
109                  'tag': 'none',  # for aligning block wrt others
110                  'title': 'Grid',
111                  'title_x_shift': 0.0,
112                  'title_y_shift': 0.25,
113                  'title_distance_center': 0.5,
114                  'title_opposite_tick': True,
115                  'u_min': 20.0,  # for alignment
116                  'u_max': 80.0,  # for alignment
117                  'f_grid': f2,
118                  'g_grid': g2,
119                  'h_grid': lambda u, v: 1.0,
120                  'u_start': 30.0,
121                  'u_stop': 80.0,
122                  'v_start': times1[0],  # day
123                  'v_stop': times1[-1],
124                  'u_values': [30.0, 40.0, 50.0, 60.0, 70.0, 80.0],
125                  'u_texts': ['30', '40', '50', 'Latitude = 60', '70', '80'],
126                  'v_values': times1,
127                  'v_texts': time_titles,
128                  'grid': True,
129                  'text_prefix_u': r'',
130                  'text_prefix_v': r'',
131                  'text_distance': 0.5,
132                  'v_texts_u_start': False,
133                  'v_texts_u_stop': True,
134                  'u_texts_v_start': False,
135                  'u_texts_v_stop': True,
136                  }
137block_params = {'block_type': 'type_9',
138                'f1_params': phi_params,
139                'f2_params': lat_day_params,
140                'f3_params': time_params,
141                'transform_ini': True,
142                'isopleth_values': [['x', [60, times1[4]], 14.0]]
143                }
144
145
146# limiting functions are to avoid NaN in contour construction that uses optimization
147def limit_xx(x):
148    x1 = x
149    return x1
150
151
152def limit_x(x):
153    x1 = x
154    return x1
155
156const_A = 0.33766
157const_B = -13.656
158
159block_params_weather = {'block_type': 'type_5',
160                        'u_func': lambda u: u,
161                        'v_func':lambda x, v: const_A + const_B * log10(limit_x(x)) + v,
162                        'u_values': [1.0, 25.0],
163                        'u_manual_axis_data': {1.0: '',
164                                               25.0: ''},
165                        'v_values': [0.0, 1.0, 3.0, 6.0, 9.0, 12.0],
166                        'v_manual_axis_data': {0.0: ['Clear sky, Cumulus clouds',
167                                                     {'x_corr': 0.5,
168                                                      'y_corr': 0.0,
169                                                      'draw_line': False}],
170                                               1.0: 'Clear sky',
171                                               3.0: 'Sun through clouds',
172                                               6.0: 'Sky light gray',
173                                               9.0: 'Sky dark gray',
174                                               12.0: 'Thunder-clouds cover sky'},
175                        'v_text_distance': 0.5,
176                        'wd_tick_levels': 0,
177                        'wd_tick_text_levels': 0,
178                        'wd_tick_side': 'right',
179                        'wd_title': '',
180                        'manual_x_scale': True,
181                        'x_min': 0.06,
182                        'x_max': 0.99,
183                        'u_title': '',
184                        'v_title': '',
185                        'wd_title_opposite_tick': True,
186                        'wd_title_distance_center': 2.5,
187                        'wd_align_func': lambda L: acos(limit_xx(10.0**((L - const_A) / const_B))) * 180.0 / pi,  # phi as L
188                        'wd_func': lambda L: 10.0**((L - const_A) / const_B),  # x as L
189                        'wd_func_inv': lambda x: const_A+const_B * log10(x),  # L as x
190                        'wd_tag': 'phi',
191                        'mirror_y': True,
192                        'mirror_x': False,
193                        'width': 10.0,
194                        'height': 10.0,
195                        'u_scale_opposite': True,
196                        'u_tag': 'AA',
197                        'horizontal_guides': True,
198                        'isopleth_values': [['x', 9.0, 'x']],
199                        }
200block_params_scene = {'block_type': 'type_5',
201                      'u_func': lambda u: u,
202                      'v_func': lambda x, v: x + v,
203                      'u_values': [1.0, 25.0],
204                      'u_manual_axis_data': {1.0: '',
205                                             25.0: ''},
206                      'u_tag': 'AA',
207                      'wd_tag': 'EV',
208                      'v_values': [-4.0, -1.0, 2.0, 5.0, 7.0, 9.0, 11.0, 13.0, 15.0],
209                      'v_manual_axis_data': {-6.0: 'Person under trees',
210                                             -4.0: 'Inside forest',
211                                             -1.0: 'Person in shadow of wall',
212                                             2.0: 'Person at open place; alley under trees',
213                                             5.0: 'Buildings; street',
214                                             7.0: 'Landscape and front matter',
215                                             9.0: 'Open landscape',
216                                             11.0: 'Snow landscape and front matter; beach',
217                                             13.0: 'Snow field; open sea',
218                                             15.0: 'Clouds',
219                                             },
220                      'wd_tick_levels': 0,
221                      'wd_tick_text_levels': 0,
222                      'wd_tick_side': 'right',
223                      'wd_title': '',
224                      'u_title': '',
225                      'v_title': '',
226                      'wd_title_opposite_tick': True,
227                      'wd_title_distance_center': 2.5,
228                      'mirror_x': True,
229                      'horizontal_guides': True,
230                      'u_align_y_offset': -0.9,
231                      'isopleth_values': [['x', 2.0, 'x']],
232                      }
233camera_params_1 = {'u_min': -10.0,
234                   'u_max': 15.0,
235                   'function': lambda u: u,
236                   'title': r'',
237                   'tick_levels': 0,
238                   'tick_text_levels': 0,
239                   'tag': 'EV',
240                   }
241camera_params_2 = {'u_min': 10.0,
242                   'u_max': 25600.0,
243                   'function': lambda S: -(10 * log10(S) + 1.0),
244                   'title': r'Film speed',
245                   'manual_axis_data': {10.0: 'ISO 10',
246                                        20.0: 'ISO 20',
247                                        50.0: 'ISO 50',
248                                        100.0: 'ISO 100',
249                                        200.0: 'ISO 200',
250                                        400.0: 'ISO 400',
251                                        800.0: 'ISO 800',
252                                        1600.0: 'ISO 1600',
253                                        3200.0: 'ISO 3200',
254                                        6400.0: 'ISO 6400',
255                                        12800.0: 'ISO 12800',
256                                        25600.0: 'ISO 25600',
257                                        },
258                   'scale_type': 'manual line'
259                   }
260camera_params_3 = {'u_min': 0.1,
261                   'u_max': 10000.0,
262                   'function': lambda t: -10 * log10((1.0 / t) / (1.0 / 10.0)) - 30,
263                   'manual_axis_data': {1/10.0: '10',
264                                        1/7.0: '7',
265                                        1/5.0: '5',
266                                        1/3.0: '3',
267                                        1/2.0: '2',
268                                        1.0: '1',
269                                        2.0: '1/2',
270                                        3.0: '1/3',
271                                        5.0: '1/5',
272                                        7.0: '1/7',
273                                        10.0: '1/10',
274                                        20.0: '1/20',
275                                        30.0: '1/30',
276                                        50.0: '1/50',
277                                        70.0: '1/70',
278                                        100.0: '1/100',
279                                        200.0: '1/200',
280                                        300.0: '1/300',
281                                        500.0: '1/500',
282                                        700.0: '1/700',
283                                        1000.0: '1/1000',
284                                        2000.0: '1/2000',
285                                        3000.0: '1/3000',
286                                        5000.0: '1/5000',
287                                        7000.0: '1/7000',
288                                        10000.0: '1/10000',
289                             },
290                   'scale_type': 'manual line',
291                   'title': r't (s)',
292                   'text_format': r"1/%3.0f s",
293                   'tag': 'shutter',
294                   'tick_side': 'left',
295                   }
296camera_params_4 = {'u_min': 1.0,
297                   'u_max': 22.0,
298                   'function': lambda N: 10 * log10((N / 3.2)**2) + 30,
299                   'manual_axis_data': {1.0: '$f$/1',
300                                        1.2: '$f$/1.2',
301                                        1.4: '$f$/1.4',
302                                        1.7: '$f$/1.7',
303                                        2.0: '$f$/2',
304                                        2.4: '$f$/2.4',
305                                        2.8: '$f$/2.8',
306                                        3.3: '$f$/3.3',
307                                        4.0: '$f$/4',
308                                        4.8: '$f$/4.8',
309                                        5.6: '$f$/5.6',
310                                        6.7: '$f$/6.7',
311                                        8.0: '$f$/8',
312                                        9.5: '$f$/9.5',
313                                        11.0 :'$f$/11',
314                                        13.0 :'$f$/13',
315                                        16.0 :'$f$/16',
316                                        19.0 :'$f$/19',
317                                        22.0 :'$f$/22',
318                                        },
319                   'scale_type': 'manual line',
320                   'title': r'Aperture',
321                   }
322block_params_camera = {'block_type': 'type_3',
323                       'width': 10.0,
324                       'height': 10.0,
325                       'f_params': [camera_params_1, camera_params_2, camera_params_3,
326                                    camera_params_4],
327                       'mirror_x': True,
328                       'isopleth_values': [['x', 100.0, 'x', 4.0]],
329                       }
330
331
332def old_EV(EV):  # C2(EV100) in wiki
333    return (-EV + 13.654) / 0.3322
334
335EV_para = {'tag': 'EV',
336           'u_min': 4.0,
337           'u_max': 19.0,
338           'function': lambda u: old_EV(u),
339           'title': r'EV$_{100}$',
340           'tick_levels': 1,
341           'tick_text_levels': 1,
342           'align_func': old_EV,
343           'title_x_shift': 0.5,
344           'tick_side': 'right',
345           }
346EV_block = {'block_type': 'type_8',
347            'f_params': EV_para,
348            'isopleth_values': [['x']],
349            }
350# maximum focal length
351FL_t_para={'u_min': 0.1,
352           'u_max': 10000.0,
353           'function': lambda t:-10 * log10((1.0 / t) / (1.0 / 10.0)) - 30,
354           'scale_type': 'linear',
355           'tick_levels': 0,
356           'tick_text_levels': 0,
357           'title': r't (s)',
358           'text_format': r"1/%3.0f s",
359           'tag': 'shutter',
360           }
361FL_factor_params_2 = {'u_min': 1.0/4.0,
362                      'u_max': 3.0/2.0,
363                      'function': lambda factor: -10 * log10(factor / 10.0) + 0,
364                      'title': r'Sensor, IS',
365                      'scale_type': 'manual point',
366                      'manual_axis_data': {1.0/(2.0/3.0): 'DSLR',
367                                           1.0/(1.0): '35mm',
368                                           1.0/(8.0/3.0): 'DSLR IS',
369                                           1.0/(4.0): '35mm IS',
370                      },
371                      'tick_side':'left',
372                      'text_size_manual': text.size.footnotesize,  # pyx directive
373                      }
374FL_fl_params = {'u_min': 20.0,
375                'u_max': 1000.0,
376                'function': lambda FL:-10 * log10(FL) + 30,
377                'title': r'Max focal length',
378                'tick_levels': 3,
379                'tick_text_levels': 2,
380                'tick_side': 'left',
381                'scale_type': 'manual line',
382                'manual_axis_data': {20.0: '20mm',
383                                     35.0: '35mm',
384                                     50.0: '50mm',
385                                     80.0: '80mm',
386                                     100.0: '100mm',
387                                     150.0: '150mm',
388                                     200.0: '200mm',
389                                     300.0: '300mm',
390                                     400.0: '400mm',
391                                     500.0: '500mm',
392                                     1000.0: '1000mm'}
393                }
394
395FL_block_params = {'block_type': 'type_1',
396                   'width': 12.0,
397                   'height': 10.0,
398                   'f1_params': FL_t_para,
399                   'f2_params': FL_factor_params_2,
400                   'f3_params': FL_fl_params,
401                   'mirror_x': True,
402                   'proportion': 0.5,
403                   'isopleth_values': [['x', 1.0/(8.0/3.0), 'x']],
404                   }
405
406main_params = {'filename': ['ex_photo_exposure.pdf', 'ex_photo_exposure.eps'],
407               'paper_height': 35.0,
408               'paper_width': 35.0,
409               'block_params': [block_params, block_params_weather, block_params_scene,
410                                block_params_camera, EV_block, FL_block_params],
411               'transformations': [('rotate', 0.01), ('scale paper',)],
412               'title_x': 7,
413               'title_y': 34,
414               'title_box_width': 10,
415               'title_str': r'\LARGE Photography exposure (Setala 1940) \par \copyright Leif Roschier  2009 '
416              }
417Nomographer(main_params)