Manually building manylinux python wheels

After having achived to build the windows wheels for pyLARDA on anacoda, I tried to tackle the linux version. There it’s a bit more complicated, as the wheels are kind of distribution dependent (and are not accepted by pypi).

auditwheel (https://github.com/pypa/auditwheel) allows to put the external shared libraries into the wheel itself, however the have to be compiled with a rather ‘old’ version to be cross-platform compatible.

The manylinux project (https://github.com/pypa/manylinux) providing docker images to simplify this step:

docker pull quay.io/pypa/manylinux2014_x86_64
docker images #to get the id

The source code needs to be available somewhere in the current path, afterwards fire up docker:

docker run -it -v $(pwd):/io 90ac8ec

Then inside the docker container:

cd io/larda
PYBIN=/opt/python/cp38-cp38/bin/
# pyLARDA requires cython as a prerequisite
${PYBIN}/pip install cython
${PYBIN}/pip install .
${PYBIN}/python -m pyLARDA
${PYBIN}/python setup.py bdist_wheel
cd dist/
auditwheel show pyLARDA-3.3.2-cp38-cp38-linux_x86_64.whl
auditwheel repair pyLARDA-3.3.2-cp38-cp38-linux_x86_64.whl

Now the manylinux wheels should be available in the dist/wheelhouse/ directory.

 

 

 

Flexpart 10.4 and GFSv16

My latest encounter with joys of input format change: With the change to GFSv16 in March 2021 additional height levels were added to the grib output. Especially heights below 1hPa. What is generally good news, is bad news for Flexpart 10.4 (as of 20 Nov 2019). The additional levels cause incompatability with the readwinds_gfs.f90. Public information on this issue is rare. There is an Issue in Flexparts ticketing system (https://www.flexpart.eu/ticket/305) but thats more related to the mean-wind trajectory module Flextra.

One suggestion is to drop the additional variables with wgrib2, which is somehow dissatisfying (and I could not figure it out on the short run). The only alternative posed ist, to switch to the dev branch (The link in the issue points to a version 9.3.2 legacy branch). So, for 10.4 dev you want to pull git clone https://www.flexpart.eu/gitmob/flexpart --branch dev --single-branch. With that version, Flexpart switches from grib_api to eccodes, so on ubuntu the libraries apt-get install libeccodes-tools libeccodes-dev are required.

At least for my docker setup, i needed to adapt the makefile:

INCPATH1 = /usr/include
INCPATH2 = /usr/include
LIBPATH1 = /usr/lib
F90 = gfortran

Then, compiling worked sucessfully and the binary could handle the new GFS files.

Slightly related: natural earth cartopy downloads

Seems like the default natural earth mirror is temporarily unavailable (e.g., http://naciscdn.org/naturalearth/110m/physical), but there is this fix: https://github.com/SciTools/cartopy/issues/1298#issuecomment-920843582

 

Schnee und Eis soweit das Auge reicht

Hier noch ein kleiner Bericht, den ich ursprünglich für die Vereinszeitung des MV Hof geschrieben habe (Palaverkist, Ausgabe 172, April 2021).

Tromsø, 27. Januar 2020. Das Thermometer zeigt -12°C und endlich geht es los. Zusammen mit rund 100 deutschen und internationalen Kollegen gehe ich abends an Bord des russischen Eisbrechers Kapitan Dranitsyn (12.900 BRZ, Länge 129m, Leistung 16.2MW). Das Ziel ist die Versorgung des Forschungseisbrechers Polarstern (12.600 BRZ, Länge 117m, Leistung 14.1MW), der für die MOSAiC Expedition an einer Eisscholle verankert durch die zentrale Arktis treibt. Dort werden wir unsere Kollegen ablösen und unsererseits 2 Monate die Messungen aufrechterhalten – so zumindest der Plan. Doch zunächst geht es über die winterlich-stürmische Barentssee nach Norden, an Franz-Josef-Land vorbei ins (nichtmehr so) ewige Eis. Die kommenden Monate wird uns eine unvergleichliche Landschaft nur aus Eisschollen, Schnee, Presseisrücken und vereinzelten Rinnen offenen Wassers begleiten. Noch ist davon allerdings nicht viel zu erkennen, es ist Polarnacht und die Scheinwerfer der Dranitsyn reichen nur wenige 100m über den Bug hinaus. Die Temperatur fällt unter -20°C. Zu dieser Jahreszeit ist das einjährige Meereis etwa 1.5m dick, dort wo es von Wind und Strömungen zusammengepresst wird ein Mehrfaches. In aufbrechenden Rinnen bildet sich in wenigen Stunden eine dünne Eisschicht. Trotz der Erfahrung der russischen Crew geht es nur langsam voran, wenn sich die Schollen zusammenschieben lässt sich häufig nur durch mehrmaliges Rammen ein Weg bahnen. Trotz der herausfordernden Eisbedingungen erreichen wir am 28 Februar die Polarstern auf 88.46°N. Nie zuvor erreichte ein Schiff in Fahrt eine nördlichere Position (den Rekord für treibende Schiffe stellte die Polarstern 4 Tage zuvor bei 88.60°N auf). Innerhalb weniger Tage werden Proviant und Versorgungsgüter übergeben, unsere Kollegen machen uns mit Details vertraut und machen sich mit der Dranitsyn auf den Heimweg. Die Temperatur fällt auf -42°C, die kältesten Tage der gesamten Expedition.

Doch wozu der ganze Aufwand? Was soll bei MOSAiC erforscht werden? Die Arktis erwärmt sich stärker und schneller als der Rest der Erde. Verändert sich die Arktis, betrifft das auch unsere mittleren Breiten. Schwächt sich zum Beispiel der Polarwirbel ab, wie zum Beispiel letzten Januar, häufen sich Extremwetterlagen in den mittleren Breiten. Besonders deutlich ist diese Erwärmung im Winter. Bis heute wissen wir aber nicht genau wie Ozean, Meereis und Atmosphäre dann miteinander wechselwirken – es fehlen schlicht genaue Messungen. Keine Überraschung in einer Gegend, die im Winter quasi unerreichbar ist. In Anlehnung an Fridtjof Nansens Fram Expedition 1893-1896 driftet nun die Polarstern innerhalb eines Jahres mit der Transpolarströmung von Ostsibirien am Pol vorbei Richtung Grönland. Von Bord aus erfasst neueste Messtechnik Energie- und Stofftransport von der Stratosphäre bis zum Ozeanboden. Messstationen auf der Scholle erlauben einen detaillierten Blick. Ein Beispiel: Wie schnell wächst das Eis an seiner Unterseite? Wie gut isoliert die Schneedecke auf dem Eis (und verhindert damit starkes Eiswachstum)? Verteilt sich gefallener Schnee gleichmäßig?

Ich betreue das Lidar des Leibniz Instituts für Troposphärenforschung. Ähnlich zu einem Radar wird ein Lichtpuls in die Atmosphäre geschickt und das zurückgestreute Signal empfangen. Aus diesen Messungen lassen sich die Höhe und Eigenschaften von Wolken und Aerosolpartikeln ableiten. Über den ganzen Winter konnten wir erstmalig beobachten wie Waldbrandrauch aus Sibirien in großer Höhe bis zum Nordpol transportiert wurde. Zusätzlich unterstütze ich Kollegen bei ihren Aufgaben auf der Scholle: Manuelle Eisdickenmessungen vornehmen, Messgeräte freischaufeln, Stromkabel vor Rinnen und Presseisrücken in Sicherheit bringen, Ausschau nach Eisbären halten, …

Am 11. März steigt die Sonne zum erstem Mal wieder über den Horizont. Wir treiben schnell nach Süden und im Eis um uns herum ist viel Bewegung. In kurzen Abständen bilden sich Rinnen und Presseisrücken, ständig müssen die Messstationen auf dem Eis angepasst werden. Aber auch an Land herrscht Aufruhr. Norwegen schließt wegen der Covid-19 Pandemie seine Grenzen. Damit ist der ursprüngliche Plan, dass unsere Ablösung im April per Flugzeug eintrifft hinfällig. Auch eine Ablösung per Schiff ist vorläufig nicht möglich, Eisausdehnung und –dicke haben ihr jährliches Maximum. Also abwarten, weiter Daten sammeln und nach Süden treiben. Zu Ostern wir es als Highlight gegrillt, inzwischen ist es 24h lang hell. Mitte April konkretisiert sich eine Lösung für unsere Ablösung. In aufwändigen Planungen konnten die Logistikfachleute des Alfred-Wegener-Instituts zwei andere Forschungsschiffe, Sonne (8500 BRZ, 116m, 6.5MW) und Maria S. Merian (5500 BRZ, 94m, 3.8MW), für die nächste Versorgung verpflichten. Einen Nachteil hat diese Option: Beide Schiffe sind keine Eisbrecher, die Polarstern wird die Scholle verlassen müssen. Um die wissenschaftlichen Geräte auf dem Eis nicht zu gefährden, ist noch einmal voller Einsatz gefragt. In kurzer Zeit wird das gesamte Camp abgebaut und verladen, nur einige autonome Sensoren bleiben zurück. Mitte Mai macht sich die Polarstern auf den Weg nach Spitzbergen, wo die Versorgung und Übergabe stattfinden wird. Auch unser Rückweg gestaltet sich zäher als erwartet, wieder ist das Eis stark komprimiert und unser Vorankommen entsprechend langsam. Anfang Juni erreichen wir den Adventfjord, wo Sonne und Maria S. Merian schon auf uns warten. Auf Reede wird verproviantiert, gebunkert und an unsere Kollegen übergeben. Danach macht sich die Polarstern für weitere 4 Monate auf den Weg ins Eis. Für uns geht es nach Bremerhaven, wo wir nach 140 Tagen erstmals wieder festen Boden betreten.

 

FLEXPART 10 into a Docker container

For a recent project, I wanted to include the Lagrangian particle dispersion model FLEXPART (more on that) into an easy-to-deploy project. The analysis part of that project runs on python, but uses some nifty geography libraries, which in turn require C and Fortran libraries. So, I already put this part into a docker container. FLEXPART provides the actual input for the analysis part, so it seemed logical to also include that part.

Not too long ago version 10 came available, also including netCDF output. However, with added dependencies, compiling the source became a bit more complicated. Let’s have a look at the Dockerfile:

FROM ubuntu:18.04


ARG DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get update && apt-get install -y \
  language-pack-en openssh-server vim software-properties-common \
  build-essential make gcc g++ zlib1g-dev git python3 python3-dev python3-pip \
  pandoc python3-setuptools imagemagick\
  gfortran autoconf libtool automake flex bison cmake git-core \
  libjpeg8-dev libfreetype6-dev libhdf5-serial-dev \
  libeccodes0 libeccodes-data libeccodes-dev \
  libnetcdf-c++4 libnetcdf-c++4-dev libnetcdff-dev \
  binutils  python3-numpy python3-mysqldb \
  python3-scipy python3-sphinx libedit-dev unzip curl wget
  
  
# replaced 'libpng12-dev' by libpng-dev
RUN add-apt-repository ppa:ubuntugis/ppa \
  && apt-get update \
  && apt-get install -y libatlas-base-dev libpng-dev \
     libproj-dev libgdal-dev gdal-bin  

RUN add-apt-repository 'deb http://security.ubuntu.com/ubuntu xenial-security main' \
  && apt-get update \
  && apt-get install -y libjasper1 libjasper-dev libeccodes-tools libeccodes-dev

#ENV HTTP https://confluence.ecmwf.int/download/attachments/45757960
#ENV ECCODES eccodes-2.9.2-Source

#
# Download, modify and compile flexpart 10
#
RUN mkdir flex_src && cd flex_src \
  && wget https://www.flexpart.eu/downloads/66 \
  && tar -xvf 66 \
  && rm 66 \
  && cd flexpart_v10.4_3d7eebf/src \
  && cp makefile makefile_local \
  && sed -i '74 a INCPATH1 = /usr/include\nINCPATH2 = /usr/include\nLIBPATH1 = /usr/lib\n F90 = gfortran' makefile_local \
  && sed -i 's/LIBS = -lgrib_api_f90 -lgrib_api -lm -ljasper $(NCOPT)/LIBS = -leccodes_f90 -leccodes -lm -ljasper $(NCOPT)/' makefile_local \
  && make -f makefile_local

ENV PATH /flex_src/flexpart_v10.4_3d7eebf/src/:$PATH

# pip install some more python libraries

The interesting part happens in L34-42. The source is downloaded and unpacked. Then the makefile  needs to be modified to find all libraries (which were installed with apt-get before). Sed is of great use here. First a line is added to the makefile_local and another line is searched and replaces. Finally make is executed to compile FLEXPART.

Now the container can be ran and FLEXPART be executed from within. At least for my application with a couple of thousand particles being simulated, performance is sufficient and a small penalty is outweigh by the ease of deployment.

Further reading:

Blue Planet

How much of the Earth’s surface is covered by water? – 2/3 elementary school knowledge. But how to confirm? And what fraction of the southern hemisphere mid-latitudes is covered by ocean?

We need some data first. For a quick shot, the MODIS land cover type classification should be sufficient. MCD12C1 provides global coverage at 0.05° in the HDF4 format. It is a resampled and  stitched together version of the 500m MCD12Q1 version. The user guide provides a nice overview.

Now we need to do some calculations. The python jupyter notebook is on github. The recipe is rather simple: Load the HDF4 dataset, select the IGBP[1] classification, quick plot for visualization, get the projection and the pixel sizes right and finally do some conditional sums.

The projections and the pixel sizes are a crucial point. The dataset is in the MODIS climate modeling grid, which is a geographic lat-lon projection. The pixel area gets smaller towards the poles, which we have to keep in mind, when calculating the size of the per pixel.

Looking at the final numbers, the fraction of water is 71.6%, which is sufficiently close to available estimates. Some discrepancy is expected, as we do not include ice shelfs, sea ice, tides, etc and the underlying resolution is ‘only’ 5km. Barren surfaces cover around 4.0% of the Earth (13.9% of the land) and ice sheets, including permanent snow cover 2.9% (10.2% of the land).

When splitting up the hemispheres, in the Northern Hemisphere water covers 61.4%, barren 7.5% and ice less than 1%. Of all the land, barren surfaces cover 19.4%. In the Southern Hemisphere, water makes up 81.8% of the surfaces, with barren less than 0.5% and ice 4.8%.

Finally the mid-latitudes. As a rather crude definition, the latitudes between 30° and 70° are used. In the northern mid-latitudes water covers 48.0% (5.7% barren and 0.7% ice). Barren surfaces make up 11.0% of the land. The southern mid-latitudes are overwhelmingly covered with water (93.9%, barren <0.2%, ice 1.4%).

[1] International Geosphere-Biosphere Programme: http://www.igbp.net/

micro-refactoring: long bool statements

“Hell is other people’s code” – while trying to reprocess some data on our server, a colleagues script did not produce the desired output. The problem was, that the location attribute was missing in the input netCDF files [1]. Which leads us to the code, that checked the attributes:

#next the formatting I'm supposed to check it against:

(key_for_range_dimension,key_for_time_dimension,key_for_azimuth_variable,key_for_elevation_variable,key_for_range_variable,key_for_unix_time_variable,key_for_doppler_velocity_variable,key_for_doppler_velocity_error_variable,key_for_nyquist_velocity_variable)=variable_name_key_setting_function(location_we_want,system_we_want,location_info,formatting_translator_dictionary,verbose)
       
#this is a logical statement that includes everything, using brackets to break up inline statements and make it easier to see
formatting_is_correct=(
               'location' in that_files_attributes
               ) and (
                              'system' in that_files_attributes
                              ) and (
                                            key_for_range_dimension in that_files_dimensions
                                            ) and (
                                                           key_for_range_variable in that_files_variables
                                                           ) and (
                                                                         key_for_time_dimension in that_files_dimensions
                                                                          ) and (
                                                                                        key_for_unix_time_variable in that_files_variables
                                                                                        ) and (
                                                                                                        key_for_azimuth_variable in that_files_variables
                                                                                                        ) and (
                                                                                                                       key_for_elevation_variable in that_files_variables
                                                                                                                       ) and (
                                                                                                                                     key_for_doppler_velocity_variable in that_files_variables
                                                                                                                                     )

print("Check format keys:")
print(('location' in that_files_attributes),
    ('system' in that_files_attributes),(key_for_range_dimension in that_files_dimensions),
    (key_for_range_variable in that_files_variables),(key_for_time_dimension in that_files_dimensions),
    (key_for_unix_time_variable in that_files_variables),
    (key_for_azimuth_variable in that_files_variables),
    (key_for_elevation_variable in that_files_variables),
    (key_for_doppler_velocity_variable in that_files_variables))

[You might want to expand the code snippet to fully appreciate the >380 column lines]

At least for me, that looked like a classical broken window. The final print statement is already a owned to the debugging efforts. Two points, that I consider problematic:

    1. insanely long lines (not the only occurrence in that script but a prominent one)
    2. the long and nested boolean statement

Both make it hard to understand, debug and modify the code. Let’s have see, what we quickly can do about it.

var_name_key_setting = variable_name_key_setting_function(
               location_we_want, system_we_want, location_info,
               formatting_translator_dictionary, verbose)
# unpack the function return
(key_for_range_dimension, key_for_time_dimension, 
 key_for_azimuth_variable, key_for_elevation_variable, 
 key_for_range_variable, key_for_unix_time_variable, 
 key_for_doppler_velocity_variable, 
 key_for_doppler_velocity_error_variable,
 key_for_nyquist_velocity_variable) = var_name_key_setting
       
formatting_is_correct_list = [
    'location' in that_files_attributes, 
    'location in attributes',
    'system' in that_files_attributes, 
    'system in attributes',
    key_for_range_dimension in that_files_dimensions, 
    'key range dim in files_dimensions',
    key_for_range_variable in that_files_variables,
    'key range var in files_variables',
    key_for_time_dimension in that_files_dimensions,
    'key time dim in files_dimensions',
    key_for_unix_time_variable in that_files_variables,
    'key unix_time_var in files_variables',
    key_for_azimuth_variable in that_files_variables,
    'key azimuth var in files_variables',
    key_for_elevation_variable in that_files_variables,
    'key elevation var in files_variables',
    key_for_doppler_velocity_variable in that_files_variables,
    'key doppler_vel_var in files_variables',
       ]
formatting_is_correct = all(formatting_is_correct_list[::2])

print("Check format keys:", formatting_is_correct)
if not formatting_is_correct:
       [print('  ', e[0], e[1]) for e in \
            zip(formatting_is_correct_list[0:-1:2], 
                formatting_is_correct_list[1::2])]

Still not perfect, but a bit more explicit and easier to read. In fact the [::2] could also be omitted, as a string also evaluates to True in all().

Maybe it’s time to setup a compulsory linter or at least make PEP 8 and the Google Python Style Guide required reading.

[1] actually required a second pair of eyes to figure out…

#ThrowbackThursday 03 Feb 2011

I recently stumbled across a couple of old pictures.

nilas_bow

I almost forgot about it, but evidently the first time I traveled trough sea ice was almost a decade ago.

grey_ice

We entered Oslofjord in the early morning of 03 February 2011 with FGS Rheinland-Pfalz.

panncakes_bowObviously not an ice breaker (not even ice strengthened), she managed the rather thin ice.

grey_white_view

SELinux, systemd and python virtual environments

Recently I wanted to configure a gunicorn backend at our newest server. The aim was to run a flask-based python application using a virtual environment in a users home. The machine is running recent Red Hat 8 and the setup of systemd was straightforward and well documented in the internet (e.g. here). However, after

systemctl daemon-reload
systemctl enable ourservice
systemctl start ourservice

a strange permission denied error occurred (also journalctl helps in that context).

After rechecking the file permission several times I finally remembered, that Red Hat now runs runs SELinux. It allows even finer access control for files, resources and actions using policies. The current status of SELinux can be shown with sudo sestatus. For testing reasons, all policies can be set to permissive using sudo setenforce permissive. The contexts of a file can be inspected with ls -Z file1.

To debug an process that fails, the /var/log/audit/audit.log file is the first place to look at. The audit2why tool translates the error messages to a more understandable format:

>
sudo cat /var/log/audit/audit.log | grep gunicorn | audit2why

Afterwards the audit2allow utility can generate rules, that would allow the denied operations:

>
sudo cat /var/log/audit/audit.log | grep gunicorn | audit2allow -M custom_rule

These rules are now stored in the format of an type enforcement (custom_rule.te) file and a policy package which can be activated using semodule -i custom_rule.pp.

Likely this process has to be repeated, as different kinds of operations might be denied.

For our setup the final type enforcement file looked like

module custom_rule 1.0;
 
require {
        type init_t;
        type unconfined_exec_t;
        type user_home_t;
        class file { append execute execute_no_trans ioctl map open read setattr };
        class lnk_file { getattr read };
}
 
#============= init_t ==============
#!!!! This avc is allowed in the current policy
allow init_t unconfined_exec_t:file { execute open read };
allow init_t user_home_t:file setattr;
 
#!!!! This avc can be allowed using the boolean 'domain_can_mmap_files'
allow init_t user_home_t:file map;
 
#!!!! This avc is allowed in the current policy
#!!!! This av rule may have been overridden by an extended permission av rule
allow init_t user_home_t:file { append execute execute_no_trans ioctl open read };
 
#!!!! This avc is allowed in the current policy
allow init_t user_home_t:lnk_file { getattr read };

The type enforcement rule can be compiled manually

checkmodule -M -m -o custom_rule.mod custom_rule.te
semodule_package -o custom_rule.pp -m custom_rule.mod
sudo semodule -i custom_rule.pp
Further useful resources
 
Disclaimer

When used correctly, SELinux adds a lot to your server’s security. When you run into problems with certain commands being denied, you should first make sure that you truly understand what causes the error. Chances are very high that SELinux complains for a reason. Often you can avoid the problem altogether by rethinking where you put which files. When you are absolutely sure that you need to build a new policy package, do yourself a favor and research thoroughly what each added rule does – it is only too easy to create security holes which would defeat SELinux’ purpose.

Michael Trojanek: How to compile a SELinux policy package

UPDATE

Wind in der Ostsee

Als Erweiterung zum Blogpost vom letzten Herbst, hier die Monatskarten mit der Windstatistik für die erste Jahreshälfte. Neu sind die Zusatzinformationen zu Flaute, Temperatur und Starkwindböen. Außerdem habe ich noch einige Gebiete angepasst/ergänzt. Außerdem basiert die Statistik jetzt auf der 0.125° (etwa 7.5nm bzw. 14km) Reanalyse.

Achtung: Die hier gezeigten Werte basieren auf eine langjährigen Statistik. Das schließt nicht aus, dass Starkwind und Sturm jederzeit auftreten können. Es liegt in der Verantwortung jedes Wassersportlers aktuelle Wetterinformationen einzuholen und die Aktivität entsprechend zu gestalten.

Download der hochaufgelösten Version: wind_statistics_baltic_sea.pdf

Februar

März

April

Mai

Juni