Тут показані розбіжності між вибраною ревізією та поточною версією сторінки.
Both sides previous revision Попередня ревізія Наступна ревізія | Попередня ревізія Остання ревізія Both sides next revision | ||
sage [2014/01/03 16:30] wombat [Sage через прокси] опечатка |
sage [2016/10/25 12:06] wombat [Обновление] опечатка |
||
---|---|---|---|
Рядок 1: | Рядок 1: | ||
+ | ====== Sage ====== | ||
+ | Приёмы работы и некоторые готовые рецепты описаны [[sage_tricks | здесь]]. | ||
+ | |||
+ | ====== Установка ====== | ||
+ | |||
+ | ===== На своём компьютере ===== | ||
+ | |||
+ | ==== Перед установкой ==== | ||
+ | |||
+ | Поскольку при запуске и использовании Sage на одном и том же (своём) компьютере нет нужды заботиться ни о шифровании передаваемых данных, ни о доступе к удалённой консоли, в которой происходит сборка, практически никакие дополнительные программы не нужны. | ||
+ | |||
+ | Для Debian GNU/Linux и Ubuntu | ||
+ | |||
+ | sudo apt-get install libpng12-dev | ||
+ | |||
+ | //libpng// нужно для экспорта изображений в PNG-файлы в R. | ||
+ | |||
+ | Создадим каталог, в который будем устанавливать Sage. | ||
+ | |||
+ | mkdir /home/username/sage | ||
+ | |||
+ | Далее продолжить согласно разделу "Обновление" | ||
+ | |||
+ | |||
+ | ==== Обновление ==== | ||
+ | |||
+ | **1.** Эвакуируем папку, в которой хранятся все рабочие листы нашего пользователя (///home/username/.sage//): | ||
+ | |||
+ | cd archive_catalog | ||
+ | tar -cvJf sage_home_backup-20120918.tar.xz /home/username/.sage | ||
+ | |||
+ | Вместо даты ''20120918'', указанной в примере, разумно указывать текущую дату в формате "ГГГГММДД". ''archive_catalog'' -- каталог, в котором будет создана резервная копия. | ||
+ | |||
+ | **2.** Проверяем, достаточно ли свободного места на диске (потребуется около 3,5 ГБ). | ||
+ | |||
+ | **3.** Загружаем архив свежей версии Sage с одного из [[http://sagemath.org/download-source.html|зеркал]] и сравниваем контрольную сумму с указанной на сайте зеркала. | ||
+ | |||
+ | Например, с http://mirror.yandex.ru/mirrors/sage.math.washington.edu/src/index.html или http://sage.igor.onlinedirect.bg/src/index.html | ||
+ | |||
+ | cd /home/username/sage | ||
+ | wget -c http://sage.igor.onlinedirect.bg/src/sage-6.4.1.tar.gz | ||
+ | md5sum sage-6.4.1.tar.gz | ||
+ | |||
+ | **4.** Распаковываем архив с исходниками: | ||
+ | |||
+ | tar -xvf sage-6.4.1.tar.gz | ||
+ | |||
+ | где ''sage-6.4.1.tar.gz'' заменить на имя скачанного файла. | ||
+ | |||
+ | **5.** Переходим в распакованный каталог: | ||
+ | |||
+ | cd sage-6.4.1/ | ||
+ | |||
+ | Для компиляции в 4 потока выполняем: | ||
+ | export MAKE="make -j4" | ||
+ | |||
+ | **6.** Запускаем сборку: | ||
+ | |||
+ | make | ||
+ | |||
+ | В случае использования пакетов, не входящих в комплект поставки Sage, после обновления самого Sage, необходимо обновить также и [[sage#Дополнительные пакеты|дополнительные пакеты]]. | ||
+ | |||
+ | ===== Дополнительные пакеты ===== | ||
+ | |||
+ | |||
+ | Если я правильно понимаю, ключевой приём такой: | ||
+ | sage -python setup.py install | ||
+ | |||
+ | Т.е. скачиваем пакет, распаковываем его куда-то. Заходим в это "куда-то", но там вместо стандартного "''python setup.py install''" запускаем то, что написано выше. Хотя, перед этим лучше прочесть README или INSTALL в самом архиве пакета, понимая, что когда попадётся там команда типа приведённой выше, нужно вместо "''python''" подставить "''sage -python''". | ||
+ | |||
+ | ==== Пакеты R ==== | ||
+ | |||
+ | sage: r.install_packages('tseriesChaos') | ||
+ | |||
+ | ==== mlpy ==== | ||
+ | |||
+ | http://mlpy.sourceforge.net/ | ||
+ | |||
+ | wget 'http://downloads.sourceforge.net/project/mlpy/mlpy 3.5.0/mlpy-3.5.0.tar.gz' | ||
+ | tar xvf mlpy-3.5.0.tar.gz | ||
+ | |||
+ | В файле документации к этому пакету //docs/source/install.txt// написано так: | ||
+ | |||
+ | $ python setup.py install | ||
+ | |||
+ | Стало быть, нужно делать так: | ||
+ | |||
+ | # sage -python setup.py install | ||
+ | |||
+ | ==== spectrum ==== | ||
+ | |||
+ | Для установки пакета spectrum потребовалось предварительно установить пакет easydev: | ||
+ | |||
+ | wget -cS 'https://pypi.python.org/packages/source/e/easydev/easydev-0.6.12.tar.gz' | ||
+ | tar xvf easydev-0.6.12.tar.gz | ||
+ | cd easydev-0.6.12/ | ||
+ | /opt/sage/sage -python setup.py install | ||
+ | |||
+ | wget -cS 'https://pypi.python.org/packages/source/s/spectrum/spectrum-0.5.6.tar.gz' | ||
+ | tar xvf spectrum-0.5.6.tar.gz | ||
+ | cd spectrum-0.5.6 | ||
+ | /opt/sage/sage -python setup.py install | ||
+ | |||
+ | Для пакетов, находящихся в PyPI, существует и такой способ установки: | ||
+ | |||
+ | /opt/sage/sage --python -m easy_install <имя_пакета> | ||
+ | |||
+ | Однако, этот способ не заработал даже после установки pyopenssl: | ||
+ | |||
+ | /opt/sage/sage -i pyopenssl | ||
+ | |||
+ | |||
+ | |||
+ | ===== На удалённом сервере ===== | ||
+ | |||
+ | Установка на сервер отличается от установки на локальный компьютер тем, что: | ||
+ | * Sage обычно устанавливается не в домашний каталог отдельного пользователя, а в //системный каталог//; | ||
+ | * запускается такой сервер также не от имени какого-либо реального пользователя, а от имени некоторого системного, специально созданного //виртуального пользователя//; | ||
+ | * от такого сервера обычно требуется //автоматический запуск//; | ||
+ | * для безопасного доступа к такому серверу, необходима //поддержка протокола HTTPS//; | ||
+ | * поскольку процесс компиляции может быть длительным, крайне желательно //работать через программу// ''screen''. | ||
+ | |||
+ | ==== Перед установкой ==== | ||
+ | |||
+ | Установим необходимые дополнительные библиотеки: | ||
+ | |||
+ | Для Debian GNU/Linux и Ubuntu | ||
+ | |||
+ | sudo apt-get install openssl libssl-dev libpng12-dev screen | ||
+ | |||
+ | //OpenSSL// нужен для опции "secure" Sage Notebook'а. Работа по протоколу HTTPS. | ||
+ | |||
+ | //libpng// нужно для R. Экспорт в PNG-файлы. | ||
+ | |||
+ | //screen// удобен для выполнения длительных операций на удалённом сервере, например, по SSH. | ||
+ | |||
+ | Создадим группу и пользователя, от имени которых в дальнейшем будет запускаться Sage-сервер: | ||
+ | |||
+ | groupadd sagenb | ||
+ | useradd -d /home/sagenb -g sagenb -m -s /usr/sbin/nologin sagenb | ||
+ | |||
+ | Создадим каталог для общесистемной установки Sage: | ||
+ | |||
+ | sudo mkdir /opt/sage | ||
+ | |||
+ | Далее выполняем то, что описано в разделе [[sage#Обновление1 |"Обновление"]]. | ||
+ | |||
+ | ==== Обновление ==== | ||
+ | |||
+ | В случае подключения к удалённому компьютеру по SSH, после подключения запускаем | ||
+ | |||
+ | screen | ||
+ | |||
+ | и все дальнейшие команды выполняем в его командной строке. | ||
+ | |||
+ | **1.** Эвакуируем папку ''/home/sagenb/.sage'' | ||
+ | |||
+ | cd archive_catalog | ||
+ | tar -cvJf sage_home_backup-20120918.tar.xz /home/sagenb/.sage | ||
+ | |||
+ | Вместо даты ''20120918'', указанной в примере, разумно указывать текущую дату в формате "ГГГГММДД". 'эarchive_catalogэ' -- каталог, в котором будет создана резервная копия. | ||
+ | |||
+ | **2.** Проверяем, достаточно ли свободного места на диске (потребуется около 3,5 ГБ). | ||
+ | |||
+ | **3.** Загружаем архив свежей версии Sage с одного из [[http://sagemath.org/download-source.html|зеркал]]. | ||
+ | |||
+ | Например, с http://mirror.yandex.ru/mirrors/sage.math.washington.edu/src/index.html или http://sage.igor.onlinedirect.bg/src/index.html и сравниваем контрольную сумму с указанной на сайте зеркала. | ||
+ | |||
+ | wget -c http://sage.igor.onlinedirect.bg/src/sage-6.4.1.tar.gz | ||
+ | md5sum sage-6.4.1.tar.gz | ||
+ | |||
+ | **4.** Распаковываем архив с исходниками: | ||
+ | |||
+ | cd /opt/sage | ||
+ | sudo tar -xvf /path/to/downloaded/sage-6.4.1.tar.gz | ||
+ | |||
+ | где ''sage-6.4.1.tar.gz'' заменить на имя скачанного файла. | ||
+ | |||
+ | **5.** Переходим в распакованный каталог: | ||
+ | |||
+ | cd sage-5.5/ | ||
+ | |||
+ | Для компиляции в 4 потока выполняем: | ||
+ | export MAKE="make -j4" | ||
+ | |||
+ | **6.** Запускаем сборку: | ||
+ | |||
+ | make | ||
+ | или | ||
+ | make ssl | ||
+ | для сборки с поддержкой OpenSSL | ||
+ | |||
+ | **7.** Если всё это выполняется в GNU Screen на удалённом компьютере, то "отсоединяем" консоль Screen от текущего экрана последовательным нажатием клавиш: | ||
+ | |||
+ | Ctrl+a d | ||
+ | Чтобы потом снова подключиться к запущенной сессии screen, нужно выполнить | ||
+ | screen -r | ||
+ | В случае, если произошло "аварийное" отключение от сессии screen, его нужно сперва отключить от несуществующего более экрана, а затем подключить к нашему: | ||
+ | screen -dr | ||
+ | |||
+ | **8.** Для удобства общесистемного использования Sage, удобно создать ссылку на его исполнимый файл в одном из каталогов, перечисленных в переменной окружения ''PATH''. Обычно подходит каталог ''/usr/local/bin/''. После успешной сборки Sage мы видим такое сообщение: | ||
+ | |||
+ | You *should* be able to move the sage-x.y.z/ directory anywhere you | ||
+ | want. If you copy the sage script or make a symbolic link to it, you | ||
+ | should modify the script to reflect this (as instructed at the top of | ||
+ | the script). It is best if the path to Sage does not have any spaces in | ||
+ | it. | ||
+ | |||
+ | Создаём символическую ссылку на исполнимый файл Sage: | ||
+ | |||
+ | ln -fs /opt/sage-5.5/sage /usr/local/bin/sage | ||
+ | |||
+ | В случае использования пакетов, не входящих в комплект поставки Sage, после обновления Sage-сервера, необходимо обновить также и [[sage#Дополнительные пакеты|дополнительные пакеты]]. | ||
+ | |||
+ | |||
+ | ==== Автозапуск ==== | ||
+ | |||
+ | Ниже приводится пример построения сценария автозапуска Sage-сервера для Debian или Ubuntu. | ||
+ | |||
+ | Если Sage компилировали **без поддержки //OpenSSL//**, то опцию "secure" нужно выключить. | ||
+ | |||
+ | DAEMON_ARGS='--notebook=sagenb interface=192.168.1.101 port=8000 secure=False automatic_login=False' | ||
+ | | ||
+ | в противном случае предпочтительно оставить её включённой: | ||
+ | |||
+ | DAEMON_ARGS='--notebook=sagenb interface=192.168.1.101 port=8000 secure=True automatic_login=False' | ||
+ | | ||
+ | Здесь ''192.168.1.101'' -- сетевой адрес компьютера, на котором будет работать Sage Notebook сервер. Если оставить строку адреса пустой, т.е. | ||
+ | |||
+ | DAEMON_ARGS='--notebook=sagenb interface= port=8000 secure=True automatic_login=False' | ||
+ | | ||
+ | то Sage Notebook сервер должен работать на всех сетевых интерфейсах компьютера. | ||
+ | |||
+ | <file bash sage> | ||
+ | #! /bin/sh | ||
+ | ### BEGIN INIT INFO | ||
+ | # Provides: skeleton | ||
+ | # Required-Start: $remote_fs $syslog | ||
+ | # Required-Stop: $remote_fs $syslog | ||
+ | # Default-Start: 2 3 4 5 | ||
+ | # Default-Stop: 0 1 6 | ||
+ | # Short-Description: Example initscript | ||
+ | # Description: This file should be used to construct scripts to be | ||
+ | # placed in /etc/init.d. | ||
+ | ### END INIT INFO | ||
+ | |||
+ | # Author: Foo Bar <foobar@baz.org> | ||
+ | # | ||
+ | # Please remove the "Author" lines above and replace them | ||
+ | # with your own name if you copy and modify this script. | ||
+ | |||
+ | # Do NOT "set -e" | ||
+ | |||
+ | # PATH should only include /usr/* if it runs after the mountnfs.sh script | ||
+ | PATH=/sbin:/usr/sbin:/bin:/usr/bin | ||
+ | DESC="sage server at port 8000" | ||
+ | NAME=sage | ||
+ | DAEMON=/usr/local/bin/$NAME | ||
+ | ###DAEMON=/usr/sbin/$NAME | ||
+ | ##DAEMON_ARGS="--options args" | ||
+ | |||
+ | # Listen on localhost only. Useful for accessing through the HTTP proxy on the same host. | ||
+ | #DAEMON_ARGS='--notebook=sagenb interface=localhost port=8000 secure=False automatic_login=False' | ||
+ | # | ||
+ | # Listen on all interfaces with SSL encryption | ||
+ | DAEMON_ARGS='--notebook=sagenb interface= port=8000 secure=True automatic_login=False' | ||
+ | # | ||
+ | # Listen on all interfaces without SSL (DANGEROUS!!!). For testing purposes only. | ||
+ | #DAEMON_ARGS='--notebook=sagenb interface= port=8000 secure=False automatic_login=False' | ||
+ | |||
+ | USERNAME="sagenb" | ||
+ | GROUPNAME="sagenb" | ||
+ | |||
+ | PIDFILE=/var/run/$NAME.pid | ||
+ | SCRIPTNAME=/etc/init.d/$NAME | ||
+ | |||
+ | # Exit if the package is not installed | ||
+ | [ -x "$DAEMON" ] || exit 0 | ||
+ | |||
+ | # Read configuration variable file if it is present | ||
+ | |||
+ | # [ -r /etc/default/$NAME ] && . /etc/default/$NAME | ||
+ | |||
+ | # Load the VERBOSE setting and other rcS variables | ||
+ | . /lib/init/vars.sh | ||
+ | |||
+ | # Define LSB log_* functions. | ||
+ | # Depend on lsb-base (>= 3.0-6) to ensure that this file is present. | ||
+ | . /lib/lsb/init-functions | ||
+ | |||
+ | # | ||
+ | # Function that starts the daemon/service | ||
+ | # | ||
+ | do_start() | ||
+ | { | ||
+ | # Return | ||
+ | # 0 if daemon has been started | ||
+ | # 1 if daemon was already running | ||
+ | # 2 if daemon could not be started | ||
+ | start-stop-daemon --start -c sagenb:sagenb --pidfile $PIDFILE --make-pidfile --exec $DAEMON --test > /dev/null \ | ||
+ | || return 1 | ||
+ | start-stop-daemon --start -c sagenb:sagenb --pidfile $PIDFILE --make-pidfile --background --exec $DAEMON -- \ | ||
+ | $DAEMON_ARGS \ | ||
+ | || return 2 | ||
+ | # Add code here, if necessary, that waits for the process to be ready | ||
+ | # to handle requests from services started subsequently which depend | ||
+ | # on this one. As a last resort, sleep for some time. | ||
+ | } | ||
+ | |||
+ | # | ||
+ | # Function that stops the daemon/service | ||
+ | # | ||
+ | do_stop() | ||
+ | { | ||
+ | # Return | ||
+ | # 0 if daemon has been stopped | ||
+ | # 1 if daemon was already stopped | ||
+ | # 2 if daemon could not be stopped | ||
+ | # other if a failure occurred | ||
+ | start-stop-daemon --stop --quiet --user $USERNAME --retry=TERM/30/KILL/5 | ||
+ | RETVAL="$?" | ||
+ | [ "$RETVAL" = 2 ] && return 2 | ||
+ | # Wait for children to finish too if this is a daemon that forks | ||
+ | # and if the daemon is only ever run from this initscript. | ||
+ | # If the above conditions are not satisfied then add some other code | ||
+ | # that waits for the process to drop all resources that could be | ||
+ | # needed by services started subsequently. A last resort is to | ||
+ | # sleep for some time. | ||
+ | start-stop-daemon --stop --quiet --user $USERNAME --oknodo --retry=0/30/KILL/5 --exec $DAEMON | ||
+ | [ "$?" = 2 ] && return 2 | ||
+ | # Many daemons don't delete their pidfiles when they exit. | ||
+ | rm -f $PIDFILE | ||
+ | return "$RETVAL" | ||
+ | } | ||
+ | |||
+ | # | ||
+ | # Function that sends a SIGHUP to the daemon/service | ||
+ | # | ||
+ | do_reload() { | ||
+ | # | ||
+ | # If the daemon can reload its configuration without | ||
+ | # restarting (for example, when it is sent a SIGHUP), | ||
+ | # then implement that here. | ||
+ | # | ||
+ | start-stop-daemon --stop --signal 1 --quiet --user $USERNAME:$GROUPNAME --pidfile $PIDFILE --name $NAME | ||
+ | return 0 | ||
+ | } | ||
+ | |||
+ | case "$1" in | ||
+ | start) | ||
+ | [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" | ||
+ | do_start | ||
+ | case "$?" in | ||
+ | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; | ||
+ | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; | ||
+ | esac | ||
+ | ;; | ||
+ | stop) | ||
+ | [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" | ||
+ | do_stop | ||
+ | case "$?" in | ||
+ | 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; | ||
+ | 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; | ||
+ | esac | ||
+ | ;; | ||
+ | status) | ||
+ | status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? | ||
+ | ;; | ||
+ | #reload|force-reload) | ||
+ | # | ||
+ | # If do_reload() is not implemented then leave this commented out | ||
+ | # and leave 'force-reload' as an alias for 'restart'. | ||
+ | # | ||
+ | #log_daemon_msg "Reloading $DESC" "$NAME" | ||
+ | #do_reload | ||
+ | #log_end_msg $? | ||
+ | #;; | ||
+ | restart|force-reload) | ||
+ | # | ||
+ | # If the "reload" option is implemented then remove the | ||
+ | # 'force-reload' alias | ||
+ | # | ||
+ | log_daemon_msg "Restarting $DESC" "$NAME" | ||
+ | do_stop | ||
+ | case "$?" in | ||
+ | 0|1) | ||
+ | do_start | ||
+ | case "$?" in | ||
+ | 0) log_end_msg 0 ;; | ||
+ | 1) log_end_msg 1 ;; # Old process is still running | ||
+ | *) log_end_msg 1 ;; # Failed to start | ||
+ | esac | ||
+ | ;; | ||
+ | *) | ||
+ | # Failed to stop | ||
+ | log_end_msg 1 | ||
+ | ;; | ||
+ | esac | ||
+ | ;; | ||
+ | *) | ||
+ | #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 | ||
+ | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 | ||
+ | exit 3 | ||
+ | ;; | ||
+ | esac | ||
+ | |||
+ | : | ||
+ | |||
+ | </file> | ||
+ | |||
+ | ==== После установки/обновления ==== | ||
+ | |||
+ | /etc/init.d/sage restart | ||
+ | |||
+ | ==== Sage через прокси ==== | ||
+ | |||
+ | Чтобы иметь доступ к Sage-серверу с компьютеров, которым не разрешены соединения в нестандартные порты (например, в 8000), можно настроить прокси-сервер, который будет ожидать соединения на 80-м порту (в случае HTTP) или на 443-м порту (в случае HTTPS) и перенаправлять их запущенному Sage-серверу. | ||
+ | |||
+ | Создадим для этого SSL-сертификат сервера((Самоподписанный сертификат по умолчанию не считается доверенным большинством программ, поэтому они будут выдавать запрос на его подтверждение. Можно получить доверенный SSL-сертификат для своего домена бесплатно -- см. [[https://letsencrypt.readthedocs.org/en/latest/intro.html | Let’s Encrypt ]])): | ||
+ | |||
+ | openssl req -new -x509 -days 365 -nodes -out "sagenb.wombat.org.ua.crt" -keyout "sagenb.wombat.org.ua.key" | ||
+ | |||
+ | Полученные файлы поместим в ''/etc/sagenb/certs/'' | ||
+ | |||
+ | === Apache === | ||
+ | |||
+ | При использовании веб-сервера Apache, необходимо в его файле конфигурации указать следующее: | ||
+ | |||
+ | <file conf /etc/apache2.conf> | ||
+ | <VirtualHost *:80> | ||
+ | ServerName sagenb.wombat.org.ua | ||
+ | Redirect permanent / https://sagenb.wombat.org.ua/ | ||
+ | </VirtualHost> | ||
+ | |||
+ | <VirtualHost *:443> | ||
+ | ServerName sagenb.wombat.org.ua | ||
+ | |||
+ | ProxyRequests off | ||
+ | # SSLProxyEngine On | ||
+ | SSLEngine on | ||
+ | SSLCertificateFile "/etc/sagenb/certs/sagenb.wombat.org.ua.crt" | ||
+ | SSLCertificateKeyFile "/etc/sagenb/certs/sagenb.wombat.org.ua.key | ||
+ | |||
+ | <Proxy *> | ||
+ | Order deny,allow | ||
+ | Allow from all | ||
+ | </Proxy> | ||
+ | ProxyPass / http://localhost:8000/ | ||
+ | ProxyPassReverse / http://localhost:8000/ | ||
+ | ProxyPreserveHost on | ||
+ | </VirtualHost> | ||
+ | </file> | ||
+ | |||
+ | Также нужно убедиться, что по умолчанию загружаются модули ''mod_proxy'', ''mod_ssl'' и ''mod_proxy_http''. | ||
+ | |||
+ | ====== Использование Sage ====== | ||
+ | |||
+ | == Sage Tutorial == | ||
+ | |||
+ | [[http://sagemath.org/doc/tutorial/index.html | Sage Tutorial ]] | ||
+ | |||
+ | [[http://freetonik.com/sage/tutorial/ | Русская версия учебного пособия Sage]] | ||
+ | |||
+ | == SDSU Sage Tutorial == | ||
+ | |||
+ | [[http://www-rohan.sdsu.edu/~mosulliv/Teaching/sdsu-sage-tutorial/ | SDSU Sage Tutorial]] | ||
+ | |||
+ | == Books == | ||
+ | |||
+ | [[http://www.gregorybard.com/sage_for_undergraduates_color.pdf.zip | Sage for Undergraduates]] | ||
+ | |||
+ | [[https://www.packtpub.com/hardware-and-creative/sage-beginners-guide | Sage Beginner's Guide by Craig Finch]] | ||
+ | |||
+ | |||
+ | == wikisage == | ||
+ | |||
+ | [[http://wikisage.ru/ | wikisage.ru]] | ||
+ | |||
+ | |||
+ | == William Stein courses == | ||
+ | |||
+ | [[http://wstein.org/courses/]] |