<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Dima Kogan</title>
  <link href="http://notes.secretsauce.netindex.xml" rel="self" />
  <link href="http://notes.secretsauce.net"/>
  <updated>2014-07-25T17:34:00Z</updated>
  <id>http://notes.secretsauce.netindex.xml</id>
  <entry><title type="html">Simple gpx export from ridewithgps</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2026/04/04_simple-gpx-export-from-ridewithgps.html"/><updated>2026-04-04T17:21:00Z</updated><published>2026-04-04T17:21:00Z</published><id>notes/2026/04/04_simple-gpx-export-from-ridewithgps.html</id><category scheme="/tags/tools.html" term="tools" label="tools"/><category scheme="/tags/data.html" term="data" label="data"/><category scheme="/tags/gis.html" term="GIS" label="GIS"/><category scheme="/tags/bike.html" term="bike" label="bike"/><content type="html">&lt;p&gt;
The &lt;a href="https://tourdelospadres.weebly.com/"&gt;Tour de Los Padres&lt;/a&gt; is coming! The race organizer post &lt;a href="https://ridewithgps.com/routes/54493422"&gt;the route on
ridewithgps&lt;/a&gt;. This works, but has convoluted interfaces for people not wanting to
use their service. I just wrote a simple script to export their data into a
plain .gpx file, &lt;i&gt;including&lt;/i&gt; all the waypoints; their exporter omits those.
&lt;/p&gt;

&lt;p&gt;
I've seen two flavors of their data, so here're two flavors of the
&lt;code&gt;gpx-from-ridewithgps.py&lt;/code&gt; script:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; sys
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; json

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;quote_xml&lt;/span&gt;(s):
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; s.replace(&lt;span style="color: #00cd00;"&gt;"&amp;amp;"&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;"&amp;amp;amp;"&lt;/span&gt;).replace(&lt;span style="color: #00cd00;"&gt;"&amp;lt;"&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;"&amp;amp;lt;"&lt;/span&gt;).replace(&lt;span style="color: #00cd00;"&gt;"&amp;gt;"&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;"&amp;amp;gt;"&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"Reading stdin"&lt;/span&gt;, &lt;span style="color: #0000ee; font-weight: bold;"&gt;file&lt;/span&gt;=sys.stderr)
&lt;span style="color: #cdcd00;"&gt;data&lt;/span&gt; = json.load(sys.stdin)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(r&lt;span style="color: #00cd00;"&gt;"""&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;&amp;lt;gpx version="1.1" creator="gpx-from-ridewithgps.py" xmlns="http://www.topografix.com/GPX/1/1"&amp;gt;"""&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; item &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; data[&lt;span style="color: #00cd00;"&gt;"extras"&lt;/span&gt;]:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; item[&lt;span style="color: #00cd00;"&gt;"type"&lt;/span&gt;] != &lt;span style="color: #00cd00;"&gt;"point_of_interest"&lt;/span&gt;:
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;continue&lt;/span&gt;
    &lt;span style="color: #cdcd00;"&gt;poi&lt;/span&gt; = item[&lt;span style="color: #00cd00;"&gt;"point_of_interest"&lt;/span&gt;]
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'  &amp;lt;wpt lat="{poi["lat"]}" lon="{poi["lng"]}"&amp;gt;'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;name&amp;gt;{quote_xml(poi["name"])}&amp;lt;/name&amp;gt;'&lt;/span&gt;)

    &lt;span style="color: #cdcd00;"&gt;desc&lt;/span&gt; = poi.get(&lt;span style="color: #00cd00;"&gt;"description"&lt;/span&gt;,&lt;span style="color: #00cd00;"&gt;""&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;len&lt;/span&gt;(desc):
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;desc&amp;gt;{quote_xml(desc)}&amp;lt;/desc&amp;gt;'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'  &amp;lt;/wpt&amp;gt;'&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"  &amp;lt;trk&amp;gt;&amp;lt;trkseg&amp;gt;"&lt;/span&gt;)
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; pt &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; data.get(&lt;span style="color: #00cd00;"&gt;"route"&lt;/span&gt;, {}).get(&lt;span style="color: #00cd00;"&gt;"track_points"&lt;/span&gt;, []):
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;trkpt lat="{pt["y"]}" lon="{pt["x"]}"&amp;gt;&amp;lt;ele&amp;gt;{pt["e"]}&amp;lt;/ele&amp;gt;&amp;lt;/trkpt&amp;gt;'&lt;/span&gt;)
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"  &amp;lt;/trkseg&amp;gt;&amp;lt;/trk&amp;gt;"&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"&amp;lt;/gpx&amp;gt;"&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; sys
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; json

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;quote_xml&lt;/span&gt;(s):
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; s.replace(&lt;span style="color: #00cd00;"&gt;"&amp;amp;"&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;"&amp;amp;amp;"&lt;/span&gt;).replace(&lt;span style="color: #00cd00;"&gt;"&amp;lt;"&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;"&amp;amp;lt;"&lt;/span&gt;).replace(&lt;span style="color: #00cd00;"&gt;"&amp;gt;"&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;"&amp;amp;gt;"&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"Reading stdin"&lt;/span&gt;, &lt;span style="color: #0000ee; font-weight: bold;"&gt;file&lt;/span&gt;=sys.stderr)
&lt;span style="color: #cdcd00;"&gt;data&lt;/span&gt; = json.load(sys.stdin)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(r&lt;span style="color: #00cd00;"&gt;"""&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;&amp;lt;gpx version="1.1" creator="gpx-from-ridewithgps.py" xmlns="http://www.topografix.com/GPX/1/1"&amp;gt;"""&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; poi &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; data[&lt;span style="color: #00cd00;"&gt;"points_of_interest"&lt;/span&gt;]:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'  &amp;lt;wpt lat="{poi["lat"]}" lon="{poi["lng"]}"&amp;gt;'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;name&amp;gt;{quote_xml(poi["name"])}&amp;lt;/name&amp;gt;'&lt;/span&gt;)

    &lt;span style="color: #cdcd00;"&gt;desc&lt;/span&gt; = poi.get(&lt;span style="color: #00cd00;"&gt;"description"&lt;/span&gt;,&lt;span style="color: #00cd00;"&gt;""&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;len&lt;/span&gt;(desc):
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;desc&amp;gt;{quote_xml(desc)}&amp;lt;/desc&amp;gt;'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'  &amp;lt;/wpt&amp;gt;'&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; poi &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; data[&lt;span style="color: #00cd00;"&gt;"course_points"&lt;/span&gt;]:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'  &amp;lt;wpt lat="{poi["y"]}" lon="{poi["x"]}"&amp;gt;'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;name&amp;gt;{quote_xml(poi["n"])}&amp;lt;/name&amp;gt;'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'  &amp;lt;/wpt&amp;gt;'&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"  &amp;lt;trk&amp;gt;&amp;lt;trkseg&amp;gt;"&lt;/span&gt;)
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; pt &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; data[&lt;span style="color: #00cd00;"&gt;'track_points'&lt;/span&gt;]:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;'    &amp;lt;trkpt lat="{pt["y"]}" lon="{pt["x"]}"&amp;gt;&amp;lt;ele&amp;gt;{pt["e"]}&amp;lt;/ele&amp;gt;&amp;lt;/trkpt&amp;gt;'&lt;/span&gt;)
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"  &amp;lt;/trkseg&amp;gt;&amp;lt;/trk&amp;gt;"&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"&amp;lt;/gpx&amp;gt;"&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
You invoke it by downloading the route and feeding it into the script:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;curl -s https://ridewithgps.com/routes/54493422.json | ./ridewithgps-to-gpx.py &amp;gt; out.gpx
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Note that the route number 54493422 is in the url above.&lt;/p&gt;
</content></entry><entry><title type="html">mrcal 2.5 released!</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2026/01/18_mrcal-25-released.html"/><updated>2026-01-18T16:00:00Z</updated><published>2026-01-18T16:00:00Z</published><id>notes/2026/01/18_mrcal-25-released.html</id><category scheme="/tags/mrcal.html" term="mrcal" label="mrcal"/><content type="html">&lt;p&gt;
&lt;a href="https://mrcal.secretsauce.net/"&gt;mrcal&lt;/a&gt; 2.5 is out: &lt;a href="https://mrcal.secretsauce.net/news-2.5.html"&gt;the release notes&lt;/a&gt;. Once &lt;i&gt;again&lt;/i&gt;, this is mostly a bug-fix
release en route to the big new features coming in 3.0.
&lt;/p&gt;

&lt;p&gt;
One cool thing is that these tools have now matured enough to no longer be
considered experimental. They have been used with great success in &lt;i&gt;lots&lt;/i&gt; of
contexts across many different projects and organizations. Some highlights:
&lt;/p&gt;

&lt;ul class="org-ul"&gt;
&lt;li&gt;I've calibrated extremely wide lenses
&lt;/li&gt;
&lt;li&gt;and extremely narrow lenses
&lt;/li&gt;
&lt;li&gt;and joint systems containing many different kinds of lenses
&lt;/li&gt;
&lt;li&gt;with lots of cameras at the same time. The biggest single joint calibration
I've done today had 10 cameras, but I'll almost certainly encounter bigger
systems in the future
&lt;/li&gt;
&lt;li&gt;mrcal has been used to process both visible and thermal cameras
&lt;/li&gt;
&lt;li&gt;The new triangulated-feature capability has been used in a
structure-from-motion context to compute the world geometry on-line.
&lt;/li&gt;
&lt;li&gt;mrcal has been used with weird experimental setups employing custom
calibration objects and single-view solves
&lt;/li&gt;
&lt;li&gt;mrcal has calibrated &lt;a href="https://github.com/dkogan/camera-lidar-calibration/"&gt;joint camera-LIDAR systems&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;and &lt;a href="https://www.github.com/dkogan/mrcal/blob/master/analyses/calibrate-camera-imu.py"&gt;joint camera-IMU systems&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Lots of students use mrcal as part of &lt;a href="https://photonvision.org/"&gt;PhotonVision&lt;/a&gt;, the toolkit used by teams
in the &lt;a href="https://www.firstinspires.org/robotics/frc"&gt;FIRST Robotics Competition&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
Some of the above is new, and not yet fully polished and documented and tested,
but it works.
&lt;/p&gt;

&lt;p&gt;
In mrcal 2.5, &lt;i&gt;most&lt;/i&gt; of the implementation of some new big features is written
and committed, but it's still incomplete. The new stuff is there, but is lightly
tested and documented. This will be completed eventually in mrcal 3.0:
&lt;/p&gt;

&lt;ul class="org-ul"&gt;
&lt;li&gt;&lt;a href="https://mrcal.secretsauce.net/uncertainty-cross-reprojection.html"&gt;Cross-reprojection uncertainty&lt;/a&gt;, to be able to perform full calibrations with a
splined model and &lt;i&gt;without&lt;/i&gt; a chessboard. &lt;a href="https://mrcal.secretsauce.net/mrcal-show-projection-uncertainty.html"&gt;&lt;code&gt;mrcal-show-projection-uncertainty
  --method cross-reprojection-rrp-Jfp&lt;/code&gt;&lt;/a&gt; is available today, and works in the
usual moving-chessboard-stationary camera case. Fully boardless coming later.
&lt;/li&gt;
&lt;li&gt;More general view of uncertainty and diffs. I want to support extrinsics-only
and/or intrinsics computations-only in lots of scenarios. Uncertainty in point
solves is already available in some conditions, for instance if the points are
fixed. New &lt;a href="https://mrcal.secretsauce.net/mrcal-show-stereo-pair-diff.html"&gt;&lt;code&gt;mrcal-show-stereo-pair-diff&lt;/code&gt; tool&lt;/a&gt; reports an extrinsics+intrinsics
diff between two calibrations of a stereo pair; experimental
&lt;a href="https://www.github.com/dkogan/mrcal/blob/master/analyses/extrinsics-stability.py"&gt;&lt;code&gt;analyses/extrinsics-stability.py&lt;/code&gt;&lt;/a&gt; tool reports an extrinsics-only diff. These
are in contrast to the intrinsics-only uncertainty and diffs in the existing
&lt;a href="https://mrcal.secretsauce.net/mrcal-show-projection-diff.html"&gt;&lt;code&gt;mrcal-show-projection-diff&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://mrcal.secretsauce.net/mrcal-show-projection-uncertainty.html"&gt;&lt;code&gt;mrcal-show-projection-uncertainty&lt;/code&gt;&lt;/a&gt; tools.
Some documentation in the &lt;a href="https://mrcal.secretsauce.net/uncertainty.html"&gt;uncertainty&lt;/a&gt; and &lt;a href="https://mrcal.secretsauce.net/differencing.html"&gt;differencing&lt;/a&gt; pages.
&lt;/li&gt;
&lt;li&gt;Implicit point solves, using the triangulation routines in the optimization
cost function. Should produce much more efficient structure-from-motion
solves. This is all the "triangulated-features" stuff. The cost function is
primarily built around &lt;code&gt;_mrcal_triangulated_error()&lt;/code&gt;. This is demoed in
&lt;a href="https://www.github.com/dkogan/mrcal/blob/master/test/test-sfm-triangulated-points.py"&gt;&lt;code&gt;test/test-sfm-triangulated-points.py&lt;/code&gt;&lt;/a&gt;. And I've been using
&lt;code&gt;_mrcal_triangulated_error()&lt;/code&gt; in structure-from-motion implementations within
other optimization routines.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
mrcal is quite good already, and will be even better in the future. Try it
today!
&lt;/p&gt;
</content></entry><entry><title type="html">Meshroom packaged for Debian</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2026/01/08_meshroom-packaged-for-debian.html"/><updated>2026-01-08T15:34:00Z</updated><published>2026-01-08T15:34:00Z</published><id>notes/2026/01/08_meshroom-packaged-for-debian.html</id><category scheme="/tags/tools.html" term="tools" label="tools"/><category scheme="/tags/debian.html" term="debian" label="debian"/><content type="html">&lt;p&gt;
Like the title says, I just packaged &lt;a href="https://alicevision.org/#meshroom"&gt;Meshroom&lt;/a&gt; (and all the adjacent
dependencies) for Debian! This is a fancy photogrammetry toolkit that uses
modern software development methods. "Modern" meaning that it has a multitude of
dependencies that come from lots of disparate places, which make it impossible
for a mere mortal to build the thing. The Linux "installer" is 13GB and probably
is some sort of container, or something.
&lt;/p&gt;

&lt;p&gt;
But now, if you have a Debian/sid box with the non-free repos enabled, you can
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;sudo apt install meshroom
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
And then you can generate and 3D-print a life-size, geometrically-accurate
statue of your cat. The &lt;code&gt;colmap&lt;/code&gt; package does a similar thing, and has been in
Debian for a while. I &lt;i&gt;think&lt;/i&gt; it can't do as many things, but it's good to have
both tools easily available.
&lt;/p&gt;

&lt;p&gt;
These packages are all in contrib, because they depend on a number of non-free
things, most notably CUDA.
&lt;/p&gt;

&lt;p&gt;
This is currently in Debian/sid, but should be picked up by the downstream
distros as they're released. The next noteworthy one is Ubuntu 26.04. Testing
and feedback welcome.
&lt;/p&gt;
</content></entry><entry><title type="html">Using libpython3 without linking it in; and old Python, g++ compatibility patches</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2026/01/01_using-libpython3-without-linking-it-in-and-old-python-g-compatibility-patches.html"/><updated>2026-01-01T21:52:00Z</updated><published>2026-01-01T21:52:00Z</published><id>notes/2026/01/01_using-libpython3-without-linking-it-in-and-old-python-g-compatibility-patches.html</id><content type="html">&lt;p&gt;
I just released &lt;a href="https://mrcal.secretsauce.net"&gt;mrcal 2.5&lt;/a&gt;; much more about that in a future post. Here, I'd like
to talk about some implementation details.
&lt;/p&gt;

&lt;div id="outline-container-sec-1" class="outline-2"&gt;
&lt;h2 id="sec-1"&gt;libpython3 and linking&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;div class="alert alert-info"&gt;&lt;p class="alert-heading"&gt;Follow-up patches&lt;/p&gt;

&lt;p&gt;
The technique described here ended up still incomplete! These extra patches were
needed:
&lt;/p&gt;

&lt;pre class="example"&gt;
commit c2475520ff0e4905e5d4b2f251ccbd0d54b7e02c
Author: Dima Kogan &amp;lt;dima@secretsauce.net&amp;gt;
Date:   Mon Jan 19 12:44:15 2026 -0800

    I weaken the python symbols using __asm__
    
    The previous scheme didn't work everywhere. Before this I had
    
      #include &amp;lt;Python.h&amp;gt;
      extern __typeof__(Py_xxx) Py_xxx __attribute__((weak));
      ...
    
    This worked for most functions and for all functions with gcc on Linux. With
    clang or any compiler on macos this did not work for inline functions. For
    instance, this test program would NOT get a weakened f2():
    
      int f(int);
      int f2(int);
      static inline int f3(int x)
      {
          return f2(x);
      }
      extern int f  (int) __attribute__((weak));
      extern int f2 (int) __attribute__((weak));
      int g(int x)
      {
          return f(x) + f3(x) + 5;
      }
    
    I would need to extern int f2 (int) __attribute__((weak)); prior to #including
    &amp;lt;Python.h&amp;gt;, but that wasn't possible because I didn't have the type of f2 then.
    The solution is to use __asm__ to tell the linker directly about the weak
    symbol. This does not need the prototype, and could be done before #include
    &amp;lt;Python.h&amp;gt;. This works. The only other wrinkle is a different attribute name on
    Linux and macos, and this is good now.

commit 1f8673c0374bcd0588bba17ce8f2bbc55db040cf
Author: Dima Kogan &amp;lt;dima@secretsauce.net&amp;gt;
Date:   Thu Jan 8 11:29:00 2026 -0800

    python-cameramodel-converter uses the limited python api if possible part 2
    
    Needed more logic to look at the python version before making a decision about
    which API to pull in

commit 9ae47f4feeec02df87a2364ba5bede4802259cd0
Author: Dima Kogan &amp;lt;dima@secretsauce.net&amp;gt;
Date:   Thu Jan 8 10:58:16 2026 -0800

    python-cameramodel-converter uses the limited python api if possible
    
    I'm not linking against libpython, so I'm not tying myself to any particular
    ABI version of libpython. To make this work regardless, I use the "limited"
    API:
    
      https://docs.python.org/3/c-api/stable.html#stable
    
    I'm calling PyUnicode_AsUTF8AndSize(), which entered the limited API in 3.10. I
    still allow this to build in older Python, but I don't explicitly declare the
    limited api. In that case, it is possible that using the binary libmrcal.so
    built with &amp;lt;3.10 with a later Python might cause issues

commit 592303d6d90712ddbde4aa7a152871d5b602cfdb
Author: Dima Kogan &amp;lt;dima@secretsauce.net&amp;gt;
Date:   Thu Jan 8 10:50:44 2026 -0800

    python-cameramodel-converter only functions that are in the limited api &amp;gt;= 3.10
&lt;/pre&gt;


&lt;/div&gt;


&lt;p&gt;
mrcal is a C library and a Python library. Much of mrcal itself interfaces the C
and Python libraries. And it is common for external libraries to want to pass
Python &lt;code&gt;mrcal.cameramodel&lt;/code&gt; objects to &lt;i&gt;their&lt;/i&gt; C code. The obvious way to do this
is in &lt;a href="https://docs.python.org/3/c-api/arg.html#other-objects"&gt;a &lt;i&gt;converter&lt;/i&gt; function&lt;/a&gt; in an &lt;code&gt;O&amp;amp;&lt;/code&gt; argument to
&lt;a href="https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTupleAndKeywords"&gt;&lt;code&gt;PyArg_ParseTupleAndKeywords()&lt;/code&gt;&lt;/a&gt;. I &lt;a href="https://github.com/dkogan/mrcal/blob/master/python-cameramodel-converter.h"&gt;wrote this &lt;code&gt;mrcal_cameramodel_converter()&lt;/code&gt;
function&lt;/a&gt;, which opened a whole can of worms when thinking about the compiling
and linking and distribution of this thing.
&lt;/p&gt;

&lt;p&gt;
&lt;code&gt;mrcal_cameramodel_converter()&lt;/code&gt; is meant to be called by code that implements
Python-wrapping of C code. This function will be called by the
&lt;a href="https://docs.python.org/3/c-api/arg.html#c.PyArg_ParseTupleAndKeywords"&gt;&lt;code&gt;PyArg_ParseTupleAndKeywords()&lt;/code&gt;&lt;/a&gt; Python library function, and it uses the Python
C API itself. Since it uses the Python C API, it would normally link against
&lt;code&gt;libpython&lt;/code&gt;. However:
&lt;/p&gt;

&lt;ul class="org-ul"&gt;
&lt;li&gt;The natural place to distribute this is in &lt;code&gt;libmrcal.so&lt;/code&gt;, but &lt;i&gt;this&lt;/i&gt; library
doesn't touch Python, and I'd rather not pull in all of &lt;code&gt;libpython&lt;/code&gt; for this
utility function, even in the 99% case when that function won't even be called
&lt;/li&gt;
&lt;li&gt;In &lt;a href="https://github.com/dkogan/mrcal/issues/22"&gt;some cases&lt;/a&gt; linking to &lt;code&gt;libpython&lt;/code&gt; actually breaks things, so I never do
that anymore anyway. This is fine: since this code will only ever be called by
&lt;code&gt;libpython&lt;/code&gt; itself, we're guaranteed that &lt;code&gt;libpython&lt;/code&gt; will already be loaded,
and we don't need to ask for it.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
OK, let's not link to &lt;code&gt;libpython&lt;/code&gt; then. But if we do that, we're going to have
unresolved references to our &lt;code&gt;libpython&lt;/code&gt; calls, and the loader will complain
when loading &lt;code&gt;libmrcal.so&lt;/code&gt;, even if we're not actually calling those functions.
This has an obvious solution: the references to the &lt;code&gt;libpython&lt;/code&gt; calls should be
marked weak. That won't generate unresolved-reference errors, and everything
will be great.
&lt;/p&gt;

&lt;p&gt;
OK, how do we mark things weak? There're two usual methods:
&lt;/p&gt;

&lt;ol class="org-ol"&gt;
&lt;li&gt;We mark the declaration (or definition?) or the relevant functions with
&lt;code&gt;__attribute__((weak))&lt;/code&gt;
&lt;/li&gt;

&lt;li&gt;We weaken the symbols after the compile with &lt;code&gt;objcopy --weaken&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
Method 1 is more work: I don't want to keep track of what Python API calls I'm
actually making. This is non-trivial, because some of the &lt;code&gt;Py_...()&lt;/code&gt; invocations
in my code are actually macros that call functions internally that I must
weaken. Furthermore, all the functions are declared in &lt;code&gt;Python.h&lt;/code&gt; that I don't
control. I can re-declare stuff with &lt;code&gt;__attribute__((weak))&lt;/code&gt;, but then I have to
match the prototypes. And I have to hope that re-declaring these will make
&lt;code&gt;__attribute__((weak))&lt;/code&gt; actually work.
&lt;/p&gt;

&lt;p&gt;
So clearly I want method 2. I implemented it:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-makefile"&gt;&lt;span style="color: #0000ee; font-weight: bold;"&gt;python-cameramodel-converter.o&lt;/span&gt;: %.o:%.c
        $(&lt;span style="color: #cdcd00;"&gt;c_build_rule&lt;/span&gt;); mv &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt; _&lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;
        $(&lt;span style="color: #cdcd00;"&gt;OBJCOPY&lt;/span&gt;) --wildcard --weaken-symbol=&lt;span style="color: #00cd00;"&gt;'Py*'&lt;/span&gt; --weaken-symbol=&lt;span style="color: #00cd00;"&gt;'_Py*'&lt;/span&gt; _&lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Works great on my machine! But doesn't work on other people's machines. Because
only the most recent &lt;code&gt;objcopy&lt;/code&gt; tool actually works to weaken &lt;i&gt;references&lt;/i&gt;.
Apparently the older tools only weaken definitions, which isn't useful to me,
and the tool only started handling references very recently.
&lt;/p&gt;

&lt;p&gt;
Well that sucks. I guess I will need to mark the symbols with
&lt;code&gt;__attribute__((weak))&lt;/code&gt; after all. I use the &lt;code&gt;nm&lt;/code&gt; tool to find the symbols that
should be weakened, and I apply the attribute with this macro:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-c"&gt;&lt;span style="color: #0000ee; font-weight: bold;"&gt;#define&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;WEAKEN&lt;/span&gt;(&lt;span style="color: #cdcd00;"&gt;f&lt;/span&gt;) &lt;span style="color: #00cdcd; font-weight: bold;"&gt;extern&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;__typeof__&lt;/span&gt;(f) &lt;span style="color: #00cd00;"&gt;f&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;__attribute__&lt;/span&gt;((weak));
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The prototypes are handled by &lt;code&gt;__typeof__&lt;/code&gt;. So are we done? With gcc, we are
done. With clang we are not done. Apparently this macro does not weaken symbols
generated by inline function calls if using clang I have no idea if this is a
bug. The Python internal machinery has some of these, so this doesn't weaken
&lt;i&gt;all&lt;/i&gt; the symbols. I give up on the people that both have a too-old objcopy
&lt;i&gt;and&lt;/i&gt; are using clang, and declare victory. So the logic ends up being:
&lt;/p&gt;

&lt;ol class="org-ol"&gt;
&lt;li&gt;Compile
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;objcopy --weaken&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nm&lt;/code&gt; to find the non-weak Python references
&lt;/li&gt;
&lt;li&gt;If there aren't any, our &lt;code&gt;objcopy&lt;/code&gt; call worked and we're done!
&lt;/li&gt;
&lt;li&gt;Otherwise, compile again, but explicitly asking to weaken &lt;i&gt;those&lt;/i&gt; symbols
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nm&lt;/code&gt; again to see if the compiler didn't do it
&lt;/li&gt;
&lt;li&gt;If any non-weak references still remain, complain and give up.
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
Whew. This logic appears &lt;a href="https://github.com/dkogan/mrcal/blob/98a0ae4bce9eede6ab86b98975c322cc475b60fc/Makefile#L61"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/dkogan/mrcal/blob/98a0ae4bce9eede6ab86b98975c322cc475b60fc/python-cameramodel-converter.c#L19"&gt;here&lt;/a&gt;. There were &lt;i&gt;even more&lt;/i&gt; things to deal
with here: calling &lt;code&gt;nm&lt;/code&gt; and &lt;code&gt;objcopy&lt;/code&gt; needed special attention and build-system
support in case we were cross-building. I &lt;a href="https://github.com/dkogan/mrbuild/commit/8ef5d4de06684535c9b3c09612492963af25ea45"&gt;took care of it in &lt;code&gt;mrbuild&lt;/code&gt;&lt;/a&gt;.
&lt;/p&gt;

&lt;p&gt;
This worked for a while. Until the converter code started to fail. Because &amp;#x2026;.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-sec-2" class="outline-2"&gt;
&lt;h2 id="sec-2"&gt;Supporting old Python&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
&amp;#x2026;. I was using &lt;code&gt;PyTuple_GET_ITEM()&lt;/code&gt;. This is a macro to access &lt;code&gt;PyTupleObject&lt;/code&gt;
data. So the layout of &lt;code&gt;PyTupleObject&lt;/code&gt; ended up encoded in &lt;code&gt;libmrcal.so&lt;/code&gt;. But
apparently this wasn't stable, and changed between Python3.13 and Python3.14. As
described above, I'm not linking to &lt;code&gt;libpython&lt;/code&gt;, so there's no &lt;code&gt;NEEDED&lt;/code&gt; tag to
make sure we pull in the right version. The solution was to &lt;a href="https://github.com/dkogan/mrcal/commit/a922e354ec4521580ed73812ed61b04312c29411"&gt;call the
&lt;code&gt;PyTuple_GetItem()&lt;/code&gt; function instead&lt;/a&gt;. This is unsatisfying, and means that in
theory other stuff here might stop working in some Python 3.future, but I'm
ready to move on for now.
&lt;/p&gt;

&lt;p&gt;
There were other annoying gymnastics that had to be performed to make this work
with old-but-not-super old tooling.
&lt;/p&gt;

&lt;p&gt;
The Python people deprecated &lt;a href="https://docs.python.org/3/c-api/module.html#c.PyModule_AddObject"&gt;&lt;code&gt;PyModule_AddObject()&lt;/code&gt;&lt;/a&gt;, and added &lt;a href="https://docs.python.org/3/c-api/module.html#c.PyModule_Add"&gt;&lt;code&gt;PyModule_Add()&lt;/code&gt;&lt;/a&gt;
as a replacement. I want to support Pythons before and after this happened, so I
needed some &lt;a href="https://github.com/dkogan/mrcal/blob/98a0ae4bce9eede6ab86b98975c322cc475b60fc/mrcal-pywrap.c#L4513"&gt;if statements&lt;/a&gt;. Today the old function still works, but eventually it
will stop, and I will have needed to do this typing sooner or later.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-sec-3" class="outline-2"&gt;
&lt;h2 id="sec-3"&gt;Supporting old C++ compilers&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-3"&gt;
&lt;p&gt;
mrcal is a C project, but it is common for people to want to &lt;code&gt;#include&lt;/code&gt; the
headers from C++. I widely use C99 designated initializers (27-years old in C!),
which causes issues with not-very-old C++ compilers. I worked around this
initialization &lt;a href="https://github.com/dkogan/mrcal/commit/e949893348dbe915e66b817fb821b8b9b818fa89"&gt;in one spot&lt;/a&gt;, and disabled it a feature for a too-old compiler &lt;a href="https://github.com/dkogan/mrcal/commit/88d81d6dd7f259c5266002e6c6ce9d2d1616575e"&gt;in
another spot&lt;/a&gt;. Fortunately, semi-recent tooling supports my usages, so this is
becoming a non-issue as time goes on.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
</content></entry><entry><title type="html">Eigen macro specializations crashes</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2025/03/17_eigen-macro-specializations-crashes.html"/><updated>2025-03-17T20:52:00Z</updated><published>2025-03-17T20:52:00Z</published><id>notes/2025/03/17_eigen-macro-specializations-crashes.html</id><category scheme="/tags/tools.html" term="tools" label="tools"/><content type="html">&lt;p&gt;
There's an issue in the &lt;a href="https://eigen.tuxfamily.org/index.php?title=Main_Page"&gt;Eigen linear algebra library&lt;/a&gt; where linking together
objects compiled with different flags causes the resulting binary to crash. Some
details are written-up in &lt;a href="https://www.mail-archive.com/debian-science@lists.debian.org/msg13672.html"&gt;this mailing list thread&lt;/a&gt;.
&lt;/p&gt;

&lt;p&gt;
I just encountered a situation where a large application sometimes crashes for
unknown reasons, and needed a method to determine whether this Eigen issue could
be the cause. I ended up doing this by using the DWARF data to see if the linked
binary contains the different incompatible flavors of &lt;code&gt;malloc&lt;/code&gt; / &lt;code&gt;free&lt;/code&gt; or not.
&lt;/p&gt;

&lt;p&gt;
I downloaded the &lt;a href="https://www.mail-archive.com/debian-science@lists.debian.org/msg13710.html"&gt;small demo program showing the problem&lt;/a&gt;. I built it:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;&lt;span style="color: #cdcd00;"&gt;CCXXXFLAGS&lt;/span&gt;=-g make
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Here if you run &lt;code&gt;./main&lt;/code&gt;, the bug is triggered, and a crash occurs. I looked at
the debug info for the code in question:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; o (main lib.so) {
  &lt;span style="color: #0000ee; font-weight: bold;"&gt;echo&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;"======== $o"&lt;/span&gt;;
  readelf --debug-dump=decodedline $&lt;span style="color: #cdcd00;"&gt;o&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  | awk &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    &lt;span style="color: #00cd00;"&gt;'$1 ~ /^Memory.h/&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       if(180 &amp;lt;= $2 &amp;amp;&amp;amp; $2 &amp;lt;= 186) {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         have["malloc_glibc"]=1&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       if(188 == $2) {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         have["malloc_handmade"]=1&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       if(201 &amp;lt;= $2 &amp;amp;&amp;amp; $2 &amp;lt;= 204) {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         have["free_glibc"]=1&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       if(206 == $2) {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         have["free_handmade"]=1&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     END&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       for (var in have) {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         print(var);&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     }'&lt;/span&gt;
}
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
It says:
&lt;/p&gt;

&lt;pre class="example"&gt;
======== main
free_handmade
======== lib.so
malloc_glibc
free_glibc
&lt;/pre&gt;

&lt;p&gt;
Here I looked at &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;lib.so&lt;/code&gt; (the build products from this little demo).
In a real case you'd look at every shared library linked into the binary and the
binary itself. On my machine &lt;code&gt;/usr/include/eigen3/Eigen/src/Core/util/Memory.h&lt;/code&gt;
looks like this, starting on line 174:
&lt;/p&gt;

&lt;pre class="example"&gt;
174 EIGEN_DEVICE_FUNC inline void* aligned_malloc(std::size_t size)
175 {
176   check_that_malloc_is_allowed();
177 
178   void *result;
179   #if (EIGEN_DEFAULT_ALIGN_BYTES==0) || EIGEN_MALLOC_ALREADY_ALIGNED
180 
181     EIGEN_USING_STD(malloc)
182     result = malloc(size);
183 
184     #if EIGEN_DEFAULT_ALIGN_BYTES==16
185     eigen_assert((size&amp;lt;16 || (std::size_t(result)%16)==0) &amp;amp;&amp;amp; "System's malloc returned an unaligned pointer. Compile with EIGEN_MALLOC_ALREADY_ALIGNED=0 to fallback to handmade aligned memory allocator.");
186     #endif
187   #else
188     result = handmade_aligned_malloc(size);
189   #endif
190 
191   if(!result &amp;amp;&amp;amp; size)
192     throw_std_bad_alloc();
193 
194   return result;
195 }
196 
197 /** \internal Frees memory allocated with aligned_malloc. */
198 EIGEN_DEVICE_FUNC inline void aligned_free(void *ptr)
199 {
200   #if (EIGEN_DEFAULT_ALIGN_BYTES==0) || EIGEN_MALLOC_ALREADY_ALIGNED
201 
202     EIGEN_USING_STD(free)
203     free(ptr);
204 
205   #else
206     handmade_aligned_free(ptr);
207   #endif
208 }
&lt;/pre&gt;

&lt;p&gt;
The above &lt;code&gt;awk&lt;/code&gt; script looks at the two malloc paths and the two free paths, and
we can clearly see that it only ever calls &lt;code&gt;malloc_glibc()&lt;/code&gt;, but has both
flavors of &lt;code&gt;free()&lt;/code&gt;. So this can crash. We want to see that the whole executable
(shared libraries and all) should only have one type of &lt;code&gt;malloc()&lt;/code&gt; and &lt;code&gt;free()&lt;/code&gt;,
and that would guarantee no crashing.
&lt;/p&gt;

&lt;p&gt;
There are a more functions in that header that should be instrumented
(&lt;code&gt;realloc()&lt;/code&gt; for instance) and the different alignment paths should be
instrumented similarly (as described in the mailing list thread above), but here
we see that this technique works.
&lt;/p&gt;
</content></entry><entry><title type="html">Getting precise timings out of RS-232 output</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2025/03/14_getting-precise-timings-out-of-rs-232-output.html"/><updated>2025-03-14T12:47:00Z</updated><published>2025-03-14T12:47:00Z</published><id>notes/2025/03/14_getting-precise-timings-out-of-rs-232-output.html</id><category scheme="/tags/tools.html" term="tools" label="tools"/><content type="html">&lt;p&gt;
For uninteresting reasons I need very regular 58Hz pulses coming out of an
RS-232 Tx line: the time between each pulse should be as close to 1/58s as
possible. I produce each pulse by writing an &lt;code&gt;\xFF&lt;/code&gt; byte to the device. The
start bit is the only active-voltage bit being sent, and that produces my pulse.
I wrote this obvious C program:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-c"&gt;&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;stdlib.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;stdbool.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;sys/ioctl.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;fcntl.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;termios.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;stdint.h&amp;gt;&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;#include&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;&amp;lt;sys/time.h&amp;gt;&lt;/span&gt;

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;static&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;gettimeofday_uint64&lt;/span&gt;()
{
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;struct&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;timeval&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;tv&lt;/span&gt;;
    gettimeofday(&amp;amp;tv, &lt;span style="color: #cd00cd;"&gt;NULL&lt;/span&gt;);
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; (&lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt;) tv.tv_sec * 1000000ULL + (&lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt;) tv.tv_usec;
}

&lt;span style="color: #00cd00;"&gt;int&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;main&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;int&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;argc&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;char&lt;/span&gt;* &lt;span style="color: #cdcd00;"&gt;argv&lt;/span&gt;[])
{
    &lt;span style="color: #cdcd00;"&gt;// &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;open the serial device, and make it as raw as possible&lt;/span&gt;
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;const&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;char&lt;/span&gt;* &lt;span style="color: #cdcd00;"&gt;device&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;"/dev/ttyS0"&lt;/span&gt;;
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;const&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;speed_t&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;baud&lt;/span&gt; = B9600;

    &lt;span style="color: #00cd00;"&gt;int&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;fd&lt;/span&gt; = open(device, O_WRONLY|O_NOCTTY);
    tcflush(fd, TCIOFLUSH);

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;struct&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;termios&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;options&lt;/span&gt; = {.c_iflag = IGNBRK,
                              .c_cflag = CS8 | CREAD | CLOCAL};
    cfsetspeed(&amp;amp;options, baud);
    tcsetattr(fd, TCSANOW, &amp;amp;options);

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;const&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;T_us&lt;/span&gt; = (&lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt;)(1e6 / 58.);

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;const&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;t0&lt;/span&gt; = gettimeofday_uint64();
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;int&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;i&lt;/span&gt;=0; ; i++)
    {
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;const&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;t_target&lt;/span&gt; = t0 + T_us*i;
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;const&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;uint64_t&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;t1&lt;/span&gt;       = gettimeofday_uint64();

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt;(t_target &amp;gt; t1)
            usleep(t_target - t1);

        write(fd, &amp;amp;((&lt;span style="color: #00cd00;"&gt;char&lt;/span&gt;){&lt;span style="color: #00cd00;"&gt;'\xff'&lt;/span&gt;}), 1);
    }
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; 0;
}
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
This tries to make sure that each &lt;code&gt;write()&lt;/code&gt; call happens at 58Hz. I need these
pulses to be regular, so I need to also make sure that the time between each
userspace &lt;code&gt;write()&lt;/code&gt; and when the edge actually hits the line is as short as
possible or, at least, stable.
&lt;/p&gt;

&lt;p&gt;
Potential reasons for timing errors:
&lt;/p&gt;

&lt;ol class="org-ol"&gt;
&lt;li&gt;The &lt;code&gt;usleep()&lt;/code&gt; doesn't wake up exactly when it should. This is subject to the
Linux scheduler waking up the &lt;code&gt;trigger&lt;/code&gt; process
&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;write()&lt;/code&gt; almost certainly ends up scheduling a helper task to actually
write the &lt;code&gt;\xFF&lt;/code&gt; to the hardware. This helper task is also subject to the
Linux scheduler waking it up.
&lt;/li&gt;
&lt;li&gt;Whatever the hardware does. RS-232 doesn't give you any guarantees about
byte-byte timings, so this could be an unfixable source of errors
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;
The scheduler-related questions are observable without any extra hardware, so
let's do that first.
&lt;/p&gt;

&lt;p&gt;
I run the &lt;code&gt;./trigger&lt;/code&gt; program, and look at diagnostics while that's running.
&lt;/p&gt;

&lt;p&gt;
I look at some device details:
&lt;/p&gt;

&lt;pre class="example"&gt;
# ls -lh /dev/ttyS0
crw-rw---- 1 root dialout 4, 64 Mar  6 18:11 /dev/ttyS0

# ls -lh /sys/dev/char/4:64/
total 0
-r--r--r-- 1 root root 4.0K Mar  6 16:51 close_delay
-r--r--r-- 1 root root 4.0K Mar  6 16:51 closing_wait
-rw-r--r-- 1 root root 4.0K Mar  6 16:51 console
-r--r--r-- 1 root root 4.0K Mar  6 16:51 custom_divisor
-r--r--r-- 1 root root 4.0K Mar  6 16:51 dev
lrwxrwxrwx 1 root root    0 Mar  6 16:51 device -&amp;gt; ../../../0000:00:16.3:0.0
-r--r--r-- 1 root root 4.0K Mar  6 16:51 flags
-r--r--r-- 1 root root 4.0K Mar  6 16:51 iomem_base
-r--r--r-- 1 root root 4.0K Mar  6 16:51 iomem_reg_shift
-r--r--r-- 1 root root 4.0K Mar  6 16:51 io_type
-r--r--r-- 1 root root 4.0K Mar  6 16:51 irq
-r--r--r-- 1 root root 4.0K Mar  6 16:51 line
-r--r--r-- 1 root root 4.0K Mar  6 16:51 port
drwxr-xr-x 2 root root    0 Mar  6 16:51 power
-rw-r--r-- 1 root root 4.0K Mar  6 16:51 rx_trig_bytes
lrwxrwxrwx 1 root root    0 Mar  6 16:51 subsystem -&amp;gt; ../../../../../../../class/tty
-r--r--r-- 1 root root 4.0K Mar  6 16:51 type
-r--r--r-- 1 root root 4.0K Mar  6 16:51 uartclk
-rw-r--r-- 1 root root 4.0K Mar  6 16:51 uevent
-r--r--r-- 1 root root 4.0K Mar  6 16:51 xmit_fifo_size
&lt;/pre&gt;

&lt;p&gt;
Unsurprisingly, this is a part of the &lt;code&gt;tty&lt;/code&gt; subsystem. I don't want to spend the
time to really figure out how this works, so let me look at &lt;i&gt;all&lt;/i&gt; the &lt;code&gt;tty&lt;/code&gt;
kernel calls and also at all the kernel tasks scheduled by the &lt;code&gt;trigger&lt;/code&gt;
process, since I suspect that the actual hardware poke is happening in a helper
task. I see this:
&lt;/p&gt;

&lt;pre class="example"&gt;
# bpftrace -e 'k:*tty* /comm=="trigger"/
               { printf("%d %d %s\n",pid,tid,probe); }
               t:sched:sched_wakeup /comm=="trigger"/
               { printf("switching to %s(%d); current backtrace:", args.comm, args.pid); print(kstack());  }'

...

3397345 3397345 kprobe:tty_ioctl
3397345 3397345 kprobe:tty_check_change
3397345 3397345 kprobe:__tty_check_change
3397345 3397345 kprobe:tty_wait_until_sent
3397345 3397345 kprobe:tty_write
3397345 3397345 kprobe:file_tty_write.isra.0
3397345 3397345 kprobe:tty_ldisc_ref_wait
3397345 3397345 kprobe:n_tty_write
3397345 3397345 kprobe:tty_hung_up_p
switching to kworker/0:1(3400169); current backtrace:
        ttwu_do_activate+268
        ttwu_do_activate+268
        try_to_wake_up+605
        kick_pool+92
        __queue_work.part.0+582
        queue_work_on+101
        rpm_resume+1398
        __pm_runtime_resume+75
        __uart_start+85
        uart_write+150
        n_tty_write+1012
        file_tty_write.isra.0+373
        vfs_write+656
        ksys_write+109
        do_syscall_64+130
        entry_SYSCALL_64_after_hwframe+118

3397345 3397345 kprobe:tty_update_time
3397345 3397345 kprobe:tty_ldisc_deref

... repeated with each pulse ...
&lt;/pre&gt;

&lt;p&gt;
Looking at the sources I see that &lt;code&gt;uart_write()&lt;/code&gt; calls &lt;code&gt;__uart_start()&lt;/code&gt;, which
schedules a task to call &lt;code&gt;serial_port_runtime_resume()&lt;/code&gt; which eventually calls
&lt;code&gt;serial8250_tx_chars()&lt;/code&gt;, which calls some low-level functions to actually send
the bits.
&lt;/p&gt;

&lt;p&gt;
I look at the time between two of those calls to quantify the scheduler latency:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;&lt;span style="color: #cdcd00;"&gt;pulserate&lt;/span&gt;=58

sudo zsh -c &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  &lt;span style="color: #00cd00;"&gt;'( echo "# dt_write_ns dt_task_latency_ns";&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     bpftrace -q -e "k:vfs_write /comm==\"trigger\" &amp;amp;&amp;amp; arg2==1/&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;                     {\$t=nsecs(); if(@t0) { @dt_write = \$t-@t0; } @t0=\$t;}&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;                     k:serial8250_tx_chars /@dt_write/&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;                     {\$t=nsecs(); printf(\"%d %d\\n\", @dt_write, \$t-@t0);}"&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;   )'&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
| vnl-filter                  &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --stream -p &lt;span style="color: #cdcd00;"&gt;dt_write_ms&lt;/span&gt;=&lt;span style="color: #00cd00;"&gt;"dt_write_ns/1e6 - 1e3/$pulserate"&lt;/span&gt;,&lt;span style="color: #cdcd00;"&gt;dt_task_latency_ms&lt;/span&gt;=dt_task_latency_ns/1e6 &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
| feedgnuplot  &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --stream   &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --lines    &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --points   &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --xlen 200 &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --vnl      &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --autolegend &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --xlabel &lt;span style="color: #00cd00;"&gt;'Pulse index'&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    --ylabel &lt;span style="color: #00cd00;"&gt;'Latency (ms)'&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Here I'm making a realtime plot showing
&lt;/p&gt;

&lt;ul class="org-ul"&gt;
&lt;li&gt;The offset from 58Hz of when each &lt;code&gt;write()&lt;/code&gt; call happens. This shows effect #1
from above: how promptly the &lt;code&gt;trigger&lt;/code&gt; process wakes up
&lt;/li&gt;
&lt;li&gt;The latency of the helper task. This shows effect #2 above.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
The raw data as I tweak things lives &lt;a href="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.scheduler.vnl"&gt;here&lt;/a&gt;. Initially I see big latency spikes:
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.scheduler.1.noise.svg" alt="timings.scheduler.1.noise.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
These can be fixed by adjusting the priority of the &lt;code&gt;trigger&lt;/code&gt; task. This tells
the scheduler to wake that task up &lt;i&gt;first&lt;/i&gt;, even if something else is currently
using the CPU. I do this:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;sudo chrt -p 90 &lt;span style="color: #cdcd00;"&gt;`pidof trigger`&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
And I get better-looking latencies:
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.scheduler.2.clean.svg" alt="timings.scheduler.2.clean.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
During some experiments (not in this dataset) I would see high helper-task
timing instabilities as well. These could be fixed by prioritizing the helper
task. In this kernel (&lt;code&gt;6.12&lt;/code&gt;) the helper task is called &lt;code&gt;kworker/N&lt;/code&gt; where &lt;code&gt;N&lt;/code&gt; is
the CPU index. I tie the &lt;code&gt;trigger&lt;/code&gt; process to cpu 0, and priorities all the
relevant helpers:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;taskset -c 0 ./trigger 58

pgrep -f kworker/0 | &lt;span style="color: #00cdcd; font-weight: bold;"&gt;while&lt;/span&gt; { &lt;span style="color: #0000ee; font-weight: bold;"&gt;read&lt;/span&gt; pid } { sudo chrt -p 90 $&lt;span style="color: #cdcd00;"&gt;pid&lt;/span&gt; }
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
This fixes the helper-task latency spikes.
&lt;/p&gt;

&lt;p&gt;
OK, so it looks like on the software side we're good to within 0.1ms of the true
period. This is in the ballpark of the precision I need; even this might be too
high. It's possible to try to push the software to do better: one could look at
the kernel sources a bit more, to do smarter things with priorities or to try an
&lt;code&gt;-rt&lt;/code&gt; kernel. But all this doesn't matter if the serial hardware adds
unacceptable delays. Let's look.
&lt;/p&gt;

&lt;p&gt;
Let's look at it with a logic analyzer. I use a saleae logic analyzer with
&lt;a href="https://sigrok.org/"&gt;sigrok&lt;/a&gt;. The tool spits out the samples as it gets them, and an &lt;code&gt;awk&lt;/code&gt; script
finds the edges and reports the timings to give me a realtime plot.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;&lt;span style="color: #cdcd00;"&gt;samplerate&lt;/span&gt;=500000;
&lt;span style="color: #cdcd00;"&gt;pulserate&lt;/span&gt;=58.;
sigrok-cli -c &lt;span style="color: #cdcd00;"&gt;samplerate&lt;/span&gt;=$&lt;span style="color: #cdcd00;"&gt;samplerate&lt;/span&gt; -O csv --continuous -C D1 &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
| mawk -Winteractive  &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
    &lt;span style="color: #00cd00;"&gt;"prev_logic==0 &amp;amp;&amp;amp; \$0==1 \&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     { &lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       iedge = NR;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       if(prev_iedge)&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         di = iedge -prev_iedge;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         dt = di/$samplerate;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;         print(dt*1000);&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       prev_iedge = iedge;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     }&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     {&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;       prev_logic=\$0;&lt;/span&gt;
&lt;span style="color: #00cd00;"&gt;     } "&lt;/span&gt; | feedgnuplot --stream --ylabel &lt;span style="color: #00cd00;"&gt;'Period (ms)'&lt;/span&gt; --equation &lt;span style="color: #00cd00;"&gt;"1000./$pulserate title \"True ${pulserate}Hz period\""&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
On the server I was using (physical RS-232 port, ancient 3.something kernel):
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.hw.serial-server.svg" alt="timings.hw.serial-server.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
OK&amp;#x2026; This is very discrete for some reason, and generally worse than 0.1ms.
What about my laptop (physical RS-232 port, recent 6.12 kernel)?
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.hw.serial-laptop.svg" alt="timings.hw.serial-laptop.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
Not discrete anymore, but not really any more precise. What about using a
usb-serial converter? I expect this to be worse.
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.hw.usbserial.svg" alt="timings.hw.usbserial.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
Yeah, looks worse. For my purposes, an accuracy of 0.1ms is marginal, and the
hardware adds non-negligible errors. So I cut my losses, and use an external
signal generator:
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/14_getting-precise-timings-out-of-rs-232-output/timings.hw.generator.svg" alt="timings.hw.generator.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
Yeah. That's better, so that's what I use.
&lt;/p&gt;
</content></entry><entry><title type="html">Shop scheduling with PuLP</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2025/03/05_shop-scheduling-with-pulp.html"/><updated>2025-03-05T12:02:00Z</updated><published>2025-03-05T12:02:00Z</published><id>notes/2025/03/05_shop-scheduling-with-pulp.html</id><category scheme="/tags/data.html" term="data" label="data"/><category scheme="/tags/tools.html" term="tools" label="tools"/><content type="html">&lt;p&gt;
I recently used the &lt;a href="https://coin-or.github.io/pulp/"&gt;PuLP modeler&lt;/a&gt; to solve a work scheduling problem to assign
workers to shifts. Here are notes about doing that. This is a common use case,
but isn't explicitly covered in the &lt;a href="https://coin-or.github.io/pulp/CaseStudies/index.html"&gt;case studies&lt;/a&gt; in the PuLP documentation.
&lt;/p&gt;

&lt;p&gt;
Here's the problem:
&lt;/p&gt;

&lt;ul class="org-ul"&gt;
&lt;li&gt;We are trying to put together a schedule for one week
&lt;/li&gt;
&lt;li&gt;Each day has some set of work shifts that need to be staffed
&lt;/li&gt;
&lt;li&gt;Each shift must be staffed with &lt;i&gt;exactly&lt;/i&gt; one worker
&lt;/li&gt;
&lt;li&gt;The shift schedule is known beforehand, and the workers each declare their
preferences beforehand: they mark each shift in the week as one of:
&lt;ul class="org-ul"&gt;
&lt;li&gt;PREFERRED (if they want to be scheduled on that shift)
&lt;/li&gt;
&lt;li&gt;NEUTRAL
&lt;/li&gt;
&lt;li&gt;DISFAVORED (if they don't love that shift)
&lt;/li&gt;
&lt;li&gt;REFUSED (if they absolutely cannot work that shift)
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;
The tool is supposed to allocate workers to the shifts to try to cover all the
shifts, give everybody work, and try to match their preferences. I implemented
the tool:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; sys
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; os
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; re

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;report_solution_to_console&lt;/span&gt;(&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;):
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; w &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; days_of_week:
        &lt;span style="color: #cdcd00;"&gt;annotation&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;''&lt;/span&gt;
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; human_annotate &lt;span style="color: #00cdcd; font-weight: bold;"&gt;is&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;:
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys():
                &lt;span style="color: #cdcd00;"&gt;m&lt;/span&gt; = re.match(rf&lt;span style="color: #00cd00;"&gt;'{w} - '&lt;/span&gt;, s)
                &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; m: &lt;span style="color: #00cdcd; font-weight: bold;"&gt;continue&lt;/span&gt;
                &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[human_annotate][s].value():
                    &lt;span style="color: #cdcd00;"&gt;annotation&lt;/span&gt; = f&lt;span style="color: #00cd00;"&gt;" ({human_annotate} SCHEDULED)"&lt;/span&gt;
                    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;break&lt;/span&gt;
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;len&lt;/span&gt;(annotation):
                &lt;span style="color: #cdcd00;"&gt;annotation&lt;/span&gt; = f&lt;span style="color: #00cd00;"&gt;" ({human_annotate} OFF)"&lt;/span&gt;

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"{w}{annotation}"&lt;/span&gt;)

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys():
            &lt;span style="color: #cdcd00;"&gt;m&lt;/span&gt; = re.match(rf&lt;span style="color: #00cd00;"&gt;'{w} - '&lt;/span&gt;, s)
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; m: &lt;span style="color: #00cdcd; font-weight: bold;"&gt;continue&lt;/span&gt;

            &lt;span style="color: #cdcd00;"&gt;annotation&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;''&lt;/span&gt;
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; human_annotate &lt;span style="color: #00cdcd; font-weight: bold;"&gt;is&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;:
                &lt;span style="color: #cdcd00;"&gt;annotation&lt;/span&gt; = f&lt;span style="color: #00cd00;"&gt;" ({human_annotate} {shifts[s][human_annotate]})"&lt;/span&gt;
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"    ---- {s[m.end():]}{annotation}"&lt;/span&gt;)

            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
                &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s].value():
                    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"         {h} ({shifts[s][h]})"&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;report_solution_summary_to_console&lt;/span&gt;(&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;):
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"\nSUMMARY"&lt;/span&gt;)

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"-- {h}"&lt;/span&gt;)
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"   benefit: {benefits[h].value():.3f}"&lt;/span&gt;)

        &lt;span style="color: #cdcd00;"&gt;counts&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; a &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; availabilities:
            &lt;span style="color: #cdcd00;"&gt;counts&lt;/span&gt;[a] = 0

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys():
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s].value():
                counts[shifts[s][h]] += 1

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; a &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; availabilities:
            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"   {counts[a]} {a}"&lt;/span&gt;)


&lt;span style="color: #cdcd00;"&gt;human_annotate&lt;/span&gt; = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;

&lt;span style="color: #cdcd00;"&gt;days_of_week&lt;/span&gt; = (&lt;span style="color: #00cd00;"&gt;'SUNDAY'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'MONDAY'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'TUESDAY'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'WEDNESDAY'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'THURSDAY'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'FRIDAY'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'SATURDAY'&lt;/span&gt;)

&lt;span style="color: #cdcd00;"&gt;humans&lt;/span&gt; = [&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;,
          &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;]

&lt;span style="color: #cdcd00;"&gt;shifts&lt;/span&gt; = {&lt;span style="color: #00cd00;"&gt;'SUNDAY - SANDING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'WEDNESDAY - SAWING 7:30 AM - 2:30 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'THURSDAY - SANDING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SATURDAY - SAWING 7:30 AM - 2:30 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SUNDAY - SAWING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'MONDAY - SAWING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'TUESDAY - SAWING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'WEDNESDAY - PAINTING 7:30 AM - 2:30 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'THURSDAY - SAWING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'FRIDAY - SAWING 9:00 AM - 4:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SATURDAY - PAINTING 7:30 AM - 2:30 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SUNDAY - PAINTING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'MONDAY - PAINTING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'TUESDAY - PAINTING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'WEDNESDAY - SANDING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'THURSDAY - PAINTING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'FRIDAY - PAINTING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SATURDAY - SANDING 9:45 AM - 4:45 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SUNDAY - PAINTING 11:00 AM - 6:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'MONDAY - PAINTING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'TUESDAY - PAINTING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'WEDNESDAY - PAINTING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'THURSDAY - PAINTING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'FRIDAY - PAINTING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SATURDAY - PAINTING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SUNDAY - SAWING 12:00 PM - 7:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'MONDAY - SAWING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'TUESDAY - SAWING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'WEDNESDAY - SAWING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'THURSDAY - SAWING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'FRIDAY - SAWING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SATURDAY - SAWING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SUNDAY - PAINTING 12:15 PM - 7:15 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'MONDAY - PAINTING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'TUESDAY - PAINTING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'WEDNESDAY - PAINTING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'THURSDAY - PAINTING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'FRIDAY - PAINTING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;},
          &lt;span style="color: #00cd00;"&gt;'SATURDAY - PAINTING 2:00 PM - 9:00 PM'&lt;/span&gt;:
          {&lt;span style="color: #00cd00;"&gt;'ALICE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'BOB'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'CAROL'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'FRANK'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'HEIDI'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'IVAN'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'JUDY'&lt;/span&gt;:  &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'EVE'&lt;/span&gt;:   &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'GRACE'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;,
           &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;: &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;}}

&lt;span style="color: #cdcd00;"&gt;availabilities&lt;/span&gt; = [&lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;]



&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; pulp
&lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; = pulp.LpProblem(&lt;span style="color: #00cd00;"&gt;"Scheduling"&lt;/span&gt;, pulp.LpMaximize)

&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt; = pulp.LpVariable.dicts(&lt;span style="color: #00cd00;"&gt;"Assignments"&lt;/span&gt;,
                             (humans, shifts.keys()),
                             &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;,&lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;, &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;bounds; unused, since these are binary variables&lt;/span&gt;
                             pulp.LpBinary)

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Everyone works at least 2 shifts&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;Nshifts_min&lt;/span&gt; = 2
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
        pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys()]) &amp;gt;= Nshifts_min,
        f&lt;span style="color: #00cd00;"&gt;"{h} works at least {Nshifts_min} shifts"&lt;/span&gt;,
    )

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;each shift is ~ 8 hours, so I limit everyone to 40/8 = 5 shifts&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;Nshifts_max&lt;/span&gt; = 5
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
        pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys()]) &amp;lt;= Nshifts_max,
        f&lt;span style="color: #00cd00;"&gt;"{h} works at most {Nshifts_max} shifts"&lt;/span&gt;,
    )

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;all shifts staffed and not double-staffed&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys():
    &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
        pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans]) == 1,
        f&lt;span style="color: #00cd00;"&gt;"{s} is staffed"&lt;/span&gt;,
    )

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;each human can work at most one shift on any given day&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; w &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; days_of_week:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
        &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
            pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys() &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; re.match(rf&lt;span style="color: #00cd00;"&gt;'{w} '&lt;/span&gt;,s)]) &amp;lt;= 1,
            f&lt;span style="color: #00cd00;"&gt;"{h} cannot be double-booked on {w}"&lt;/span&gt;
        )


&lt;span style="color: #cdcd00;"&gt;#### &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Some explicit constraints; as an example&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;DAVID can't work any PAINTING shift and is off on Thu and Sun&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;h&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;'DAVID'&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
    pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys() &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; re.search(r&lt;span style="color: #00cd00;"&gt;'- PAINTING'&lt;/span&gt;,s)]) == 0,
    f&lt;span style="color: #00cd00;"&gt;"{h} can't work any PAINTING shift"&lt;/span&gt;
)
&lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
    pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys() &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; re.match(r&lt;span style="color: #00cd00;"&gt;'THURSDAY|SUNDAY'&lt;/span&gt;,s)]) == 0,
    f&lt;span style="color: #00cd00;"&gt;"{h} is off on Thursday and Sunday"&lt;/span&gt;
)

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Do not assign any "REFUSED" shifts&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys():
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; shifts[s][h] == &lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt;:
            &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
                &lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] == 0,
                f&lt;span style="color: #00cd00;"&gt;"{h} is not available for {s}"&lt;/span&gt;
            )


&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Objective. I try to maximize the "happiness". Each human sees each shift as&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;one of:&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;#   &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;PREFERRED&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;#   &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;NEUTRAL&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;#   &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;DISFAVORED&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;#   &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;REFUSED&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;I set a hard constraint to handle "REFUSED", and arbitrarily, I set these&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;benefit values for the others&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;]  = 3
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;]    = 2
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;] = 1

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Not used, since this is a hard constraint. But the code needs this to be a&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;part of the benefit. I can ignore these in the code, but let's keep this&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;simple&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'REFUSED'&lt;/span&gt; ] = -1000

&lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt;[h] = \
        pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] * benefit_availability[shifts[s][h]] \
                    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys()])

&lt;span style="color: #cdcd00;"&gt;benefit_total&lt;/span&gt; = \
    pulp.lpSum([benefits[h] \
                &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans])

&lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (
    benefit_total,
    &lt;span style="color: #00cd00;"&gt;"happiness"&lt;/span&gt;,
)

prob.solve()

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; pulp.LpStatus[prob.status] == &lt;span style="color: #00cd00;"&gt;"Optimal"&lt;/span&gt;:
    report_solution_to_console(&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;)
    report_solution_summary_to_console(&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The set of workers is in the &lt;code&gt;humans&lt;/code&gt; variable, and the shift schedule and the
workers' preferences are encoded in the &lt;code&gt;shifts&lt;/code&gt; dict. The problem is defined by
a &lt;code&gt;vars&lt;/code&gt; dict of dicts, each a boolean variable indicating whether a particular
worker is scheduled for a particular shift. We define a set of constraints to
these worker allocations to restrict ourselves to &lt;i&gt;valid&lt;/i&gt; solutions. And among
these valid solutions, we try to find the one that maximizes some &lt;i&gt;benefit&lt;/i&gt;
function, defined here as:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'PREFERRED'&lt;/span&gt;]  = 3
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'NEUTRAL'&lt;/span&gt;]    = 2
&lt;span style="color: #cdcd00;"&gt;benefit_availability&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'DISFAVORED'&lt;/span&gt;] = 1

&lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt;[h] = \
        pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] * benefit_availability[shifts[s][h]] \
                    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys()])

&lt;span style="color: #cdcd00;"&gt;benefit_total&lt;/span&gt; = \
    pulp.lpSum([benefits[h] \
                &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans])
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
So for instance each shift that was scheduled as somebody's PREFERRED shift
gives us 3 benefit points. And if all the shifts ended up being PREFERRED, we'd
have a total benefit value of 3*Nshifts. This is impossible, however, because
that would violate some constraints in the problem.
&lt;/p&gt;

&lt;p&gt;
The exact trade-off between the different preferences is set in the
&lt;code&gt;benefit_availability&lt;/code&gt; dict. With the above numbers, it's equally good for
somebody to have a NEUTRAL shift and a day off as it is for them to have
DISFAVORED shifts. If we really want to encourage the program to work people as
much as possible (days off discouraged), we'd want to raise the DISFAVORED
threshold.
&lt;/p&gt;

&lt;p&gt;
I run this program and I get:
&lt;/p&gt;

&lt;pre class="example"&gt;
....
Result - Optimal solution found

Objective value:                108.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.01
Time (Wallclock seconds):       0.01

Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.02   (Wallclock seconds):       0.02

SUNDAY
    ---- SANDING 9:00 AM - 4:00 PM
         EVE (PREFERRED)
    ---- SAWING 9:00 AM - 4:00 PM
         IVAN (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM
         FRANK (PREFERRED)
    ---- PAINTING 11:00 AM - 6:00 PM
         HEIDI (PREFERRED)
    ---- SAWING 12:00 PM - 7:00 PM
         ALICE (PREFERRED)
    ---- PAINTING 12:15 PM - 7:15 PM
         CAROL (PREFERRED)
MONDAY
    ---- SAWING 9:00 AM - 4:00 PM
         DAVID (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM
         IVAN (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM
         GRACE (PREFERRED)
    ---- SAWING 2:00 PM - 9:00 PM
         ALICE (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM
         HEIDI (NEUTRAL)
TUESDAY
    ---- SAWING 9:00 AM - 4:00 PM
         DAVID (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM
         EVE (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM
         FRANK (NEUTRAL)
    ---- SAWING 2:00 PM - 9:00 PM
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM
         HEIDI (NEUTRAL)
WEDNESDAY
    ---- SAWING 7:30 AM - 2:30 PM
         DAVID (PREFERRED)
    ---- PAINTING 7:30 AM - 2:30 PM
         IVAN (PREFERRED)
    ---- SANDING 9:45 AM - 4:45 PM
         FRANK (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM
         JUDY (PREFERRED)
    ---- SAWING 2:00 PM - 9:00 PM
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM
         ALICE (NEUTRAL)
THURSDAY
    ---- SANDING 9:00 AM - 4:00 PM
         GRACE (PREFERRED)
    ---- SAWING 9:00 AM - 4:00 PM
         CAROL (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM
         EVE (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM
         JUDY (PREFERRED)
    ---- SAWING 2:00 PM - 9:00 PM
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM
         ALICE (NEUTRAL)
FRIDAY
    ---- SAWING 9:00 AM - 4:00 PM
         DAVID (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM
         FRANK (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM
         GRACE (NEUTRAL)
    ---- SAWING 2:00 PM - 9:00 PM
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM
         HEIDI (NEUTRAL)
SATURDAY
    ---- SAWING 7:30 AM - 2:30 PM
         CAROL (PREFERRED)
    ---- PAINTING 7:30 AM - 2:30 PM
         IVAN (PREFERRED)
    ---- SANDING 9:45 AM - 4:45 PM
         DAVID (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM
         FRANK (NEUTRAL)
    ---- SAWING 2:00 PM - 9:00 PM
         ALICE (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM
         BOB (NEUTRAL)

SUMMARY
-- ALICE
   benefit: 13.000
   3 PREFERRED
   2 NEUTRAL
   0 DISFAVORED
-- BOB
   benefit: 14.000
   4 PREFERRED
   1 NEUTRAL
   0 DISFAVORED
-- CAROL
   benefit: 9.000
   3 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- DAVID
   benefit: 15.000
   5 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- EVE
   benefit: 9.000
   3 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- FRANK
   benefit: 13.000
   3 PREFERRED
   2 NEUTRAL
   0 DISFAVORED
-- GRACE
   benefit: 8.000
   2 PREFERRED
   1 NEUTRAL
   0 DISFAVORED
-- HEIDI
   benefit: 9.000
   1 PREFERRED
   3 NEUTRAL
   0 DISFAVORED
-- IVAN
   benefit: 12.000
   4 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- JUDY
   benefit: 6.000
   2 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
&lt;/pre&gt;

&lt;p&gt;
So we have a solution! We have 108 total benefit points. But it looks a bit
uneven: Judy only works 2 days, while some people work many more: David works 5
for instance. Why is that? I update the program with =human_annotate = 'JUDY'=,
run it again, and it tells me more about Judy's preferences:
&lt;/p&gt;

&lt;pre class="example"&gt;
Objective value:                108.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.01
Time (Wallclock seconds):       0.01

Option for printingOptions changed from normal to all
Total time (CPU seconds):       0.01   (Wallclock seconds):       0.02

SUNDAY (JUDY OFF)
    ---- SANDING 9:00 AM - 4:00 PM (JUDY NEUTRAL)
         EVE (PREFERRED)
    ---- SAWING 9:00 AM - 4:00 PM (JUDY PREFERRED)
         IVAN (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM (JUDY PREFERRED)
         FRANK (PREFERRED)
    ---- PAINTING 11:00 AM - 6:00 PM (JUDY NEUTRAL)
         HEIDI (PREFERRED)
    ---- SAWING 12:00 PM - 7:00 PM (JUDY PREFERRED)
         ALICE (PREFERRED)
    ---- PAINTING 12:15 PM - 7:15 PM (JUDY NEUTRAL)
         CAROL (PREFERRED)
MONDAY (JUDY OFF)
    ---- SAWING 9:00 AM - 4:00 PM (JUDY PREFERRED)
         DAVID (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM (JUDY NEUTRAL)
         IVAN (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM (JUDY NEUTRAL)
         GRACE (PREFERRED)
    ---- SAWING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         ALICE (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         HEIDI (NEUTRAL)
TUESDAY (JUDY OFF)
    ---- SAWING 9:00 AM - 4:00 PM (JUDY PREFERRED)
         DAVID (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM (JUDY PREFERRED)
         EVE (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM (JUDY REFUSED)
         FRANK (NEUTRAL)
    ---- SAWING 2:00 PM - 9:00 PM (JUDY REFUSED)
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM (JUDY REFUSED)
         HEIDI (NEUTRAL)
WEDNESDAY (JUDY SCHEDULED)
    ---- SAWING 7:30 AM - 2:30 PM (JUDY REFUSED)
         DAVID (PREFERRED)
    ---- PAINTING 7:30 AM - 2:30 PM (JUDY REFUSED)
         IVAN (PREFERRED)
    ---- SANDING 9:45 AM - 4:45 PM (JUDY NEUTRAL)
         FRANK (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM (JUDY PREFERRED)
         JUDY (PREFERRED)
    ---- SAWING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         ALICE (NEUTRAL)
THURSDAY (JUDY SCHEDULED)
    ---- SANDING 9:00 AM - 4:00 PM (JUDY PREFERRED)
         GRACE (PREFERRED)
    ---- SAWING 9:00 AM - 4:00 PM (JUDY PREFERRED)
         CAROL (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM (JUDY PREFERRED)
         EVE (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM (JUDY PREFERRED)
         JUDY (PREFERRED)
    ---- SAWING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         ALICE (NEUTRAL)
FRIDAY (JUDY OFF)
    ---- SAWING 9:00 AM - 4:00 PM (JUDY DISFAVORED)
         DAVID (PREFERRED)
    ---- PAINTING 9:45 AM - 4:45 PM (JUDY DISFAVORED)
         FRANK (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM (JUDY DISFAVORED)
         GRACE (NEUTRAL)
    ---- SAWING 2:00 PM - 9:00 PM (JUDY REFUSED)
         BOB (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM (JUDY REFUSED)
         HEIDI (NEUTRAL)
SATURDAY (JUDY OFF)
    ---- SAWING 7:30 AM - 2:30 PM (JUDY REFUSED)
         CAROL (PREFERRED)
    ---- PAINTING 7:30 AM - 2:30 PM (JUDY REFUSED)
         IVAN (PREFERRED)
    ---- SANDING 9:45 AM - 4:45 PM (JUDY REFUSED)
         DAVID (PREFERRED)
    ---- PAINTING 12:00 PM - 7:00 PM (JUDY DISFAVORED)
         FRANK (NEUTRAL)
    ---- SAWING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         ALICE (PREFERRED)
    ---- PAINTING 2:00 PM - 9:00 PM (JUDY DISFAVORED)
         BOB (NEUTRAL)

SUMMARY
-- ALICE
   benefit: 13.000
   3 PREFERRED
   2 NEUTRAL
   0 DISFAVORED
-- BOB
   benefit: 14.000
   4 PREFERRED
   1 NEUTRAL
   0 DISFAVORED
-- CAROL
   benefit: 9.000
   3 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- DAVID
   benefit: 15.000
   5 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- EVE
   benefit: 9.000
   3 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- FRANK
   benefit: 13.000
   3 PREFERRED
   2 NEUTRAL
   0 DISFAVORED
-- GRACE
   benefit: 8.000
   2 PREFERRED
   1 NEUTRAL
   0 DISFAVORED
-- HEIDI
   benefit: 9.000
   1 PREFERRED
   3 NEUTRAL
   0 DISFAVORED
-- IVAN
   benefit: 12.000
   4 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
-- JUDY
   benefit: 6.000
   2 PREFERRED
   0 NEUTRAL
   0 DISFAVORED
&lt;/pre&gt;

&lt;p&gt;
This tells us that on Monday Judy does not work, although she marked the SAWING
shift as PREFERRED. Instead David got that shift. What would happen if David
gave that shift to Judy? He would lose 3 points, she would gain 3 points, and
the total would remain exactly the same at 108.
&lt;/p&gt;

&lt;p&gt;
How would we favor a more even distribution? We need some sort of tie-break. I
want to add a nonlinearity to strongly disfavor people getting a low number of
shifts. But PuLP is very explicitly a &lt;i&gt;linear&lt;/i&gt; programming solver, and cannot
solve nonlinear problems. Here we can get around this by enumerating each
specific case, and assigning it a nonlinear benefit function. The most obvious
approach is to define another set of boolean variables:
&lt;code&gt;vars_Nshifts[human][N]&lt;/code&gt;. And then using them to add extra benefit terms, with
values nonlinearly related to &lt;code&gt;Nshifts&lt;/code&gt;. Something like this:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;benefit_boost_Nshifts&lt;/span&gt; = \
    {2: -0.8,
     3: -0.5,
     4: -0.3,
     5: -0.2}
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt;[h] = \
        ... + \
        pulp.lpSum([vars_Nshifts[h][n] * benefit_boost_Nshifts[n] \
                    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; n &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; benefit_boost_Nshifts.keys()])
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
So in the previous example we considered giving David's 5th shift to Judy, for
her 3rd shift. In that scenario, David's extra benefit would change from -0.2 to
-0.3 (a shift of -0.1), while Judy's would change from -0.8 to -0.5 (a shift of
+0.3). So the balancing out the shifts in this way would work: the solver would
favor the solution with the higher benefit function.
&lt;/p&gt;

&lt;p&gt;
Great. In order for this to work, we need the &lt;code&gt;vars_Nshifts[human][N]&lt;/code&gt; variables
to function as intended: they need to be binary indicators of whether a specific
person has that many shifts or not. That would need to be implemented with
constraints. Let's plot it like this:
&lt;/p&gt;


&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; np
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; gnuplotlib &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; gp

&lt;span style="color: #cdcd00;"&gt;Nshifts_eq&lt;/span&gt;  = 4
&lt;span style="color: #cdcd00;"&gt;Nshifts_max&lt;/span&gt; = 10

&lt;span style="color: #cdcd00;"&gt;Nshifts&lt;/span&gt; = np.arange(Nshifts_max+1)
&lt;span style="color: #cdcd00;"&gt;i0&lt;/span&gt; = np.nonzero(Nshifts != Nshifts_eq)[0]
&lt;span style="color: #cdcd00;"&gt;i1&lt;/span&gt; = np.nonzero(Nshifts == Nshifts_eq)[0]

gp.plot( &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;True value: var_Nshifts4==0, Nshifts!=4&lt;/span&gt;
         ( np.zeros(i0.shape),
           Nshifts[i0],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "red"'&lt;/span&gt;) ),
         &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;True value: var_Nshifts4==1, Nshifts==4&lt;/span&gt;
         ( np.ones(i1.shape),
           Nshifts[i1],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "red"'&lt;/span&gt;) ),
         &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;False value: var_Nshifts4==1, Nshifts!=4&lt;/span&gt;
         ( np.ones(i0.shape),
           Nshifts[i0],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "black"'&lt;/span&gt;) ),
         &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;False value: var_Nshifts4==0, Nshifts==4&lt;/span&gt;
         ( np.zeros(i1.shape),
           Nshifts[i1],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "black"'&lt;/span&gt;) ),
        unset=(&lt;span style="color: #00cd00;"&gt;'grid'&lt;/span&gt;),
        _set = (f&lt;span style="color: #00cd00;"&gt;'xtics ("(Nshifts=={Nshifts_eq}) == 0" 0, "(Nshifts=={Nshifts_eq}) == 1" 1)'&lt;/span&gt;),
        _xrange = (-0.1, 1.1),
        ylabel = &lt;span style="color: #00cd00;"&gt;"Nshifts"&lt;/span&gt;,
        title = &lt;span style="color: #00cd00;"&gt;"Nshifts equality variable: not linearly separable"&lt;/span&gt;,
        hardcopy = &lt;span style="color: #00cd00;"&gt;"/tmp/scheduling-Nshifts-eq.svg"&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/05_shop-scheduling-with-pulp/scheduling-Nshifts-eq.svg" alt="scheduling-Nshifts-eq.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
So a hypothetical &lt;code&gt;vars_Nshifts[h][4]&lt;/code&gt; variable (plotted on the x axis of this
plot) would need to be defined by a set of linear AND constraints to &lt;a href="https://en.wikipedia.org/wiki/Linear_separability"&gt;linearly
separate&lt;/a&gt; the true (red) values of this variable from the false (black) values.
As can be seen in this plot, this isn't possible. So this representation does
&lt;i&gt;not&lt;/i&gt; work.
&lt;/p&gt;

&lt;p&gt;
How do we fix it? We can use inequality variables instead. I define a different
set of variables &lt;code&gt;vars_Nshifts_leq[human][N]&lt;/code&gt; that are 1 iff &lt;code&gt;Nshifts&lt;/code&gt; &amp;lt;= &lt;code&gt;N&lt;/code&gt;.
The equality variable from before can be expressed as a difference of these
inequality variables: &lt;code&gt;vars_Nshifts[human][N] =
vars_Nshifts_leq[human][N]-vars_Nshifts_leq[human][N-1]&lt;/code&gt;
&lt;/p&gt;

&lt;p&gt;
Can these &lt;code&gt;vars_Nshifts_leq&lt;/code&gt; variables be defined by a set of linear AND
constraints? Yes:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; np
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; numpysane &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; nps
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; gnuplotlib &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; gp

&lt;span style="color: #cdcd00;"&gt;Nshifts_leq&lt;/span&gt; = 4
&lt;span style="color: #cdcd00;"&gt;Nshifts_max&lt;/span&gt; = 10

&lt;span style="color: #cdcd00;"&gt;Nshifts&lt;/span&gt; = np.arange(Nshifts_max+1)
&lt;span style="color: #cdcd00;"&gt;i0&lt;/span&gt; = np.nonzero(Nshifts &amp;gt;  Nshifts_leq)[0]
&lt;span style="color: #cdcd00;"&gt;i1&lt;/span&gt; = np.nonzero(Nshifts &amp;lt;= Nshifts_leq)[0]

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;linear_slope_yintercept&lt;/span&gt;(xy0,xy1):
    &lt;span style="color: #cdcd00;"&gt;m&lt;/span&gt; = (xy1[1] - xy0[1])/(xy1[0] - xy0[0])
    &lt;span style="color: #cdcd00;"&gt;b&lt;/span&gt; = xy1[1] - m * xy1[0]
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; np.array(( m, b ))
&lt;span style="color: #cdcd00;"&gt;x01&lt;/span&gt;     = np.arange(2)
&lt;span style="color: #cdcd00;"&gt;x01_one&lt;/span&gt; = nps.glue( nps.transpose(x01), np.ones((2,1)), axis=-1)
&lt;span style="color: #cdcd00;"&gt;y_lowerbound&lt;/span&gt; = nps.inner(x01_one,
                         linear_slope_yintercept( np.array((0, Nshifts_leq+1)),
                                                  np.array((1, 0)) ))
&lt;span style="color: #cdcd00;"&gt;y_upperbound&lt;/span&gt; = nps.inner(x01_one,
                         linear_slope_yintercept( np.array((0, Nshifts_max)),
                                                  np.array((1, Nshifts_leq)) ))
&lt;span style="color: #cdcd00;"&gt;y_lowerbound_check&lt;/span&gt; = (1-x01) * (Nshifts_leq+1)
&lt;span style="color: #cdcd00;"&gt;y_upperbound_check&lt;/span&gt; = Nshifts_max - x01*(Nshifts_max-Nshifts_leq)

gp.plot( &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;True value: var_Nshifts_leq4==0, Nshifts&amp;gt;4&lt;/span&gt;
         ( np.zeros(i0.shape),
           Nshifts[i0],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "red"'&lt;/span&gt;) ),
         &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;True value: var_Nshifts_leq4==1, Nshifts&amp;lt;=4&lt;/span&gt;
         ( np.ones(i1.shape),
           Nshifts[i1],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "red"'&lt;/span&gt;) ),
         &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;False value: var_Nshifts_leq4==1, Nshifts&amp;gt;4&lt;/span&gt;
         ( np.ones(i0.shape),
           Nshifts[i0],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "black"'&lt;/span&gt;) ),
         &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;False value: var_Nshifts_leq4==0, Nshifts&amp;lt;=4&lt;/span&gt;
         ( np.zeros(i1.shape),
           Nshifts[i1],
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;(_with     = &lt;span style="color: #00cd00;"&gt;'points pt 7 ps 1 lc "black"'&lt;/span&gt;) ),

         ( x01, y_lowerbound, y_upperbound,
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;( _with     = &lt;span style="color: #00cd00;"&gt;'filledcurves lc "green"'&lt;/span&gt;,
                 tuplesize = 3) ),
         ( x01, nps.cat(y_lowerbound_check, y_upperbound_check),
           &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;( _with     = &lt;span style="color: #00cd00;"&gt;'lines lc "green" lw 2'&lt;/span&gt;,
                 tuplesize = 2) ),

        unset=(&lt;span style="color: #00cd00;"&gt;'grid'&lt;/span&gt;),
        _set = (f&lt;span style="color: #00cd00;"&gt;'xtics ("(Nshifts&amp;lt;={Nshifts_leq}) == 0" 0, "(Nshifts&amp;lt;={Nshifts_leq}) == 1" 1)'&lt;/span&gt;,
                &lt;span style="color: #00cd00;"&gt;'style fill transparent pattern 1'&lt;/span&gt;),
        _xrange = (-0.1, 1.1),
        ylabel = &lt;span style="color: #00cd00;"&gt;"Nshifts"&lt;/span&gt;,
        title = &lt;span style="color: #00cd00;"&gt;"Nshifts inequality variable: linearly separable"&lt;/span&gt;,
        hardcopy = &lt;span style="color: #00cd00;"&gt;"/tmp/scheduling-Nshifts-leq.svg"&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/03/05_shop-scheduling-with-pulp/scheduling-Nshifts-leq.svg" alt="scheduling-Nshifts-leq.svg" width="80%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
So we can use two linear constraints to make each of these variables work
properly. To use these in the benefit function we can use the equality
constraint expression from above, or we can use these directly:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;I want to favor people getting more extra shifts at the start to balance&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;things out: somebody getting one more shift on their pile shouldn't take&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;shifts away from under-utilized people&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;benefit_boost_leq_bound&lt;/span&gt; = \
    {2: .2,
     3: .3,
     4: .4,
     5: .5}

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Constrain vars_Nshifts_leq variables to do the right thing&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; b &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; benefit_boost_leq_bound.keys():
        &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys()])
                 &amp;gt;= (1 - vars_Nshifts_leq[h][b])*(b+1),
                 f&lt;span style="color: #00cd00;"&gt;"{h} at least {b} shifts: lower bound"&lt;/span&gt;)
        &lt;span style="color: #cdcd00;"&gt;prob&lt;/span&gt; += (pulp.lpSum([&lt;span style="color: #0000ee; font-weight: bold;"&gt;vars&lt;/span&gt;[h][s] &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; s &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; shifts.keys()])
                 &amp;lt;= Nshifts_max - vars_Nshifts_leq[h][b]*(Nshifts_max-b),
                 f&lt;span style="color: #00cd00;"&gt;"{h} at least {b} shifts: upper bound"&lt;/span&gt;)

&lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; h &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; humans:
    &lt;span style="color: #cdcd00;"&gt;benefits&lt;/span&gt;[h] = \
        ... + \
        pulp.lpSum([vars_Nshifts_leq[h][b] * benefit_boost_leq_bound[b] \
                    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; b &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; benefit_boost_leq_bound.keys()])
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
In &lt;i&gt;this&lt;/i&gt; scenario, David would get a boost of 0.4 from giving up his 5th shift,
while Judy would lose a boost of 0.2 from getting her 3rd, for a net gain of 0.2
benefit points. The exact numbers will need to be adjusted on a case by case
basis, but this works.
&lt;/p&gt;

&lt;p&gt;
The full program, with this and other extra features is available &lt;a href="../../../notes/2025/03/05_shop-scheduling-with-pulp/schedule.py"&gt;here&lt;/a&gt;.
&lt;/p&gt;
</content></entry><entry><title type="html">When are the days getting longer the fastest?</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2025/02/18_when-are-the-days-getting-longer-the-fastest.html"/><updated>2025-02-18T18:47:00Z</updated><published>2025-02-18T18:47:00Z</published><id>notes/2025/02/18_when-are-the-days-getting-longer-the-fastest.html</id><category scheme="/tags/data.html" term="data" label="data"/><category scheme="/tags/vnlog.html" term="vnlog" label="vnlog"/><category scheme="/tags/tools.html" term="tools" label="tools"/><content type="html">&lt;p&gt;
We're way past the winter solstice, and approaching the equinox. The
sun is noticeably staying up later and later every day, which raises
an obvious question: when are the days getting longer the fastest?
Intuitively I want to say it should happen at the equinox. But does it
happen &lt;i&gt;exactly&lt;/i&gt; at the equinox? I could read up on all the gory
details of this, or I could just make some plots. I wrote this:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; sys
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; datetime
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; astral.sun

&lt;span style="color: #cdcd00;"&gt;lat&lt;/span&gt;  = 34.
&lt;span style="color: #cdcd00;"&gt;year&lt;/span&gt; = 2025

&lt;span style="color: #cdcd00;"&gt;city&lt;/span&gt; = astral.LocationInfo(latitude=lat, longitude=0)

&lt;span style="color: #cdcd00;"&gt;date0&lt;/span&gt; = datetime.datetime(year, 1, 1)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"# date sunrise sunset length_min"&lt;/span&gt;)

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; i &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;range&lt;/span&gt;(365):
    &lt;span style="color: #cdcd00;"&gt;date&lt;/span&gt; = date0 + datetime.timedelta(days=i)

    &lt;span style="color: #cdcd00;"&gt;s&lt;/span&gt; = astral.sun.sun(city.observer, date=date)

    &lt;span style="color: #cdcd00;"&gt;date_sunrise&lt;/span&gt; = s[&lt;span style="color: #00cd00;"&gt;'sunrise'&lt;/span&gt;]
    &lt;span style="color: #cdcd00;"&gt;date_sunset&lt;/span&gt;  = s[&lt;span style="color: #00cd00;"&gt;'sunset'&lt;/span&gt;]

    &lt;span style="color: #cdcd00;"&gt;date_string&lt;/span&gt;    = date.strftime(&lt;span style="color: #00cd00;"&gt;'%Y-%m-%d'&lt;/span&gt;)
    &lt;span style="color: #cdcd00;"&gt;sunrise_string&lt;/span&gt; = date_sunrise.strftime(&lt;span style="color: #00cd00;"&gt;'%H:%M'&lt;/span&gt;)
    &lt;span style="color: #cdcd00;"&gt;sunset_string&lt;/span&gt;  = date_sunset.strftime (&lt;span style="color: #00cd00;"&gt;'%H:%M'&lt;/span&gt;)

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"{date_string} {sunrise_string} {sunset_string} {(date_sunset-date_sunrise).total_seconds()/60}"&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
This computes the sunrise and sunset time every day of 2025 at a latitude of
34degrees (i.e. Los Angeles), and writes out a &lt;a href="../../../notes/2025/02/18_when-are-the-days-getting-longer-the-fastest/sunrise-sunset.vnl"&gt;log file&lt;/a&gt; (using the &lt;a href="https://github.com/dkogan/vnlog"&gt;vnlog&lt;/a&gt;
format).
&lt;/p&gt;

&lt;p&gt;
Let's plot it:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;&amp;lt; sunrise-sunset.vnl                   &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  vnl-filter -p date,&lt;span style="color: #cdcd00;"&gt;l&lt;/span&gt;=&lt;span style="color: #00cd00;"&gt;'length_min/60'&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
| feedgnuplot                          &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --set &lt;span style="color: #00cd00;"&gt;'format x "%b %d"'&lt;/span&gt;             &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --domain                             &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --timefmt &lt;span style="color: #00cd00;"&gt;'%Y-%m-%d'&lt;/span&gt;                 &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --lines                              &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --ylabel &lt;span style="color: #00cd00;"&gt;'Day length (hours)'&lt;/span&gt;        &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --hardcopy day-length.svg
&lt;/pre&gt;
&lt;/div&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/02/18_when-are-the-days-getting-longer-the-fastest/day-length.svg" alt="day-length.svg" width="70%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
Well that makes sense. When are the days the longest/shortest?
&lt;/p&gt;

&lt;pre class="example"&gt;
$ &amp;lt; sunrise-sunset.vnl vnl-sort -grk length_min | head -n2 | vnl-align

#  date    sunrise sunset     length_min   
2025-06-21 04:49   19:14  864.8543702000001


$ &amp;lt; sunrise-sunset.vnl vnl-sort -gk length_min | head -n2 | vnl-align

#  date    sunrise sunset     length_min   
2025-12-21 07:01   16:54  592.8354265166668
&lt;/pre&gt;

&lt;p&gt;
Those are the solstices, as expected. Now let's look at the time gained/lost
each day:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-sh"&gt;$ &amp;lt; sunrise-sunset.vnl                                  &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  vnl-filter -p date,&lt;span style="color: #cdcd00;"&gt;d&lt;/span&gt;=&lt;span style="color: #00cd00;"&gt;'diff(length_min)'&lt;/span&gt;               &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
| vnl-filter --has d                                    &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
| feedgnuplot                                           &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --set &lt;span style="color: #00cd00;"&gt;'format x "%b %d"'&lt;/span&gt;                              &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --domain                                              &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --timefmt &lt;span style="color: #00cd00;"&gt;'%Y-%m-%d'&lt;/span&gt;                                  &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --lines                                               &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --ylabel &lt;span style="color: #00cd00;"&gt;'Daytime gained from the previous day (min)'&lt;/span&gt; &lt;span style="color: #00cd00;"&gt;\&lt;/span&gt;
  --hardcopy gain.svg
&lt;/pre&gt;
&lt;/div&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2025/02/18_when-are-the-days-getting-longer-the-fastest/gain.svg" alt="gain.svg" width="70%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
Looks vaguely sinusoidal, like the last plot. And looks like we gain/lost as
most ~2 minutes each day. When does the gain peak?
&lt;/p&gt;

&lt;pre class="example"&gt;
$ &amp;lt; sunrise-sunset.vnl vnl-filter -p date,d='diff(length_min)' | vnl-filter --has d | vnl-sort -grk d | head -n2 | vnl-align

#  date       d   
2025-03-19 2.13167


$ &amp;lt; sunrise-sunset.vnl vnl-filter -p date,d='diff(length_min)' | vnl-filter --has d | vnl-sort -gk d | head -n2 | vnl-align

#  date        d   
2025-09-25 -2.09886
&lt;/pre&gt;

&lt;p&gt;
&lt;i&gt;Not&lt;/i&gt; at the equinoxes! The fastest gain is a few days before the equinox and
the fastest loss a few days after.
&lt;/p&gt;
</content></entry><entry><title type="html">Strava track filtering validation</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2024/11/30_strava-track-filtering-validation.html"/><updated>2024-11-30T14:48:00Z</updated><published>2024-11-30T14:48:00Z</published><id>notes/2024/11/30_strava-track-filtering-validation.html</id><category scheme="/tags/data.html" term="data" label="data"/><content type="html">&lt;p&gt;
After years of seeing people's strava tracks, I became convinced that strava
doesn't sufficiently filter the data, resulting in over-estimated effort
numbers. Today I did a bit of lazy analysis, and half-confirmed this: in the one
case I looked at, strava reported reasonable elevation gain numbers, but greatly
overestimated the distance traveled.
&lt;/p&gt;

&lt;p&gt;
I looked at a single gps track of a long bike ride. This was uploaded to strava
manually, as a &lt;code&gt;.gpx&lt;/code&gt; file. I can imagine that something different happens if
you use the strava app or some device that integrates with the service: the
filtering might happen before the data hits the server, and the server could
then decide to not apply any additional filtering.
&lt;/p&gt;

&lt;p&gt;
I processed the data with a simple hysteretic filter, ignoring small changes in
position and elevation, trying out different thresholds in the process. I
completely ignore the timestamps, and only look at the differences between
successive points. This handles the usual GPS noise; it does &lt;i&gt;not&lt;/i&gt; handle GPS
jumps, which I completely ignore in this analysis. Ignoring these would produce
inflated elevation/gain numbers, but I'm working with a looong track, so
hopefully this is a small effect.
&lt;/p&gt;

&lt;p&gt;
Clearly this is not scientific, but it's something.
&lt;/p&gt;

&lt;div id="outline-container-sec-1" class="outline-2"&gt;
&lt;h2 id="sec-1"&gt;The code&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-1"&gt;
&lt;p&gt;
Parsing &lt;code&gt;.gpx&lt;/code&gt; is slow (this is a &lt;i&gt;big&lt;/i&gt; file), so I cache that into a &lt;a href="https://github.com/dkogan/vnlog"&gt;&lt;code&gt;.vnl&lt;/code&gt;&lt;/a&gt;:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; sys
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; gpxpy

&lt;span style="color: #cdcd00;"&gt;filename_in&lt;/span&gt;  = &lt;span style="color: #00cd00;"&gt;'INPUT.gpx'&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;filename_out&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;'OUTPUT.gpx'&lt;/span&gt;

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;with&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;open&lt;/span&gt;(filename_in, &lt;span style="color: #00cd00;"&gt;'r'&lt;/span&gt;) &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; f:
    &lt;span style="color: #cdcd00;"&gt;gpx&lt;/span&gt; = gpxpy.parse(f)

&lt;span style="color: #cdcd00;"&gt;f_out&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;open&lt;/span&gt;(filename_out, &lt;span style="color: #00cd00;"&gt;'w'&lt;/span&gt;)

&lt;span style="color: #cdcd00;"&gt;tracks&lt;/span&gt; = gpx.tracks
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;len&lt;/span&gt;(tracks) != 1:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"I want just one track"&lt;/span&gt;, &lt;span style="color: #0000ee; font-weight: bold;"&gt;file&lt;/span&gt;=sys.stderr)
    sys.&lt;span style="color: #cd00cd;"&gt;exit&lt;/span&gt;(1)
&lt;span style="color: #cdcd00;"&gt;track&lt;/span&gt; = tracks[0]

&lt;span style="color: #cdcd00;"&gt;segments&lt;/span&gt; = track.segments
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;len&lt;/span&gt;(segments) != 1:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"I want just one segment"&lt;/span&gt;, &lt;span style="color: #0000ee; font-weight: bold;"&gt;file&lt;/span&gt;=sys.stderr)
    sys.&lt;span style="color: #cd00cd;"&gt;exit&lt;/span&gt;(1)
&lt;span style="color: #cdcd00;"&gt;segment&lt;/span&gt; = segments[0]

&lt;span style="color: #cdcd00;"&gt;time0&lt;/span&gt; = segment.points[0].time
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(&lt;span style="color: #00cd00;"&gt;"# time lat lon ele_m"&lt;/span&gt;)
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; p &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; segment.points:
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;print&lt;/span&gt;(f&lt;span style="color: #00cd00;"&gt;"{(p.time - time0).seconds} {p.latitude} {p.longitude} {p.elevation}"&lt;/span&gt;,
          &lt;span style="color: #0000ee; font-weight: bold;"&gt;file&lt;/span&gt; = f_out)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
And I then process this data with the different filters (this is a silly Python
loop, and is slow):
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #cdcd00;"&gt;#&lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;!/usr/bin/python3&lt;/span&gt;

&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; sys
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; np
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; numpysane &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; nps
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; gnuplotlib &lt;span style="color: #00cdcd; font-weight: bold;"&gt;as&lt;/span&gt; gp
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; vnlog
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;import&lt;/span&gt; pyproj

&lt;span style="color: #cdcd00;"&gt;geod&lt;/span&gt; = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;dist_ft&lt;/span&gt;(lat0,lon0, lat1,lon1):

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;global&lt;/span&gt; geod
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; geod &lt;span style="color: #00cdcd; font-weight: bold;"&gt;is&lt;/span&gt; &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;:
        &lt;span style="color: #cdcd00;"&gt;geod&lt;/span&gt; = pyproj.Geod(ellps=&lt;span style="color: #00cd00;"&gt;'WGS84'&lt;/span&gt;)
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; \
        geod.inv(lon0,lat0, lon1,lat1)[2] * 100./2.54/12.




&lt;span style="color: #cdcd00;"&gt;f&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;'OUTPUT.gpx'&lt;/span&gt;

&lt;span style="color: #cdcd00;"&gt;track&lt;/span&gt;,&lt;span style="color: #cdcd00;"&gt;list_keys&lt;/span&gt;,&lt;span style="color: #cdcd00;"&gt;dict_key_index&lt;/span&gt; = \
    vnlog.slurp(f)

&lt;span style="color: #cdcd00;"&gt;t&lt;/span&gt;      = track[:,dict_key_index[&lt;span style="color: #00cd00;"&gt;'time'&lt;/span&gt; ]]
&lt;span style="color: #cdcd00;"&gt;lat&lt;/span&gt;    = track[:,dict_key_index[&lt;span style="color: #00cd00;"&gt;'lat'&lt;/span&gt;  ]]
&lt;span style="color: #cdcd00;"&gt;lon&lt;/span&gt;    = track[:,dict_key_index[&lt;span style="color: #00cd00;"&gt;'lon'&lt;/span&gt;  ]]
&lt;span style="color: #cdcd00;"&gt;ele_ft&lt;/span&gt; = track[:,dict_key_index[&lt;span style="color: #00cd00;"&gt;'ele_m'&lt;/span&gt;]] * 100./2.54/12.



&lt;span style="color: #00cd00;"&gt;@nps.broadcast_define&lt;/span&gt;( ( (), ()),
                       (2,))
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;filter_track&lt;/span&gt;(ele_hysteresis_ft,
                 dxy_hysteresis_ft):

    &lt;span style="color: #cdcd00;"&gt;dist&lt;/span&gt;        = 0.0
    &lt;span style="color: #cdcd00;"&gt;ele_gain_ft&lt;/span&gt; = 0.0

    &lt;span style="color: #cdcd00;"&gt;lon_accepted&lt;/span&gt; = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;
    &lt;span style="color: #cdcd00;"&gt;lat_accepted&lt;/span&gt; = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;
    &lt;span style="color: #cdcd00;"&gt;ele_accepted&lt;/span&gt; = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;for&lt;/span&gt; i &lt;span style="color: #00cdcd; font-weight: bold;"&gt;in&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;range&lt;/span&gt;(&lt;span style="color: #0000ee; font-weight: bold;"&gt;len&lt;/span&gt;(lat)):

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; ele_accepted &lt;span style="color: #00cdcd; font-weight: bold;"&gt;is&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;:
            &lt;span style="color: #cdcd00;"&gt;dxy_here&lt;/span&gt;  = dist_ft(lat_accepted,lon_accepted, lat[i],lon[i])
            &lt;span style="color: #cdcd00;"&gt;dele_here&lt;/span&gt; = np.&lt;span style="color: #0000ee; font-weight: bold;"&gt;abs&lt;/span&gt;( ele_ft[i] - ele_accepted )

            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; dxy_here &amp;lt; dxy_hysteresis_ft &lt;span style="color: #00cdcd; font-weight: bold;"&gt;and&lt;/span&gt; dele_here &amp;lt; ele_hysteresis_ft:
                &lt;span style="color: #00cdcd; font-weight: bold;"&gt;continue&lt;/span&gt;

            &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; ele_ft[i] &amp;gt; ele_accepted:
                &lt;span style="color: #cdcd00;"&gt;ele_gain_ft&lt;/span&gt; += dele_here;

            &lt;span style="color: #cdcd00;"&gt;dist&lt;/span&gt; += np.sqrt(dele_here * dele_here +
                            dxy_here  * dxy_here)

        &lt;span style="color: #cdcd00;"&gt;lon_accepted&lt;/span&gt; = lon[i]
        &lt;span style="color: #cdcd00;"&gt;lat_accepted&lt;/span&gt; = lat[i]
        &lt;span style="color: #cdcd00;"&gt;ele_accepted&lt;/span&gt; = ele_ft[i]

    &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;lose the last point. It simply doesn't matter&lt;/span&gt;

    &lt;span style="color: #cdcd00;"&gt;dist_mi&lt;/span&gt; = dist / 5280.
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; np.array((ele_gain_ft, dist_mi))




&lt;span style="color: #cdcd00;"&gt;Nele_hysteresis_ft&lt;/span&gt;    = 20
&lt;span style="color: #cdcd00;"&gt;ele_hysteresis0_ft&lt;/span&gt;    = 5
&lt;span style="color: #cdcd00;"&gt;ele_hysteresis1_ft&lt;/span&gt;    = 100
&lt;span style="color: #cdcd00;"&gt;ele_hysteresis_ft_all&lt;/span&gt; = np.linspace(ele_hysteresis0_ft,
                                    ele_hysteresis1_ft,
                                    Nele_hysteresis_ft)

&lt;span style="color: #cdcd00;"&gt;Ndxy_hysteresis_ft&lt;/span&gt; = 20
&lt;span style="color: #cdcd00;"&gt;dxy_hysteresis0_ft&lt;/span&gt; = 5
&lt;span style="color: #cdcd00;"&gt;dxy_hysteresis1_ft&lt;/span&gt; = 1000
&lt;span style="color: #cdcd00;"&gt;dxy_hysteresis_ft&lt;/span&gt;  = np.linspace(dxy_hysteresis0_ft,
                                 dxy_hysteresis1_ft,
                                 Ndxy_hysteresis_ft)


&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;shape (Nele,Ndxy,2)&lt;/span&gt;
&lt;span style="color: #cdcd00;"&gt;gain&lt;/span&gt;,&lt;span style="color: #cdcd00;"&gt;distance&lt;/span&gt; = \
    nps.mv( filter_track( nps.dummy(ele_hysteresis_ft_all,-1),
                          dxy_hysteresis_ft),
            -1,0 )


&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Stolen from mrcal&lt;/span&gt;
&lt;span style="color: #00cdcd; font-weight: bold;"&gt;def&lt;/span&gt; &lt;span style="color: #0000ee; font-weight: bold;"&gt;options_heatmap_with_contours&lt;/span&gt;( plotoptions, &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;we update this on output&lt;/span&gt;

                                   *,
                                   contour_min           = 0,
                                   contour_max,
                                   contour_increment     = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;,
                                   do_contours           = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt;,
                                   contour_labels_styles = &lt;span style="color: #00cd00;"&gt;'boxed'&lt;/span&gt;,
                                   contour_labels_font   = &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;):
    r&lt;span style="color: #00cd00;"&gt;'''Update plotoptions, return curveoptions for a contoured heat map'''&lt;/span&gt;

    gp.add_plot_option(plotoptions,
                       &lt;span style="color: #00cd00;"&gt;'set'&lt;/span&gt;,
                       (&lt;span style="color: #00cd00;"&gt;'view equal xy'&lt;/span&gt;,
                        &lt;span style="color: #00cd00;"&gt;'view map'&lt;/span&gt;))

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; do_contours:
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; contour_increment &lt;span style="color: #00cdcd; font-weight: bold;"&gt;is&lt;/span&gt; &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;:
            &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Compute a "nice" contour increment. I pick a round number that gives&lt;/span&gt;
            &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;me a reasonable number of contours&lt;/span&gt;

            &lt;span style="color: #cdcd00;"&gt;Nwant&lt;/span&gt; = 10
            &lt;span style="color: #cdcd00;"&gt;increment&lt;/span&gt; = (contour_max - contour_min)/Nwant

            &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;I find the nearest 1eX or 2eX or 5eX&lt;/span&gt;
            &lt;span style="color: #cdcd00;"&gt;base10_floor&lt;/span&gt; = np.power(10., np.floor(np.log10(increment)))

            &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Look through the options, and pick the best one&lt;/span&gt;
            &lt;span style="color: #cdcd00;"&gt;m&lt;/span&gt;   = np.array((1., 2., 5., 10.))
            &lt;span style="color: #cdcd00;"&gt;err&lt;/span&gt; = np.&lt;span style="color: #0000ee; font-weight: bold;"&gt;abs&lt;/span&gt;(m * base10_floor - increment)
            &lt;span style="color: #cdcd00;"&gt;contour_increment&lt;/span&gt; = -m[ np.argmin(err) ] * base10_floor

        gp.add_plot_option(plotoptions,
                           &lt;span style="color: #00cd00;"&gt;'set'&lt;/span&gt;,
                           (&lt;span style="color: #00cd00;"&gt;'key box opaque'&lt;/span&gt;,
                            &lt;span style="color: #00cd00;"&gt;'style textbox opaque'&lt;/span&gt;,
                            &lt;span style="color: #00cd00;"&gt;'contour base'&lt;/span&gt;,
                            f&lt;span style="color: #00cd00;"&gt;'cntrparam levels incremental {contour_max},{contour_increment},{contour_min}'&lt;/span&gt;))

        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;if&lt;/span&gt; contour_labels_font &lt;span style="color: #00cdcd; font-weight: bold;"&gt;is&lt;/span&gt; &lt;span style="color: #00cdcd; font-weight: bold;"&gt;not&lt;/span&gt; &lt;span style="color: #cd00cd;"&gt;None&lt;/span&gt;:
            gp.add_plot_option(plotoptions,
                               &lt;span style="color: #00cd00;"&gt;'set'&lt;/span&gt;,
                               f&lt;span style="color: #00cd00;"&gt;'cntrlabel format "%d" font "{contour_labels_font}"'&lt;/span&gt; )
        &lt;span style="color: #00cdcd; font-weight: bold;"&gt;else&lt;/span&gt;:
            gp.add_plot_option(plotoptions,
                               &lt;span style="color: #00cd00;"&gt;'set'&lt;/span&gt;,
                               f&lt;span style="color: #00cd00;"&gt;'cntrlabel format "%.0f"'&lt;/span&gt; )

        &lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'cbrange'&lt;/span&gt;] = [contour_min, contour_max]

        &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;I plot 3 times:&lt;/span&gt;
        &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;- to make the heat map&lt;/span&gt;
        &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;- to make the contours&lt;/span&gt;
        &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;- to make the contour labels&lt;/span&gt;
        &lt;span style="color: #cdcd00;"&gt;_with&lt;/span&gt; = np.array((&lt;span style="color: #00cd00;"&gt;'image'&lt;/span&gt;,
                          &lt;span style="color: #00cd00;"&gt;'lines nosurface'&lt;/span&gt;,
                          f&lt;span style="color: #00cd00;"&gt;'labels {contour_labels_styles} nosurface'&lt;/span&gt;))
    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;else&lt;/span&gt;:
        gp.add_plot_option(plotoptions, &lt;span style="color: #00cd00;"&gt;'unset'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'key'&lt;/span&gt;)
        &lt;span style="color: #cdcd00;"&gt;_with&lt;/span&gt; = &lt;span style="color: #00cd00;"&gt;'image'&lt;/span&gt;

    &lt;span style="color: #cdcd00;"&gt;using&lt;/span&gt; = \
        f&lt;span style="color: #00cd00;"&gt;'({dxy_hysteresis0_ft}+$1*{float(dxy_hysteresis1_ft-dxy_hysteresis0_ft)/(Ndxy_hysteresis_ft-1)}):'&lt;/span&gt; + \
        f&lt;span style="color: #00cd00;"&gt;'({ele_hysteresis0_ft}+$2*{float(ele_hysteresis1_ft-ele_hysteresis0_ft)/(Nele_hysteresis_ft-1)}):3'&lt;/span&gt;
    &lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'_3d'&lt;/span&gt;]     = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt;
    &lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'_xrange'&lt;/span&gt;] = [dxy_hysteresis0_ft,dxy_hysteresis1_ft]
    &lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'_yrange'&lt;/span&gt;] = [ele_hysteresis0_ft,ele_hysteresis1_ft]
    &lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt;[&lt;span style="color: #00cd00;"&gt;'ascii'&lt;/span&gt;]   = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt; &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;needed for using to work&lt;/span&gt;

    gp.add_plot_option(plotoptions, &lt;span style="color: #00cd00;"&gt;'unset'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'grid'&lt;/span&gt;)

    &lt;span style="color: #00cdcd; font-weight: bold;"&gt;return&lt;/span&gt; \
        &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;( tuplesize=3,
              legend = &lt;span style="color: #00cd00;"&gt;""&lt;/span&gt;, &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;needed to force contour labels&lt;/span&gt;
              using = using,
              _with=_with)




&lt;span style="color: #cdcd00;"&gt;contour_granularity&lt;/span&gt; = 1000
&lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #cdcd00;"&gt;curveoptions&lt;/span&gt; = \
    options_heatmap_with_contours( plotoptions, &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;we update this on output&lt;/span&gt;
                                   &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;round down to the nearest contour_granularity&lt;/span&gt;
                                   contour_min = (np.&lt;span style="color: #0000ee; font-weight: bold;"&gt;min&lt;/span&gt;(gain) // contour_granularity)*contour_granularity,
                                   &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;round up to the nearest contour_granularity&lt;/span&gt;
                                   contour_max = ((np.&lt;span style="color: #0000ee; font-weight: bold;"&gt;max&lt;/span&gt;(gain) + (contour_granularity-1)) // contour_granularity) * contour_granularity,
                                   do_contours = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt;)
gp.add_plot_option(plotoptions, &lt;span style="color: #00cd00;"&gt;'unset'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'key'&lt;/span&gt;)
gp.add_plot_option(plotoptions, &lt;span style="color: #00cd00;"&gt;'set'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'size square'&lt;/span&gt;)
gp.plot(gain,
        xlabel  = &lt;span style="color: #00cd00;"&gt;"Distance hysteresis (ft)"&lt;/span&gt;,
        ylabel  = &lt;span style="color: #00cd00;"&gt;"Elevation hysteresis (ft)"&lt;/span&gt;,
        cblabel = &lt;span style="color: #00cd00;"&gt;"Elevation gain (ft)"&lt;/span&gt;,
        wait = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt;,
        **curveoptions,
        **plotoptions,
        title    = &lt;span style="color: #00cd00;"&gt;'Computed gain vs filtering parameters'&lt;/span&gt;)


&lt;span style="color: #cdcd00;"&gt;contour_granularity&lt;/span&gt; = 10
&lt;span style="color: #cdcd00;"&gt;plotoptions&lt;/span&gt; = &lt;span style="color: #0000ee; font-weight: bold;"&gt;dict&lt;/span&gt;()
&lt;span style="color: #cdcd00;"&gt;curveoptions&lt;/span&gt; = \
    options_heatmap_with_contours( plotoptions, &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;we update this on output&lt;/span&gt;
                                   &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;round down to the nearest contour_granularity&lt;/span&gt;
                                   contour_min = (np.&lt;span style="color: #0000ee; font-weight: bold;"&gt;min&lt;/span&gt;(distance) // contour_granularity)*contour_granularity,
                                   &lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;round up to the nearest contour_granularity&lt;/span&gt;
                                   contour_max = ((np.&lt;span style="color: #0000ee; font-weight: bold;"&gt;max&lt;/span&gt;(distance) + (contour_granularity-1)) // contour_granularity) * contour_granularity,
                                   do_contours = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt;)
gp.add_plot_option(plotoptions, &lt;span style="color: #00cd00;"&gt;'unset'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'key'&lt;/span&gt;)
gp.add_plot_option(plotoptions, &lt;span style="color: #00cd00;"&gt;'set'&lt;/span&gt;, &lt;span style="color: #00cd00;"&gt;'size square'&lt;/span&gt;)
gp.plot(distance,
        xlabel  = &lt;span style="color: #00cd00;"&gt;"Distance hysteresis (ft)"&lt;/span&gt;,
        ylabel  = &lt;span style="color: #00cd00;"&gt;"Elevation hysteresis (ft)"&lt;/span&gt;,
        cblabel = &lt;span style="color: #00cd00;"&gt;"Distance (miles)"&lt;/span&gt;,
        wait = &lt;span style="color: #cd00cd;"&gt;True&lt;/span&gt;,
        **curveoptions,
        **plotoptions,
        title    = &lt;span style="color: #00cd00;"&gt;'Computed distance vs filtering parameters'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-sec-2" class="outline-2"&gt;
&lt;h2 id="sec-2"&gt;Results: gain&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-2"&gt;
&lt;p&gt;
Strava says the gain was 46307ft. The analysis says:
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2024/11/30_strava-track-filtering-validation/strava-gain.png" alt="strava-gain.png" width="70%" /&gt;
&lt;/p&gt;
&lt;/div&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2024/11/30_strava-track-filtering-validation/strava-gain-zoom.png" alt="strava-gain-zoom.png" width="70%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
These show the filtered gain for different values of the distance and gain
hysteresis thresholds. The same data is shown at diffent zoom levels. There's no
clear sweet spot, but we get 46307ft with a reasonable amount of filtering.
Maybe 46307ft is a bit low even.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;

&lt;div id="outline-container-sec-3" class="outline-2"&gt;
&lt;h2 id="sec-3"&gt;Results: distance&lt;/h2&gt;
&lt;div class="outline-text-2" id="text-3"&gt;
&lt;p&gt;
Strava says the distance covered was 322 miles. The analysis says:
&lt;/p&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2024/11/30_strava-track-filtering-validation/strava-distance.png" alt="strava-distance.png" width="70%" /&gt;
&lt;/p&gt;
&lt;/div&gt;


&lt;div class="figure"&gt;
&lt;p&gt;&lt;img src="../../../notes/2024/11/30_strava-track-filtering-validation/strava-distance-zoom.png" alt="strava-distance-zoom.png" width="70%" /&gt;
&lt;/p&gt;
&lt;/div&gt;

&lt;p&gt;
Once again, there's no sweet spot, but we get 322 miles only if we apply no
filtering at all. That's clearly too high, and is not reasonable. From the map
(and from other people's strava routes) the true distance is closer to 305
miles. Why &lt;i&gt;those&lt;/i&gt; people's strava numbers are more believable is anybody's
guess.
&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
</content></entry><entry><title type="html">GNU Make: details regarding intermediate files</title><author><name>Dima Kogan</name></author><link href="http://notes.secretsauce.net/notes/2024/09/08_gnu-make-details-regarding-intermediate-files.html"/><updated>2024-09-08T12:31:00Z</updated><published>2024-09-08T12:31:00Z</published><id>notes/2024/09/08_gnu-make-details-regarding-intermediate-files.html</id><category scheme="/tags/tools.html" term="tools" label="tools"/><content type="html">&lt;p&gt;
Check this out!
&lt;/p&gt;

&lt;p&gt;
Suppose I have this &lt;code&gt;Makefile&lt;/code&gt;:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-makefile"&gt;&lt;span style="color: #0000ee; font-weight: bold;"&gt;a&lt;/span&gt;: b
      touch &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;b&lt;/span&gt;:
      touch &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;A common chain of build steps&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;%-GENERATED.c&lt;/span&gt;: %-generate
      touch &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;%.o&lt;/span&gt;: %.c
      touch &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;%.so&lt;/span&gt;: %-GENERATED.o
      touch &lt;span style="color: #0000ee; font-weight: bold;"&gt;$&lt;/span&gt;&lt;span style="color: #cd00cd; font-weight: bold;"&gt;@&lt;/span&gt;
&lt;span style="color: #0000ee; font-weight: bold;"&gt;xxx-GENERATED.o&lt;/span&gt;: &lt;span style="color: #cdcd00;"&gt;CFLAGS&lt;/span&gt; += adsf

&lt;span style="color: #cdcd00;"&gt;# &lt;/span&gt;&lt;span style="color: #cdcd00;"&gt;Imitates .d files created with "gcc -MMD". Does not exist on the initial build&lt;/span&gt;
ifneq ($(&lt;span style="color: #cdcd00;"&gt;wildcard&lt;/span&gt; xxx.so),)
&lt;span style="color: #0000ee; font-weight: bold;"&gt;xxx-GENERATED.o&lt;/span&gt;: xxx-GENERATED.c
endif
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
This is all very simple build-system stuff. Let's see how it works:
&lt;/p&gt;

&lt;pre class="example"&gt;
$ rm -rf a b xxx-GENERATED.c xxx-GENERATED.o xxx.so
  [start from a clean slate]

$ touch xxx-generate xxx.h
  [Files that would be available in a project exist; xxx-generate is some tool]
  [that would generate xxx-GENERATED.c                                        ]

$ touch a
  ["a" exists but the file "b" it depends on does not]

$ make a xxx.so

  touch b
  touch a
  touch xxx-GENERATED.c
  touch xxx-GENERATED.o
  touch xxx.so
  rm xxx-GENERATED.c

  [It built everything, but then deleted xxx-GENERATED.c]

$ make a xxx.so

  remake: 'a' is up to date.
  touch xxx-GENERATED.c
  touch xxx-GENERATED.o
  touch xxx.so

  [It knew to not rebuild "a", but the missing xxx-GENERATED.c caused it to]
  [re-build stuff                                                          ]
&lt;/pre&gt;

&lt;p&gt;
Well that's not good. What if we add &lt;code&gt;.SECONDARY:&lt;/code&gt; to the end of the &lt;code&gt;Makefile&lt;/code&gt;
to mark everything as a secondary file?
&lt;/p&gt;

&lt;pre class="example"&gt;
$ rm -rf a b xxx-GENERATED.c xxx-GENERATED.o xxx.so
$ touch xxx-generate xxx.h
$ touch a

$ make a xxx.so

  remake: 'a' is up to date.
  touch xxx-GENERATED.c
  touch xxx-GENERATED.o
  touch xxx.so

  [It didn't bother rebuilding "a" even though its prerequisites "b" doesn't]
  [exist. But it didn't delete the xxx-GENERATED.c at least                 ]

$ make a xxx.so

  remake: 'a' is up to date.
  remake: 'xxx.so' is up to date.

  [It knew to not rebuild anything. Great.]
&lt;/pre&gt;

&lt;p&gt;
So it doesn't work right with or without &lt;code&gt;.SECONDARY:&lt;/code&gt;, but it's much closer
with it. The solution is to mark everything as &lt;i&gt;not&lt;/i&gt; an intermediate file.
mrbuild cannot do this without a bleeding-edge version of &lt;code&gt;GNU Make&lt;/code&gt;, but users
of mrbuild &lt;i&gt;can&lt;/i&gt; do this by explicitly mentioning specific files in rules. This
would suffice:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-makefile"&gt;&lt;span style="color: #0000ee; font-weight: bold;"&gt;___dummy___&lt;/span&gt;: file1 file2
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Detailed notes are in a &lt;a href="https://github.com/dkogan/mrbuild/commit/87a2be281d7d7a27d501196ca4ec5e82324f4095"&gt;commit in mrbuild&lt;/a&gt; (mrbuild 1.13) and in a &lt;a href="https://lore.kernel.org/lkml/Y6WUlth8KrR6EcsI@bergen.fjasle.eu/T/"&gt;post to LKML
by Masahiro Yamada&lt;/a&gt;.
&lt;/p&gt;
</content></entry>
</feed>
