diff --git a/README b/README index d8dbff0..1d6cacd 100644 --- a/README +++ b/README @@ -10,12 +10,11 @@ Disclaimers: enough to get students started in the right direction, I stopped working on it. I accept no blame for any faults in the labs. -- I provided some input to the syllabus, but the final call was - always the lecturer, not me. These labs include some stuff - that I don't think is useful; they even include some stuff that - I think is actively harmful to students learning to program. - (e.g. pointers, excessive amounts of sine waves) - Again, I accept no fault for these. +"The Lecturer" has now rewritten a lot of the course with +Python and PyQt, although it's now been replaced by +a completely new course and back to C++ again, as it's for +electronics engineers who will be doing embedded stuff. +This can stay here in case it's of any use to anyone. This material is dual-licensed under: diff --git a/python/index.html b/python/index.html index 89eb5ad..3fdf911 100644 --- a/python/index.html +++ b/python/index.html @@ -25,7 +25,7 @@
+Unless otherwise noted, all materials on these pages +are licenced under a Creative +Commons Licence. +
+ + + + +Back to main + ++Python isn't the fastest language. It's certainly great +for rapid prototyping and cuts development time by a +great deal, but at run time it's slow, and can't easily make +use of multiple cores you find even in telephones, let +alone desktop workstations. C(++) on the other hand is +more difficult to develop, but offers potentially huge +speed advantages by being "close to the metal". +There will very probably come a time when at least the speed-critical +part of your Python needs to be faster, or you might just +want to call one of the function libraries developed in +C(++) over the last 4 decades. +
+ ++Python is written in C, so of course it's possible to +extend it by writing new functionality in C(++). +However, calling a C(++) function from Python or using +a C++ class as a Python one requires a lot of boiler-plate +code because of the differences between the languages. +Just think about them: +
+Python | C(++) | |
---|---|---|
Types | Dynamic | Static, Strict |
Allocation | On demand | Explict |
Memory Management | Garbage collection | RAII, explicit or end-of-scope deletion |
+Every call to a C(++) function from Python needs to take +these differences into consideration, and that makes for +a lot of tedious, error prone work to connect the two +together. Who allocates memory for the variables passed +between the Python and C(++) contexts? Who frees that memory? +How do we check that the right version of the C(++) function +is being called and that the types agree? +
+ ++Fortunately, Python is designed to make things as painless +as possible, to the extent that almost all of the boiler-plate +can be generated automatically by a Simple Wrapper Interface Generator +program. One of these programs is called (guess...) SWIG. +
+ ++Our game plan is as follows: +
+You'll aready know that you can compile a C++ program using the +GNU compiler with the following command +
++g++ -o output_file_name \ + input_file_names \ + -Iinclude_file_path \ + -Llibrary_path -llibrary ++
+and that unless you specify otherwise with the -o flag, +the program is written into a file called a.out +(assembler output). Of course, you can leave the flags you don't +need out, and the include file and library search paths will have +sensible defaults if the compiler has been properly installed. +
+ ++To work with SWIG you'll need to know +about these other compiler options: +
‑c | Compile only: don't link. + Produces an output file with the same name + but a .o extension (by default). |
---|---|
‑fPIC | Produce Position Independent Code, + i.e. code which may be loaded at any memory address + without modification, and is therefore suitable to + be used in a shared library. |
‑shared | Link the named object files + resolving as many references as possibe producing + a shared object library (extension .so). + |
+The python3-config
command is your friend.
+If you want to know which include files to use compile
+your SWIG wrapper, python3-config ‑‑includes
+will tell you.
+Type it at the prompt and see what it says.
+Try ldflags
instead of includes
.
+Remember you can get output of a command
+such as this included in the command you type using either
+` cmd `
or $( cmd )
.
+
+The SWIG documentation is very comprehensive, +although aimed at expert users. Check out the +Executive Summary. +SWIG works with many languages, not just Python, so you'll +find the tutorial useful +as well as the more detailed information about +SWIG and Python. +
++Write two C++ files. In one, write a function which prints +"Hello, " followed by its argument. Then write a test file +with a main function in it to make sure your C function behaves +as you wish it to behave. For example, here's the header file: +
++// File: simple_hello.h +// +// Print a greeting. +// + +#include <string> + +void hw(std::string who = std::string("World")); ++
+here's the implementation: +
++// File: simple_hello.cxx +// +// Implementation of the Hello function suite in C++ + +#include <iostream> +#include <string> + +#include "simple_hello.h" + +void hw(std::string who) +{ + std::cout << "Hello, " << who << std::endl; +} ++
+and here's the evaluation program: +
++// File: simple_hello_test.cxx + +#include <iostream> + +#include "simple_hello.h" + +using namespace std; + +int main(int argc, char *argv[]) +{ + cout << "Calling hw()\n"; + hw(); + cout << "Calling hw(argv[0])\n"; + hw(argv[0]); + return 0; +} ++ + +
export PATH=/local/anaconda/32/bin/:$PATH
+Compile this and test it. Maybe say
+g++ -o simple_hello simple_hello_test.cxx simple_hello.cxx
+
+./simple_hello
+Now we have to bind this to python. First write an interface file. +In very simple cases like this, you can instruct SWIG simply to +parse the C(++) header file and work out what's going on for itself. +Let's call our python module simple_hello. A suitable +interface file might contain: +
++/* File: simple_hello.i */ + +/* Name our python module */ +%module simple_hello + +/* We'll be using C++ standard library strings */ +%include <std_string.i> + +/* Put the literal code needed at the top of the output file */ +%{ +#define SWIG_FILE_WITH_INIT +#include "simple_hello.h" +%} + +/* Parse the c++ header file and generate the output file */ +%include "simple_hello.h" ++
Note that the awfully nice SWIG people have already provided +interface files for much of the C++ standard library, including +the C++ string type. We take the opportunity to put two +C preprocessor directives at the top of the wrapper file; +as we get more advanced, we can add extra python-specific +functionality to the wrapper, but for now these two lines are +all you need. They are simply copied into the output file. +We then parse our "library" header file and produce the +C++ code. +
+ +
+To generate the wrapper code for our simple_hello module
+you need to say
+swig -c++ -python simple_hello.i
+which tells swig to enable C++ processing, use Python as the
+target language, and to perform the actions required by the
+interface file. This will generate a file called simple_hello_wrap.cxx
+which you should feel free to look at. You should never have to edit
+this file (which is just as well!): you can change the interface
+file instead to modify it's behaviour.
+
+We're almost there now. We have to make position independent object
+files and link them into a shared library, then import that into a
+Python session. Here are the appropriate incantations:
+
+Note the leading underscore character on the filename of the
+shared object, and that I told the C++ compiler to look in
+Python's include directory for the necessary header files
+to connect with the Python interpreter. The include directory
+might be different on the machine you're using.
+
g++ -fPIC -c simple_hello.cxx
+g++ -fPIC -c simple_hello_wrap.cxx $( python3-config ‑‑includes )
+g++ -shared simple_hello_wrap.o simple_hello.o -o _simple_hello.so
+Sometimes you might need extra linkage flags. For example,
+MacOS requires a whole bunch of stuff to do with frameworks.
+You might try appending $( python3-config --ldflags )
+to the last command above if you get linkage errors. On a Mac, you'd
+also normally use the .dylib
extension instead
+of .so
, and say -dynamiclib
instead
+of -shared
.
+
‑‑std=c++0x
+ or ‑‑std=c++11
.
+ The C++ produced by SWIG relies on modern language features.
++You should have covered the use of build tools in an earlier +programming class. Once you've understood what's going on, +you might save yourself some time by writing a Makefile +so that you can run all of the necessary steps by simply +typing make. There is an example of how to construct a +make file in the appendix of the +MCM document. +
+ +
+The simple Hello World program
+passed a value to a C++ function. It didn't return
+any value, but returning simple built-in types
+(int
, float
, double
+etc) is handled trivially by SWIG and you can call the function
+entirely as expected, using the return value in Python as normal.
+
+An issue arises with C(++) types which are semantically
+ambiguous. In C (and C++, although it's frowned upon),
+strings are represented as null-terminated character arrays,
+and passed around the program as pointers to their
+first character. Indeed, the declaration
+char* s = "Hi";
+
is really just a shorthand for
+char* s = {'H', 'i', '\0'};
+
The C "string" doesn't even have a method to determine
+its length; it's just a pointer, not an object.
+So what is SWIG to do with the pointer-to-char type?
+There is a possibility that the C programmer really did
+mean a pointer-to-character and it isn't to be regarded
+as a "string".
+
+The solution that SWIG offers to this is the application +of "Typemaps". A Typemap tells SWIG to generate some specific +code when the variable maps a type template. Most specific +templates are considered first (and they can include the +name of the variable), so it's possible to specify how +a variable is to be treated on a case-by-case basis. +Writing the Typemap requires a fairly intimate knowledge +of SWIG and the Python run-time, but the numpy authors +have provided an interface file for their package, so we +are in the happy position of only ever having to use those +which we're given. That's not to say you shouldn't ever +think of writing your own Typemap. Just be prepared to +read the manual. +
+ ++Finally, it's worth noting that as well as functions, SWIG +will convert wrappers for C++ classes wholesale if required. +The wrapped class in the C++ appears as a regular Python +class, and all of its public methods are accessible from +Python. +
+ ++ To make use of the Typemaps provided to support + numpy nd-arrays through SWIG, it's necessary to + include in your interface file the instructions + to read numpy's interface file and initialise + the necessary array-support code. The first two + stanzas of the following code fragment achieve + this. +
++%include <numpy.i> + +%init %{ +import_array(); +%} + +%apply (double* IN_ARRAY1, int DIM1) { + (const double* in, std::size_t in_size) +}; +%apply (double* INPLACE_ARRAY1, int DIM1) { + (double* out, std::size_t out_size) +}; ++
+ The final two stanzas apply the numpy array Typemaps to
+ functions which have the named arguments and types. You
+ have to understand that numpy.i is designed to pass arrays
+ using the convention of supplying a pointer to the first
+ element and the length of the dimension.
+ The first %apply
instructs functions with the
+ formal parameter list (double* in, int in_size)
+ to be treated as if described in the Typemaps IN_ARRAY1
+ and DIM1. So supplying a single nd-array argument
+ in the Python call invokes the C function with two
+ arguments: a pointer to the first element and the size of
+ the first (only) dimension. To know that, you have to read
+ the manual.
+
+ numpy.i provides a choice of signiatures for arrays of up
+ to 4D. To pass a 2D array, for example, you might use the
+ signiature
+ (DATA_TYPE* IN_ARRAY2, int DIM1, int DIM2)
+ where DATA_TYPE can be one of the supported types (that includes
+ almost any simple type you can think of).
+
+ Notice that the IN_ARRAY can't be modified: the C(++) type
+ which it is applied to is represented by a const pointer.
+ If you want to modify the Python array with the C function,
+ you must use a INPLACE_ARRAY1, INPLACE_ARRAY2, etc.
+ Python often returns more than one value with calls like
+ a,b=myfunction(x)
, but C(++) usually achieves
+ this by having pointers (or references) passed to the function.
+ If your code already uses numpy, and the return values are all
+ of the same type, an INPLACE array might be used to return
+ them. There is also the ARGOUTVIEWM_ARRAYx template to allow
+ properly memory-managed views of public arrays in your C++:
+ check out the documentation. However, if you want to return
+ multiple values, you might ask yourself whether you shouldn't
+ do the Pythonic thing and return a tuple.
+
+First of all, provide the implementation for the +class described below. Then implement the resonator +you built in python using C++ from scratch. +
+ +export CPLUS_INCLUDE_PATH=/local/anaconda/32/lib/python3.5/site‑packages/numpy/core/include
+ +To illustrate the techniques involved, I've made a couple +of functions which calculate the minimum, maximum and sum +of an 1D numpy array of doubles. The calculation is done +in C++. +
+ +
+The functions are gratuitously wrapped in a class, for no
+other reason than to show the calling convention. They are
+static functions so they belong to the class and you don't
+have to instantiate an object to use them. mms_t
+calculates the maximum, minimum and sum and returns them
+as a tuple (actually a Python list). mms_a
+returns the values in a numpy array which the caller is
+responsible for creating.
+
+Here is the header file. +
+ ++// File: min_max_sum.h +// +// Given an array of doubles, return its minimum and +// maximum values and the sum of its contents in +// diverse and amusing ways. +// + +#include <cstddef> + +class MMS { +public: + // return the min, max and sum of the given array. + // SWIG will arrange for this to be returned as a tuple. + static void mms_t(const double* in, std::size_t in_size, + double* min, double* max, double* sum); + + // return the min, max and sum of the given numpy array 'in'. + // SWIG will write the values into the given numpy array, 'out'. + static void mms_a(double* out, std::size_t out_size, + const double* in, std::size_t in_size); +}; ++
+And here is the interface specification +
++/* File: min_max_sum.i */ + +/* Name our python module */ +%module min_max_sum + +/* Put the literal code needed at the top of the output file */ +%{ +#define SWIG_FILE_WITH_INIT +#include "min_max_sum.h" +%} + +/* The typemaps interface file lets us use the *OUTPUT Typemap. + This will enable us to use pointers to variables as return results. + If more than one *OUTPUT is matched, a tuple gets constructed. */ +%include <typemaps.i> + +/* Use the numpy interface for ndarrays. See the warning below */ +%include <numpy.i> + +%init %{ +import_array(); +%} + +/* Match the arguments of our various C++ methods */ +%apply (double* IN_ARRAY1, int DIM1) { (const double* in, std::size_t in_size) }; +%apply double *OUTPUT { double* min, double* max, double* sum }; +%apply (double* INPLACE_ARRAY1, int DIM1) { (double* out, std::size_t out_size) }; + +/* Parse the c++ header file and generate the output file */ +%include "min_max_sum.h" + ++
+Just to prove it works, here's a session running on Nick's laptop: +
++>>> from min_max_sum import MMS +>>> import numpy as np +>>> result = np.zeros(3) +>>> MMS.mms_t(np.array([1,4,3,7])) +[1.0, 7.0, 15.0] +>>> MMS.mms_a(result,np.array([1,4,3,7])) +>>> result +array([ 1., 7., 15.]) ++ +
+Unless otherwise noted, all materials on these pages +are licenced under a Creative +Commons Licence. +
+ + ++ diff --git a/python/labs/lab-swig.html b/python/labs/lab-swig.html new file mode 100644 index 0000000..f9465ef --- /dev/null +++ b/python/labs/lab-swig.html @@ -0,0 +1,628 @@ + + +
+
+ + +
+ diff --git a/python/labs/lab1.html b/python/labs/lab1.html index 0650893..368acfe 100644 --- a/python/labs/lab1.html +++ b/python/labs/lab1.html @@ -9,7 +9,7 @@ Back to main -
+
@@ -101,7 +101,7 @@
-print "hello world!" +print("hello world!")
@@ -126,9 +126,9 @@
# this is a comment -print "hello" +print("hello") -# print "this will not be printed" +# print("this will not be printed")
@@ -237,24 +237,24 @@
x = 3 -print "x is currently", x +print("x is currently", x) y = 4.001 -print "y is currently", y -print "Make that shorter: %.1f" % (y) -print "Make that longer: %.8f" % (y) +print("y is currently", y) +print("Make that shorter: {:.1f}".format(y)) +print("Make that longer: {:.8f}".format(y)) z = x/y z = x + z*y - y -print "Why does z = %i ?\n" % (z) +print("Why does z = {} ?\n".format(z)) animal_name = "cat" cute_name = "kitty" combo = cute_name + animal_name -print "We can do \"math\" on strings: %s" % (combo) +print("We can do \"math\" on strings: {}".format(combo)) -print "We can print multiple", -print "variables: %i %f %i %s\n" % (x, y, z, cute_name) +print("We can print multiple") +print("variables: {} {} {} {}\n".format(x, y, z, cute_name))
print "Enter an animal name:" animal_name = raw_input() -print "You typed: %s" % (animal_name) +print("You typed: ", animal_name)
@@ -365,7 +365,7 @@
-print "Enter your age (as an integer):" +print("Enter your age (as an integer):") user_input = raw_input() age = int(user_input) @@ -373,7 +373,7 @@Technical details
user_input = raw_input() weight = float(user_input) -print "You typed: %i, %f" % (age, weight) +print("You typed: {}, {}".format(age, weight))
@@ -106,7 +106,7 @@
while True: - print "Infinite loop is infinite" + print("Infinite loop is infinite")
@@ -230,7 +230,7 @@
i = 0 while i < 10: - print "The loop has repeated", i, "times." + print("Looped {} times.".format(i)) i = i + 1@@ -325,7 +325,7 @@
for i in range(10): - print "The loop has repeated", i, "times." + print("For the {} time.".format(i))
@@ -334,10 +334,10 @@
for i in range(5, 15): - print "What is this loop doing? i is", i + print("What is this doing? i is", i) for i in range(20, 2, -2): - print "What is this loop doing? i is", i + print("What is this doing? i is", i)
An array is an ordered sequence of values; you cannot -rearrange values without changing the meaning). A two-dimensional +rearrange values without changing the meaning. A two-dimensional array is just a normal "table".
@@ -74,7 +75,7 @@@@ -197,7 +198,7 @@
@@ -345,7 +346,7 @@
@@ -367,7 +368,7 @@
@@ -386,13 +387,31 @@
+What objects do you need if you want to write a program to make +a tone? +
++Sometimes you need to generate a sound in small blocks, +for example in real-time response to some input like a +MIDI-keyboard or an event in a video game. Does this change +your design? +
+-Calling sin(x) requires a lot of CPU power (relatively -speaking). Implementing trig functions in hardware takes a lot of -silicon. Can we reduce this burden? -
- --Yes, and the answer is a look-up table. Before starting our -program, we pre-calculate one full cycle of a sine wave. We store -that in memory, and then when we want to calculate sin(x) -during our program, we look at the table in memory instead of -calculating it fresh each time. When dealing with silicon, the -look-up table can be written in ROM (read-only memory) instead of -calculated. -
- --There are some notes here: mcm.pdf. -Skip over chapter 1, and read chapter 2 up to and including -section 2.3 Determining the Table Length. A print-out of -this material will be handed out in class. -
- --Answer these questions, assuming you are creating CD-quality audio -(44100 Hz, 16-bit samples, stereo channels). -
- --f(x) = sin(x) -g(x) = sin(2*x) -- -Suppose you wanted to create a look-up table for g(x). How large -should that table be? - -
-- -Assume that you are producing audio at 10 hz (yes, ridiculously -low!). - --
-- Index 0 1 2 ... - Value 0 0.588 0.951 ...
-Let the user specify a frequency, then generate a sine wave at -that frequency -
- --Storing a full sine wave in ROM requires a bunch of silicon. Can -we reduce this burden? -
- --Yes, and the answer is a partial look-up table. In particular, a -sine wave can be re-created using only one quarter of a cycle; the -other three quarters are simple the original quarter flipped -across the y axis and/or mirrored around the x axis. -
- --There are some notes here: mcm.pdf. -Skip over chapter 1, and read chapter 2 up to and including -section 2.4 Shortening the Lookup Table. A print-out of -this material will be handed out in class. -
- --- -Assume that you are producing audio at 10 hz (yes, ridiculously -low!). - --
-- Index 0 1 2 ... - Value 0 0.156 0.309 ...
-Let the user specify a frequency, then generate a sine wave at -that frequency -
- --Unless otherwise noted, all materials on these pages -are licenced under a Creative -Commons Licence. -
- - - - diff --git a/python/labs/lab5.xhtml b/python/labs/lab5.xhtml index ba70fd8..61d77b7 100644 --- a/python/labs/lab5.xhtml +++ b/python/labs/lab5.xhtml @@ -17,193 +17,14 @@ Exercises:-As well as programming a large computer for -audio output, it's often useful to be able to -minimise the time and storage spent generating -a sine-wave. Some devices contain very low -power processors but might still need to -implement audio-frequency oscillators; others -may need to generate sine-waves at very high -frequency sample rates. It is now possible to -build FM receivers using software-only IF -processing (at 10.7MHz) on a relatively modest -dsp chip. -
- --The size of the look-up table, even in its -quarter-cycle version, would be regarded by -hardware designers as a profligate waste of -memory, when a simple piece of trig can cut -the storage requirement dramatically. -
- -
-sin(A+B) = sinAcosB
- + cosAsinB
-sin(x) ≈ x;
-cos(x) ≈ 1 - x2/2
-(for small x).
-
-Here's the clever bit: let's store a smaller -number of entries in the look-up table, and use -the identity above to interpolate between them. -So A will be a "big" angle (cos and -sine obtained from the table) -and B will be the difference -between the value which can be obtained by the -table lookup and the angle we need. B is a "small" -angle, so the approximations can be used to -calculate the final value. -
- --Do the Maths Homework to find out how much of the -table can be thrown away without compromising -the accuracy of the result. -
- --Before investing effort in coding up an interpolated -solution, we'd better find out exactly how much storage -can be saved from the look-up table. -
- --You can find the roots of (most) -differentiable polynomials -iteratively using Newton's method. (Hint: it's easy -to code in Python if you get bored using a calculator) -
- - --Modify the oscillator class you have already -written so that it uses a smaller -look-up table and interpolation. -
- -As described in Chapter 2 of mcm.pdf +href="http://markov.music.gla.ac.uk/AP_Sandpit/mcm.pdf">mcm.pdf the position of poles and zeros in Laplacian space characterises a system response. Poles on the left half of the s-plane represent a damped @@ -521,7 +342,7 @@ previous two outputs. This all sounds quite daunting, but it isn't hard after you've worked through what's happening once. Read Chapter 2 of mcm.pdf for more detail. +href="http://markov.music.gla.ac.uk/AP_Sandpit/mcm.pdf">mcm.pdf for more detail.
In this short exercise, we'll practice writing classes @@ -84,7 +84,7 @@ comparison.
The design process for a digital filter based on one of the "classical" polynomials is covered in detail in the -Making Computer Music PDF file. In (extreme) summary, +Making Computer Music PDF file. In (extreme) summary,
+Research the literature and present a short (15-minute) presentation on a +Music Technology topic of your choice, to be presented at the Science and Music +Research Group Seminar on Issues in Music Technology. +
++If you have agreed your on topic for Exercise 21 already, you may present +on that. You may not present on any of the given topics for Exercise 21. +
++You can use moodle, web sites, html files with links in a browser, +demonstrations etc., but the use of +Powerpoint or any similar software product designed to make vacuous +executives look important or knowledgeable will result in instant failure +and possible disciplinary proceedings. +
+