Andrew Cooke | Contents | RSS | Previous
Welcome to my blog, which was once a mailing list of the same name and is still generated by mail. Please reply via the "comment" links.
Always interested in offers/projects/new ideas. Eclectic experience in fields like: numerical computing; Python web; Java enterprise; functional languages; GPGPU; SQL databases; etc. Based in Santiago, Chile; telecommute worldwide. CV; email.
Choochoo Training Diary
[Bike] Fixing Spyre Brakes (That Need Constant Adjustment)
[Computing, Music] Raspberry Pi Media (Audio) Streamer
[Computing] Amazing Hack To Embed DSL In Python
[Bike] Ruta Del Condor (El Alfalfal)
[Bike] Estimating Power On Climbs
[Computing] Applying Azure B2C Authentication To Function Apps
[Bike] Gearing On The Back Of An Envelope
[Computing] Okular and Postscript in OpenSuse
[Computing] Fail2Ban on OpenSuse Leap 15.3 (NFTables)
[Cycling, Computing] Power Calculation and Brakes
[Hardware, Computing] Amazing Pockit Computer
How I Am - 3 Years Post Accident, 8+ Years With MS
[USA Politics] In America's Uncivil War Republicans Are The Aggressors
[Programming] Selenium and Python
[Bike] How Fast Before Walking More Efficient Than Cycling?
[COVID] Coronavirus And Cycling
[Programming] Docker on OpenSuse
[Bike] Gearing For Real Cyclists
[Programming] React plotting - visx
Felicitations - Empowerment Grant
Applebaum - Twilight of Democracy
You can Masquerade in Firewalld
Update - Garmin Express / Connect
© 2006-2015 Andrew Cooke (site) / post authors (content).
From: andrew cooke <andrew@...>
Date: Thu, 12 Jan 2023 16:48:36 -0300
I have been working on a solver for Rummikub, but I haven't actually made much progress. This email is to note down some of my ideas and related issues. The underlying insight is that this is a graph colouring problem. The nodes of the graph are the known tiles. Edges connect any two tiles that can appear "next to each other" in a group. The colouring identifies each group (a successful colouring meets the appropriate rules). There may be an efficiency gain in using the previous colouring as a starting point when searching for a new one. This is fine as far as it goes. It becomes more problematic when you include jokers. A naive first approach is to include the jokers as additional nodes. The problem is that these are connected to *every* other node. This complicates hugely the search process when trying t find a colouring. In practice, when you play, you tend to separate things into two stages: first, liberating a joker and second, using that joker. Using this approach the joker tiles become "normal" tiles in the graph (ie take their assumed values) when searching for liberation. Then, in the next step, thereare many possible graphs (one for each possible value of the joker). I currently don't see how to handle this explosion in the number of graphs. So I'm stuck. It seems like the colouring view doesn't reduce the combinatorial complexity sufficiently. Or doesn't allow for the complexity to be reduced using a "natural" compromise. Andrew
From: andrew cooke <andrew@...>
Date: Sat, 21 Jan 2023 19:56:42 -0300
In the note above I modeled the game as a graph with colourings. But things kinda fell apart because I needed multiple graphs. After thinking some more, it may be better to ditch colourings altogether and simply search for graphs. These graphs have connections only where there are "played" relationships between the tiles. Now, the problem with this is that there are an awful lot of possible graphs. How do you generate them efficiently (where efficiency includes memory, speed, and accuracy (generating no or few "impossible" graphs))? It seems like there may be a sweet-spot implementation that uses association tables where each row is in fact a list. Each entry in the list describes a locally-consistent set of neigbours (so, for example, a neigbour that belongs to a run and another that belongs to a group would not appear in the list together). To first aproximation the graphs we want to generate are then all the different combinations possible by selecting one list entry from each node. But that ignores remote (contrast w local) consistency - if a node A has selected node B as part of a run then node B must select A as part of a run too (for example). So one can imagine a search process in which selecting a list entry for one node constraints the available list entries in neghbouring nodes (in fact, constraints can cascade across multiple connections, which complicaes things somewhat). I think this would be most efficient as a "push" - then the entry is selected all neighbours are corrected immediately. Thinking in more detail about the implementation, each list entry might have a tag that indicates whether it is a run or group locally. That tag could then have a third value - unused - when filtered by remote constraints (and maybe a fourth indicating that the tile is unused). A further advantage of this approach is that jokers are more naturally handled (their "assumed value" can be stored in the list entry?). Andrew
Permalink | Comment on this post
For comments, see relevant pages (permalinks).
From: andrew cooke <andrew@...>
Date: Thu, 12 Jan 2023 16:25:32 -0300
https://marcachile.cl/vida-cultura/las-nuevas-voces-de-la-poesia-chilena-que-cruzan-fronteras > Su [Tamym Maulén] libro «PAF» es considerado una obra fundamental de > la nueva poesía chilena. https://en.calameo.com/read/004645115620198c12433 It's not clear (to me) what "nueva poesía chilena" refers to. Particularly since https://es.wikipedia.org/wiki/Antolog%C3%ADa_de_poes%C3%ADa_chilena_nueva is probably not what's in mind. Andrew
From: andrew cooke <andrew@...>
Date: Wed, 1 Jun 2022 19:09:58 -0400
I have sme Spyre brakes and they're great - work well and are easy to maintain. But recently (after a couple of years use) I found that one pad would slowly move further from the disk and, every few rides, need adjusting (tightening). This is something I have heard others complain about, too. Fortunately there is an easy fix. I did this by removing the caliper from the bike, but with care I think you could do it leaving the caliper in place (but with the wheel removed). Here's what I did: - Remove the pads - Screw the 3mm adjustment on the side of the problem pad inwards (as you would to move the pad closer to the disk). Keep screwing and something will eventually fall out! - The thing that falls out (the thing that pushes the pads against the disk) is like a short screw with a very flat head. Clean it with a rag and put a decent amount of blue locktite on the threads (the stuff that stops things from coming loose, but leaves them still able to be adjusted by hand). - Once the loctite is fairly dry you need to screw that part back in. This is a bit tricky (and more tricky still if you've not removed the calipers from the bike). What you need to do is put the screw inside the caliper, with the threaded part pointing towards where you adjust from the inside (a ruler, screwdriver, or lollipop stick will help). Once you get everything lined up you can use the 3mm allen key from outside and "skewer" the screw, then, by turning anti-clockwise, screw it back into place. Basically, you're doing the opposite of what you did to make it fall out. - Once in place leave for, say, 12 hours to allow the loctite to set. - And don't forget to replace the pads and adjust before riding. After all that you'll find that the adjustment is much "stiffer" and desn't come loose. I have no idea how long it lasts because I only did it myself a few days ago. Andrew
From: andrew cooke <andrew@...>
Date: Sat, 7 May 2022 17:36:36 -0400
I have a Raspberry Pi that I want to take music from a HDD and send it to a USB DAC. I want to be able to control this remotely from my phone. It should look good and "just work". I have tried a pile of things in the last week. The best (by far) has been volumio. After flashing the image and booting you may need to enable ssh (via http://address.of.volumio/dev) and then you can modify fstab and mount the music at /mnt/INTERNAL You can also modify /volumio/app/plugins/music_service/mpd/mpd.conf.tmpl to enable replaygain (and then save settigns from the gui to regenerate /etc/mpd.conf). Various other things learnt: - mopidy is an MPD rewrite in Python (ish). Unfortunately it's not super-well supported and the clients are buggy (even when you finally understand what it is and that you need to install mopidy-local). The MPD plugin doesn't make much sense. - MPD clients for android are not great. MpDroid seems to be no longer maintained and MALP is flakey. - I tried moode a while back and that was OK (but volumio seems more responsive and easier to use). Andrew
From: andrew cooke <andrew@...>
Date: Mon, 31 Jan 2022 17:35:03 -0300
This library - https://github.com/pythonql/pythonql - extends Python's syntax (and semantics) by calling a preprocessor triggered by the line #coding: pythonql at the top of the file. The meaning of "#coding" is defined here - https://www.python.org/dev/peps/pep-0263/ - and was an early way of defining source encodings (eg UTF8) for Python code. Andrew
From: andrew cooke <andrew@...>
Date: Sun, 23 Jan 2022 13:12:33 -0300
Was up in El Alfalfal today and noticed two riders disappear into the village. The guards at the hydroelectric plant said that there's a path up to the trail above. I didn't find it on a quick look, but when I got home checked online maps. Google is useless, but OpenStreetMap clearly shows the path! It's along the side of the football pitch in the village (the NE side). Maybe this will help someone searching for how to access the route. And if not, the road ride up + back is pretty nice anyway! Andrew
From: andrew cooke <andrew@...>
Date: Thu, 20 Jan 2022 19:31:49 -0300
I was slowly pedaling up a hill today, staring at my cycle computer to avoid looking at the endless climb in front of me, when I realized that you can easily estimate your power from the grade and speed (km/h). Me + my bike weight about 72kg. Actually a bit more (I need to lose a kilo or two), but 72 turns out to be a nice number in what follows. 72kg ascending vertically at 1m/s requires a power of 720W (because g=10, more or less). A speed of 1km/h is 1000m/3600s or 1/3.6 m/s. Small angle approx means that if you are riding at a speed S(km/h) on a gradient of G then your vertical speed is SG/100 km/h or SG/360 m/s. So the power required (at 72kg) is SGx720/360 W or 2SG W. For example, at 10km/h on a 5% grade your power is 2x10x5 = 100W. Now this is incorrect if you're moving fast enough, or riding up a shallow enough slope, that air resistance is important. But for climbing it's fine. Obviously if you+bike weighs more or less than 72kg you need to adjust accordingly. Andrew
From: andrew cooke <andrew@...>
Date: Mon, 17 Jan 2022 20:09:08 -0300
Solving this has taken a long time (months) going back + forth with Microsoft support. Since I was stuck because I couldn't find this info on the web I thought I should put something out there. This is not a general explanation of how to enable B2C authentication between a SPA (Single Page Application - JavaScript in a web page) and a Function App. Instead, what I will focus on is converting the project at https://github.com/Azure-Samples/ms-identity-b2c-javascript-spa.git to work with a Function App that you have deployed. If you can get that working, you can carry everything across to your application. So this is what you need to do: 1 - Clone https://github.com/Azure-Samples/ms-identity-b2c-javascript-spa.git and get it working as is. Understand it. 2 - Deploy your application that uses a Function App (currently without authentication), make sure it works, and note what the result is if you call your Function App directly from a browser (in my case, for example, I had a Function App that provided an access token to Azure Maps - if I called it by hand, I got a token). 3 - Configure B2C. There are three parts that need to be connected: 3a - Define a User Flow. Go to B2C in the Portal and select "User Flows" (left menu) then "New user flow" (top bar), "Sign up and sign in" (option box), "Recommended" (popup) and "Create" (button). Provide a name (eg "demoflow"), select "Email Signup" (checkbox) and "Create" (button). Leave everything else as defaults. 3b - Register the Function App. Go to B2C in the Portal and select "App registrations" (left menu), "New registration" (top bar). Provide a name (eg "myfnapp"), set "Select a platform" as "Web" then select "Register". Leave everything else as defaults. 3c - Register the SPA. Go to B2C in the Portal and select "App registrations" (left menu), "New registration" (top bar). Provide a name (eg "msdemo"), set "Select a platform" as "SPA" with a redirect URI of "http://localhost:6420" then select "Register". Leave everything else as defaults. 3d - Connect things. Go to B2C in the Portal and select "App registrations" (left menu), select "myfnapp", select "Expose an API" (left menu), select "Add a scope", save the default URI (popup on right), provide a name for the scope (eg "access") and provide some text for display (anything will do), then select "Add Scope" (button). Go to B2C in the Portal and select "App registrations" (left menu), select "msdemo", select "Add a permission", then "My APIs" (top menu), then "myfnapp", select "access" and then "Add permissions". In the "Configured permissions" page (displayed after the above), select "Grant admin consent" and "Yes". 4 - Enable authentication for the Function App. Find the Function App in the Portal, select "Authentication" (left menu), select "Add identity provider", select "Microsoft", select "Pick an existing app registration in this directory" and then select "myfnapp". Choose HTTP 401 for "Unauthenticated requests" and select "Add". CORS also needs to be enabled for the Function App. Select "CORS" (left menu) and add "http://localhost:6420" then select "Save". If you call your Function App from a browser now it will give an error. 5 - Modify the MS demo. All the above is standard, really; it's this section that is critical. The files that need altering are all in the App directory and are all pure data structures (no code - the app is nicely structured). 5a - authConfig.js Change "clientID" to the "Application (client) ID" for the "msdemo" registration you defined above (it's displayed in the Portal in the App registration for that name). 5b - policies.js Remove the "editProfile" entries because we're not using them. Change the "authorityDomain" to be "sts.windows.net" (more exactly, use the domain in the "Issuer URL" if you go to "Authentication" for the Function App and then select "Edit" for the provider). Change "fabrikamb2c" in "authentication" to match whatever you call your B2C. Change "B2C_1_susi_reset_v2" in the "authority" to be the user flow defined above ("B2C_1_demoflow") 5c - apiConfig.js Change "b2cScopes" to be the default URI created for the "access" scope above (go to B2C, "App registrations", "myfnapp", "Expose an API" and copy the URI) Change "webApi" to be the URL of your Function App. 6 - Run the demo. > npm install > npm start Open http://localhost:6420 and enable the dev console so you can see network requests. Sign-in and call the API. In a perfect world, it should work (should make successful calls; the web app itself won't do much because the function being called is not what is expected). You should be able to see the expected return value from the API in the network panel. If you get an HTTP 401 error when calling the API then you may be able to view the actual error from the MS auth system by doing the following (this is deep magic, but it works): Select the Function App in the Portal. Select "New Support Request" (left menu) (don't worry, we are not going to actually submit a request). Enter "authentication error" as "Summary", select "Authentication and Authorization" as "Problem Type", select "Azure App Service built-in authentication" as "Problem subtype". Select "Next:Solutions". When something is displayed, scroll down and see if there are any errors. If you don't see any errors, wait a little and try again - it seems to take 30min or so for the errors to propagate. The critical step in all the above was learning how to see the errors. From those I managed to infer setting the authorityDomain in 5b above. Have fun, Andrew
From: andrew cooke <andrew@...>
Date: Mon, 4 Oct 2021 21:28:20 -0300
Uphill If you + bike weigh 100kg and you can put out a steady 100W then you can ascend vertically at 0.1m/s (work against gravity if g=10m/s^2). On a 10% incline (steep, but possible where I ride in the Andes foothills) that means you can move forwards at 1m/s. The circumference of a 700C wheel is about 2m and the slowest comfortable cadence while seated is around 60rpm. So with 1:1 gearing that's 2m/s. So you need 1:2 gearing for the climb. That kind of gearing is pushing the low end of available MTB gears. In other words, pretty much no-one has gearing that low. Pretty much all entry-level riders would have real problems climbing a 10% grade for any distance. Downhill Air resistance increases as v^3. That has a sharp "bend" - either it's not important or it's a brick wall. So basically everyone (whatever their power output) is limited to a similar top speed. From experience it's roughly 60km/h. Faster / steeper than that you need to get seriously aero and pedalling efficiently isn't really an option. You can probably manage 100rpm if you have to, in your top gear, all out. With a 2m circumference wheel that's 200m/min. 60km/h is 1km/min. So you need 5:1 gearing for the descent (to go from 200m/min to 1,000 m/min). This is consistent with the top end of traditional "race" gearing (maybe 53 teeth at the front and 11 at the rear). Gear Range The above implies a gear range from 1:2 to 5:1. That's a factor of 10, or 1,000%. In practice, pro riders can put out around 4x the power / weight used above. Compared to inexperienced riders they are absolute monsters. That shifts the lower gears needed up by a factor of 4. So instead of a 1,000% range they need a 250% range, which is well within typical "race bike" gearing. Non-pro riders (and people with loaded touring bikes) that have the technical understanding compromise at both ends. At the top, 4:1 is generally enough, and at the bottom they can live with maybe 1:1.5. That gives a 600% range which is possile with a Rohloff IG hub, or a frankenstien mix of road and MTB components (as on my own road bike). Currently fashionable 1x road / gravel can reach maybe 500% range. Exactly where that is "centred" depends on the front ring - there will likely be significant compromise at both the high and low ends. Andrew
From: andrew cooke <andrew@...>
Date: Thu, 9 Sep 2021 10:13:20 -0300
Wow. PS is so old-fashioned now that support in okular is in a separate package (okular-spectre) that is not installed by default. Andrew
From: andrew cooke <andrew@...>
Date: Fri, 30 Jul 2021 09:58:50 -0400
Couldn't find this documented anywhere, but it works just fine. localhost:/etc/fail2ban # cat jail.local [DEFAULT] bantime = 1h maxretry = 3 [sshd] enabled = true backend = systemd banaction = nftables localhost:/etc/fail2ban # cat fail2ban.local [DEFAULT] loglevel = INFO Typical log output: localhost:/var/log # tail fail2ban.log 2021-07-30 08:19:02,184 fail2ban.filter [16653]: INFO [sshd] Found 49.88.112.69 - 2021-07-30 08:19:01 2021-07-30 08:19:02,249 fail2ban.actions [16653]: NOTICE [sshd] Ban 49.88.112.69 2021-07-30 08:28:00,927 fail2ban.actions [16653]: NOTICE [sshd] Unban 205.185.125.109 2021-07-30 09:05:49,815 fail2ban.actions [16653]: NOTICE [sshd] Unban 77.227.158.129 2021-07-30 09:19:02,048 fail2ban.actions [16653]: NOTICE [sshd] Unban 49.88.112.69 2021-07-30 09:23:52,935 fail2ban.filter [16653]: INFO [sshd] Found 205.185.127.25 - 2021-07-30 09:23:52 2021-07-30 09:23:54,934 fail2ban.filter [16653]: INFO [sshd] Found 205.185.127.25 - 2021-07-30 09:23:54 2021-07-30 09:24:52,933 fail2ban.filter [16653]: INFO [sshd] Found 205.185.127.25 - 2021-07-30 09:24:52 2021-07-30 09:24:53,125 fail2ban.actions [16653]: NOTICE [sshd] Ban 205.185.127.25 2021-07-30 09:24:54,434 fail2ban.filter [16653]: INFO [sshd] Found 205.185.127.25 - 2021-07-30 09:24:54 All under systemd control. Andrew
From: andrew cooke <andrew@...>
Date: Sat, 24 Apr 2021 16:20:34 -0400
It's common (eg Strava) to estimate a cyclist's power output from their GPS and elevation (barometric) data. The basic idea is fairly simple: we can calculate the power required for the bicycle to behave as recorded at each point in time using mass / gravity / height, CdA / speed, and rolling resistance / speed. This ignores wind, which makes the effective speed for wind resistance (CdA) different from the measured speed, and so introduces errors. Also ignored, but not commonly discussed, is braking. If the cyclist does not use the brakes then all we have discussed so far is correct. If the cyclist brakes *and* pedals then we will underestimate their power output because they are expending additional energy we do not "see" in the data (assuming that there is no power meter data). If the cyclist brakes when not pedalling then we *should* (if we have sufficient resolution, perfect parameters, etc) see a negative input power. In practice, when you estimate power, you do see negative spikes. I had assumed that these were noise, but the above suggests that it is also reasonable to interpret them as braking. In conclusion, then, braking may be visible in the output from modelling and is not inherently a source of uncertainty *unless* the cyclist brakes when also pedalling. That exception is more likely than it sounds since "when also" does not, in practice, mean (only) concurrently, but also at any time within the "time step" used in the calculation (eg 10s). Andrew