systemctl --failed
This gives me a list of all my systemd services that have failed. I pray that it’s empty
UNIT LOAD ACTIVE SUB DESCRIPTION
0 loaded units listed.
Except, I don’t.
Instead, I have it set up so that I receive a webhook notification via Zulip whenever a service fails. With the right infrastructure in place, it’s as simple as adding a OnFailure
line to all the services you want to monitor.
Step 1: Setting up the webhook.
On Zulip, I use the Slack incoming webhook integration. (Note the URL specification)
As you might guess, this style of webhook works on Slack and on Discord as well.
For our notification script we’ll need two environmental variables
Name | Description |
---|---|
SERVICE | The name of the systemd service. We will automatically populate this |
WEBHOOK_URL | The URL to send the webhook to. This is chat application specific. |
We’ll need the following CLI applications installed
Name | Description |
---|---|
curl |
Sends the POST request. |
jq |
Sanitizes the log output before sending it to curl. |
The script /bin/webhook-notify.sh
#!/bin/bash
if [ -z "$SERVICE" ]; then
echo "SERVICE variable not set or empty"
exit 1
fi
if [ -z "$WEBHOOK_URL" ]; then
echo "WEBHOOK_URL variable not set or empty"
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "jq is not installed"
exit 1
fi
LOG_CONTENTS=$(systemctl status --full --no-pager ${SERVICE} | jq -Rsa .)
curl -X POST --data-urlencode "payload={\"text\": $LOG_CONTENTS}" ${WEBHOOK_URL}
Make the script executable
chmod u+x /bin/webhook-notify.sh
At this point you should be able to test out the script and make sure you get notifications. Set the two environmental variables and run the script.
Example:
export WEBHOOK_URL="https://INSERT-NAMESPCE.zulipchat.com/api/v1/external/slack_incoming?api_key=INSERT-API-KEY&stream=INSERT-STREAM-ID&topic=Systemd"
export SERVICE=NetworkManager
/bin/webhook-notify.sh
Step 2: Setup the Systemd Service
When a systemd unit fails, we are able to call another systemd service. The service that we’ll call will run our script from the last step.
In /etc/systemd/system/webhook-notify@.service
[Unit]
Description=Send Systemd Notifications via Webhook
[Service]
Type=oneshot
Environment=WEBHOOK_URL="INSERT-WEBHOOK-URL-HERE"
Environment=SERVICE=%i
ExecStart=/bin/webhook-notify.sh
[Install]
WantedBy=multi-user.target
Note the @
in the filename. This is important since this service will run with the failed unit name as the argument that appears after the @
. Within the script, this is the %i
variable.
Example test:
sudo systemctl start webhook-notify@NetworkManager
Step 3: Add OnFailure
to all the services we want to monitor
Within the [Unit]
section of our Systemd service, add the following
OnFailure=webhook-notify@%i.service
I mean if the unit test doesn’t even pass, why bother with the proof?
Luckily, Lean 4 let’s us do unit tests with a cool command called #guard
#guard 1 = 1
This checks whether the following expression evaluates to true
. Note that this does not provide a proof since this check is done using the untrusted evaluator.
What does need to be proven, however, is that the expression given is decidable.
One issue I faced is that I often use the Except
type for error handling in my code.
inductive Except (ε : Type u) (α : Type v) where
/-- A failure value of type `ε` -/
| error : ε → Except ε α
/-- A success value of type `α` -/
| ok : α → Except ε α
For a simple (somewhat silly) example, look at the following function
def gt_0 (n : Nat) : Except String Bool :=
if n = 0 then .error s!"{n} is not greater than zero"
else .ok true
We can evaluate our function on a given input:
#eval gt_0 1 -- Except.ok true
However, we can’t guard against it without encountering an error. This is because we haven’t shown that the equality is decidable.
To help with this, I wrote up a function that we can apply generically.
def Except.deq [DecidableEq α] [DecidableEq β] : DecidableEq (Except α β) := by
unfold DecidableEq
intro a b
cases a <;> cases b <;>
-- Get rid of obvious cases where .ok != .err
try { apply isFalse ; intro h ; injection h }
case error.error c d =>
match decEq c d with
| isTrue h => apply isTrue (by rw [h])
| isFalse _ => apply isFalse (by intro h; injection h; contradiction)
case ok.ok c d =>
match decEq c d with
| isTrue h => apply isTrue (by rw [h])
| isFalse _ => apply isFalse (by intro h; injection h; contradiction)
We can then show that equality of Except String Bool
is decidable, since Lean already knows that string and boolean equality is decidable.
instance: DecidableEq (Except String Bool) := Except.deq
From here, we can use #guard
#guard gt_0 1 = (Except.ok true)
When a #guard
fails, it will throw an error during compilation. This is great for ensuring that our unit tests pass during compilation.
inductive Sign where
| pos
| neg
def posOrNegThree (s : Sign) : match s with | Sign.pos => Nat | Sign.neg => Int :=
match s with
| Sign.pos => (3 : Nat)
| Sign.neg => (-3 : Int)
The function posOrNegThree
depending on the input, can either return an expression of type Nat
or an expression of type Int
. That’s super neat!
What happens if we add a wildcard to the match?
For example, let’s say we want a type based on the number of bits of precision specified. If we don’t support the number of bits, we return the arbitrary precision Nat
as our default.
def UIntN (n: Nat) : Type := match n with
| 32 => UInt32
| 64 => UInt64
| _ => Nat
Now let’s write a function that returns the zero element of our specified type:
def u0 (x: Nat) : UIntN x := match x with
| 32 => (0: UInt32)
| 64 => (0: UInt64)
| _ => Nat.zero
This will result in the following error:
type mismatch
Nat.zero
has type
ℕ : Type
but is expected to have type
UIntN x✝ : Type
I got stuck on this for a while, so I asked on the really helpful Lean Zulip and got a great response
def u0 (x: Nat) : UIntN x := dite (x=32) (λ h ↦ by
subst h
exact (0 : UInt32)
) (dite (x = 64) (λ h ↦ by
intro h2
subst h
exact (0: UInt64)
)
(λ h ↦ by
intro h2
have : UIntN x = Nat := by
unfold UIntN
simp only
rw [this]
exact 0
))
Unfortunately this doesn’t look pretty, but this was a massive clue in finding the prettier syntax to solve this problem!
def u0 (x: Nat) : UIntN x :=
if h: x = 6 then by
subst h
exact (0: UInt32)
else if h2: x = 4 then by
subst h2
exact (0: UInt64)
else by
have : UIntN x = Nat := by
unfold UIntN
simp only
rw [this]
exact 0
]]>When I query my toots, I receive a JSON response like the following:
[{"id": "112581869512127927", ..., "content": "...",}, ...]
Some parts of the JSON response changes quite frequently. Since I archive the changes, I want to strip out the highly dynamic information before saving it off to a file.
let mut json_response: serde_json::Value = serde_json::from_str("...")
.expect("JSON parse error");
In order to modify this variable, we need to have some knowledge of it’s structure. I’ll show in this post how to modify our JSON data given whether we’re working with a JSON array or a JSON object.
serde_json::Value::Array
Our example JSON starts off as an array, so let’s extract that out:
let json_array = json_response
.as_array_mut()
.expect("Expected JSON Array");
The as_array_mut
says to interpret the json_response
variable as an array. The mut
component is important for us to be able to edit the data in place without making copies.
The as_array_mut
method returns an option type. Calling .expect(...)
on it will cause the program to crash if it isn’t indeed an array. We can alternatively perform some error handling:
if let Some(json_array) = json_response.as_array_mut() {
// Do something with json_array
} else {
// Error handling here
}
Though I’ll assume that you’re following best practices and not discuss more about error handling in this post.
Our variable json_array
has type &mut Vec<serde_json::Value>
which means we can do things like add another element to said array.
let new_element = serde_json::Value::from(1);
json_array.push(new_element);
We can also remove the last element of the array if it exists
json_array.pop()
serde_json::Value::Object
Within the array, we have a list of objects. Let us grab the first element as an example:
let first_item = json_array.get_mut(0).unwrap();
In order to be able to modify the data, we use the get_mut
method. This, like before, returns an option if it doesn’t exist. We can call unwrap
on it to get access to the data or panic if the element doesn’t exist.
The variable first_item
has type serde_json::Value
. To interpret this as an object, we need to call as_object_mut
.
let first_item_obj = first_item.as_object_mut().unwrap();
Now our variable first_item_obj
has type &mut Map<String, serde_json::Value>
.
We can remove any fields that we don’t think is important
first_item_obj.remove("bot");
Add any fields we want
let new_key = "PoweredBy".to_string();
let new_value = serde_json::Value::from("Rust");
toot.insert(new_key, new_value);
Renaming a field is the combination of the last two:
let toot_date = toot.remove("created_at")
.expect("Missing created_at");
toot.insert("date".to_string(), toot_date);
Personally, this worked great on my Linux machines. However, I found it a pain to use on a mobile device.
On my phone, I want to be able to open my browser and go to a publicly resolvable URL to access my homelab services in a secure way. What’s very common in enterprise settings is to run an authenticated proxy through a single sign-on (SSO) service. This would normally prompt for a username and password, followed sometimes with a 2FA code before redirecting you to the service you want.
I don’t fully trust myself to securely maintain a SSO service as a hobby. Also instead of typing in a username and password each time, I want to rely on keys instead. This is where mutual-TLS (mTLS) comes in.
In the standard TLS setup, the server provides the client with its public key so that the client can send encrypted communications as well as validate that the server is who they say they are. This validation is done through trusted certificate authorities.
The CA/Browser Forum is a self-regulated group that issues a set of requirements for certificate authorities to follow. Ultimately, vendors have their own policies (ex: Mozilla) for deciding whether to trust a SSL certificate coming from a certificate authority. However, many follow the CAB forum guidelines.
When a server is configured with mTLS, not only does the client authenticate the server but the server also authenticates the client. If the client doesn’t respond with the proper certificate, then the server will return a HTTP 400 error.
To setup an authenticated proxy with mTLS, we will use nginx
as the reverse proxy and Cloudflare’s PKI and TLS toolkit cfssl
. You can replace the cfssl
commands with the equivalent openssl
ones, but I won’t cover that in this post.
I’ll have a publicly available server with the reverse proxy installed and setup to relay traffic over the VPN to my homelab. We’ll use certificates generated by cfssl
to authenticate the client and server.
I also wrote about cfssl
in the past, but I’ll keep this blog post self-contained.
cfssl
relies on having JSON files for its configuration. First let’s set up the certificate authority in a file called csr_ca.json
replacing the fields as needed:
{
"CN": "SomeCommonName",
"key": {
"algo": "rsa",
"size": 4096
},
"names": [
{
"C": "US",
"O": "SomeOrg",
"OU": "SomeOrgUnit",
"ST": "New York",
"L": "New York"
}
]
}
Then generate the root certificates for our certificate authority
cfssl gencert -initca csr_ca.json | cfssljson -bare ca
This will generate the following files
Filename | Purpose |
---|---|
ca.pem | Public Certificate |
ca-key.pem | Private Key |
ca.csr | Certificate Signing Request |
With these files that constitute our certificate authority, we’ll generate the server and client certificates.
When generating the server certificate, we need to declare which URLs the certificate is valid for. Instead of creating a new certificate for every service in my homelab, we’ll make use of a wildcard certificate on a particular domain.
As a personal preference, I like to keep my certificates separated by device. Create a new folder called server
with the following in csr_server.json
{
"hosts": [
"*.internal.somedomain.com"
],
"key": {
"algo": "rsa",
"size": 4096
},
"names": [
{
"C": "US",
"O": "SomeOrg",
"OU": "SomeOrgUnit",
"ST": "New York",
"L": "New York"
}
]
}
Then we similarly create three certificates relating to the server with the following command
cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem csr_server.json | cfssljson -bare cert
Here I assumed the root certificates are stored one level up (..
) but replace the path as necessary.
To match how nginx
expects its certificates, we’ll perform some renames and concatenations.
mv cert-key.pem privkey.pem
mv cert.pem chain.pem
cat privkey.pem > fullchain.pem
cat chain.pem >> fullchain.pem
We’ll need to run cfssl
one more time to generate our client’s certificates. Create a folder for the specific client and have the following with csr_client.json
.
{
"key": {
"algo": "rsa",
"size": 4096
},
"names": [
{
"C": "US",
"O": "SomeOrg",
"OU": "SomeOrgUnit",
"ST": "New York",
"L": "New York"
}
]
}
Then generate the certificates:
cfssl gencert -ca=../ca.pem -ca-key=../ca-key.pem csr_client.json | cfssljson -bare cert
Firefox expects the key in a specific format, so we’ll ask openssl
to create it for us.
openssl pkcs12 -export -out user.pfx -inkey cert-key.pem -in cert.pem -certfile ../ca.pem
When running this command, it’ll ask for an export password. You’ll need to remember this when attempting to import this key. You can leave it blank if you’re intending to use it for Firefox, but I found that Android devices won’t accept this unless some password is set.
Before we can import user.pfx
on our favorite device, we need to have our device trust the root certificate authority. Since we created our own and didn’t go through a CA like Let’s Encrypt, devices will not trust the certificates by default.
On Android you can import ca.pem
via More security & privacy > Encryption & credentials -> Install a certificate -> CA certificate
.
Then you can install user.pfx
via More security & privacy > Encryption & credentials -> Install a certificate -> VPN & app user certificate
.
Since we’re looking to access multiple homelab services from our authenticated proxy, we’ll make use of regexes in the server_name
so that we only need to write one config.
server_name "~^(?<subdomain>.+)\.internal\.somedomain\.com$";
This matches on *.internal.somedomain.com
where the *
wildcard means that it can be any string. The regex looks slightly different since we want to capture the wildcard portion in a variable called $subdomain
.
A good security posture is to provide a white-list of what subdomains to allow.
set $valid_subdomain 0;
if ($subdomain = "X") {
set $valid_subdomain 1;
}
if ($subdomain = "Y") {
set $valid_subdomain 1;
}
if ($valid_subdomain = 0) {
return 403;
}
We can see here that if we don’t match on X.internal.somedomain.com
or Y.internal.somedomain.com
that we will return a 403 Not Authorized
HTTP code.
To enable mTLS, we need to turn on client verification. For this, we need the certificate of the root CA.
ssl_verify_client on;
ssl_client_certificate /path/to/root/certificates/ca.pem;
Then when we do the proxying, we need to set the Host
variable so that it does not have the internal
component of the domain name.
location / {
proxy_pass https://10.10.10.2;
proxy_set_header Host "$subdomain.somedomain.com";
// Other options omitted for brevity
}
The proxy URL shouldn’t allow outside traffic by default. Otherwise people can bypass the authentication proxy! You can do this by either (1) only responding when the source IP matches the proxy IP, or (2) having the URL only resolvable when using a VPN.
Check the bottom of this post for the whole config, but when that’s all setup we can restart the nginx
service and verify that no errors are shown.
sudo systemctl restart nginx
With that, we should be able to securely access our homelab remotely! Give it a test, visiting X.internal.somedomain.com
replacing the relevant parts of the URL. Your browser should then prompt for the certificate, and if all goes well you should see the service.
On Android, sadly the Firefox app doesn’t support mTLS, but the default Chrome browser does. On desktop, both Firefox and Chrome does support mTLS.
Feel free to write in if you have any questions about your setup.
Full Nginx config which I store at /etc/nginx/conf.d/internal.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name "~^(?<subdomain>.+)\.internal\.somedomain\.com$";
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name "~^(?<subdomain>.+)\.internal\.somedomain\.com$";
set $valid_subdomain 0;
if ($subdomain = "X") {
set $valid_subdomain 1;
}
if ($valid_subdomain = 0) {
return 403;
}
ssl_certificate /path/to/server/certificates/fullchain.pem;
ssl_certificate_key /path/to/server/certificates/privkey.pem;
include /etc/letsencrypt/conf/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/conf/ssl-dhparams.pem;
ssl_trusted_certificate /path/to/server/certificates/chain.pem;
ssl_stapling on;
ssl_stapling_verify on;
ssl_verify_client on;
ssl_client_certificate /path/to/root/certificates/ca.pem;
location / {
proxy_pass https://10.10.10.2;
proxy_set_header Host "$subdomain.somedomain.com";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_cache_bypass $http_upgrade;
}
}
It’s something that I think about semi-often. It’s difficult to characterize what static+
means, because I believe we each have our reasons for using a static site generator. This includes but is not limited to speed, security, and my personal reason of data portability.
I like how my Hugo website is written in Markdown files. This means that if Hugo stopped development now, I can move to a different static site generator in the future.
With this in mind, what does static+
mean for me? For data portability, this means that if I want an API to create a blog post ideally it’ll:
hugo
to build the siteI enjoy my writing workflow on my computer, so the idea is crafting some easy way to compose posts on my phone.
Hugo maintains a list of frontends which I haven’t vetted but may actually satisfy those requirements. Perhaps it’s time and energy that’s preventing me from Jamstack’n my website.
Or it could be simplicity that makes me want a simpler solution like a custom API that Gabriel describes.
Or I want a reason to tinker with a custom API :)
]]>Separating sections with an additional slide is nice because it not only gives an audience an opportunity for questions, but it also allows you to sneak in a sip of water.
To accomplish this automatically, thanks to a tip from James Oswald, we add the following to the preamble of our Beamer file (before the \begin{document}
)
\AtBeginSection[]{
\begin{frame}
\vfill
\textbf{\insertsectionhead}
\vfill
\end{frame}
}
This produces the following slide right after your \section{}
code:
Which for me and my template is equivalent to if I typed this in the beginning of each section:
\begin{frame}
\textbf{First Awesome Section}
\end{frame}
Saving a little bit of typing is always nice :)
My slide is fairly bare-bones, another cool approach shows the entire table of contents with every other section faded out.
]]>In this post, we’ll go over how to encode constraints and safety into an automated planning domain. As they’re inherently similar, we’ll look at it in the lens of constraints for the rest of the post.
Running Example: Suppose we’re in a three room domain laid out in the following way:
kitchen <-> Living Room <-> Dining Room
You’re currently in the kitchen holding dinner for tonight. The goal is to get dinner ready. You first need to set the table with the plates, and you need two hands for that. You can walk between the three rooms, but it’s slow and incurs extra action cost. To get around faster, you can dash. However, dashing when holding food leads to spilling some of it and making a mess. You can cleanup the mess in any room you’re in.
The PDDL domain will look like the following:
(define (domain dinner)
(:requirements :typing)
(:types location - object)
(:predicates
(at ?1 - location) (CONNECTED ?l1 ?l2 - location)
(table-set) (dinner-ready) (mess-made ?l - location)
(holding-food) (at-food ?l - location)
)
(:action walk
:parameters (?l1 ?l2 - location)
:precondition (and
(at ?l1)
(CONNECTED ?l1 ?l2)
)
:effect (and
(at ?l2)
(not (at ?l1))
(increase (total-cost) 10)
)
)
(:action dash
:parameters (?l1 ?l2 - location)
:precondition (and
(at ?l1)
(CONNECTED ?l1 ?l2)
)
:effect (and
(at ?l2)
(not (at ?l1))
(when (holding-food) (mess-made ?l2))
)
)
(:action set-table
:precondition (and
(at dining-room)
(not (holding-food))
)
:effect (table-set)
)
(:action present-dinner
:precondition (and
(at dining-room)
(holding-food)
(table-set)
)
:effect (dinner-ready)
)
(:action drop
:parameters (?l1 - location)
:precondition (and
(holding-food)
(at ?l1)
)
:effect (and
(not (holding-food))
(at-food ?l1)
)
)
(:action pickup
:parameters (?l1 - location)
:precondition (and
(not (holding-food))
(at ?l1)
(at-food ?l1)
)
:effect (and
(holding-food)
(not (at-food ?l1))
)
)
(:action cleanup
:parameters (?l - location)
:precondition (at ?l)
:effect (not (mess-made ?l))
)
)
For the goal of having dinner ready, the problem file is the following:
(define (problem dinner)
(:domain dinner)
(:requirements :typing)
(:objects
kitchen living-room dining-room - location
)
(:init
(CONNECTED kitchen living-room)
(CONNECTED living-room kitchen)
(CONNECTED living-room dining-room)
(CONNECTED dining-room living-room)
(at kitchen)
(holding-food)
)
(:goal (dinner-ready))
(:metric minimize (total-cost))
)
Let’s say that we don’t want any messes to be made. One way to go about this is encoding this as a goal.
(:goal (and
(dinner-ready)
(forall (?l - location) (not (mess-made ?l)))
))
Let’s ask Fast Downward to find an optimal plan.
fast-downward.sif domain.pddl problem.pddl --search "astar(blind())"
It produces the following:
dash kitchen living-room
cleanup living-room
dash living-room dining-room
cleanup dining-room
drop dining-room
set-table
pickup dining-room
present-dinner
Note that it still made several messes, but it cleaned up after itself to satisfy the goal. If we want to avoid making messes at all during the plan, we’ll have to use a constraint. We can add the following to the problem file:
(:goal (dinner-ready))
(:constraints (and
(forall (?l - location) (not (mess-made ?l)))
))
Not all planners support this PDDL 3.0 feature. Fast Downward does not. In this case, we’ll need to do a compilation trick.
check
(not check)
(check)
check
in the effect if the constraint is satisfied.(not (check))
to the goal.For example, our new action for making sure that messes aren’t made is the following:
(:action check-constraint
:precondition (and
(check)
(forall (?l - location) (not (mess-made ?l)))
)
:effect ((not (check)))
)
Our new domain file needs to modify every prior action:
(define (domain dinner)
(:requirements :typing)
(:types location - object)
(:predicates
(at ?1 - location) (CONNECTED ?l1 ?l2 - location)
(table-set) (dinner-ready) (mess-made ?l - location)
(holding-food) (at-food ?l - location) (check)
)
(:action walk
:parameters (?l1 ?l2 - location)
:precondition (and
(at ?l1)
(CONNECTED ?l1 ?l2)
(not (check))
)
:effect (and
(at ?l2)
(not (at ?l1))
(check)
(increase (total-cost) 10)
)
)
(:action dash
:parameters (?l1 ?l2 - location)
:precondition (and
(at ?l1)
(CONNECTED ?l1 ?l2)
(not (check))
)
:effect (and
(at ?l2)
(not (at ?l1))
(when (holding-food) (mess-made ?l2))
(check)
)
)
(:action set-table
:precondition (and
(at dining-room)
(not (holding-food))
(not (check))
)
:effect (and (table-set) (check))
)
(:action present-dinner
:precondition (and
(at dining-room)
(holding-food)
(table-set)
(not (check))
)
:effect (and (dinner-ready) (check))
)
(:action drop
:parameters (?l1 - location)
:precondition (and
(holding-food)
(at ?l1)
(not (check))
)
:effect (and
(not (holding-food))
(at-food ?l1)
(check)
)
)
(:action pickup
:parameters (?l1 - location)
:precondition (and
(not (holding-food))
(at ?l1)
(at-food ?l1)
(not (check))
)
:effect (and
(holding-food)
(not (at-food ?l1))
(check)
)
)
(:action cleanup
:parameters (?l - location)
:precondition (and (at ?l) (not (check)))
:effect (and (not (mess-made ?l)) (check))
)
(:action check-constraint
:precondition (and
(check)
(forall (?l - location) (not (mess-made ?l)))
)
:effect (not (check))
)
)
New Problem File:
(define (problem dinner)
(:domain dinner)
(:requirements :typing )
(:objects
kitchen living-room dining-room - location
)
(:init
(CONNECTED kitchen living-room)
(CONNECTED living-room kitchen)
(CONNECTED living-room dining-room)
(CONNECTED dining-room living-room)
(at kitchen)
(holding-food)
)
(:goal (and
(dinner-ready)
(not (check))
))
)
Fast Downward will find us a plan satisfying the constraint, but with a lot of check-constraint
actions.
drop kitchen
check-constraint
dash kitchen living-room
check-constraint
dash living-room dining-room
check-constraint
set-table
check-constraint
dash dining-room living-room
check-constraint
dash living-room kitchen
check-constraint
pickup kitchen
check-constraint
walk kitchen living-room
check-constraint
walk living-room dining-room
check-constraint
present-dinner
check-constraint
Perhaps we want to introduce some mercy and instead use a strike system. This would allow an agent to break a constraint at most $X$ times before saying they failed at a task.
For this example, let us use the two-strike system. That is once they make the second mess, it is considered that the agent has failed at the task.
We adjust the check-constraint
action from before to capture this new system:
(:action check-constraint
:precondition (check)
:effect (and
; We haven't striked out and satisfy the constraint
(when
(and
(forall (?l - location) (not (mess-made ?l)))
(not (strike-max))
)
(not (check))
)
; First time breaking the constraint
(when
(and
(not (forall (?l - location) (not (mess-made ?l))))
(not (strike-one))
)
(and
(strike-one)
(not (check))
)
)
; Second time breaking the constraint
(when
(and
(not (forall (?l - location) (not (mess-made ?l))))
(strike-one)
)
(strike-max)
)
)
)
Notice that each of these conditional effects are mutually exclusive from each other. We can use a different encoding, but having only one conditional effect fire after each execution makes it easier to reason about.
The problem file stays the same as the last example, and asking Fast Downward for an optimal plan results in the following:
dash kitchen living-room
check-constraint
cleanup living-room
check-constraint
drop living-room
check-constraint
dash living-room dining-room
check-constraint
set-table
check-constraint
dash dining-room living-room
check-constraint
pickup living-room
check-constraint
walk living-room dining-room
check-constraint
present-dinner
check-constraint
The main difference between this plan and the last one is that it makes use of the strike system to first dash from the kitchen to the living room, creating a mess. Then it avoids messes for the rest of the plan.
]]>Thanks @revolteh for creating this!
]]>x
on this distribution or is it called y
? This may lead to a lot of distribution specific code.
It might not make sense to commit these changes to the main software repository. An alternative is to create patch files. These are files that capture exactly how you want to update a given file.
Let’s take the following C code as an example:
#include <stdio.h>
int main(void)
{
printf("Goodbye World\n");
return 0;
}
Instead, we want for it to print out the classic “Hello World”. To do this, first let us save the original file.
cp hello-world.c hello-world.c.orig
Then let’s edit hello-world.c
to produce the desired result
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
return 0;
}
Now we can create the patch file.
diff -Naur hello-world.c.orig hello-world.c > hello.patch
The options of diff
are the following:
Flag | Description |
---|---|
-N |
Treat absent files as empty |
-a |
Treat all files as text |
-u |
Output 3 lines of unified context |
-r |
Recursively compare any subdirectories found |
This will output the following patch file
--- hello-world.c.orig 2024-05-12 20:41:27.708297085 -0400
+++ hello-world.c 2024-05-12 20:41:36.742348955 -0400
@@ -2,7 +2,7 @@
int main(void)
{
- printf("Goodbye World\n");
+ printf("Hello World\n");
return 0;
}
In order to test out the patch, let us revert the hello-world.c file to it’s original state.
cp hello-world.c.orig hello-world.c
Now apply the patch
patch < hello.patch
To undo the patch
patch -R hello-world.c < hello.patch
This experience inspired me to work towards learning more Spanish again. My ancestry is one reason, though another strong reason is that Spanish is the second most widely spoken language in the United States.
As a child, I learned a mix of Spanish, French, and German in school. Sadly like many Americans, I did not retain much of what I learned. This is mostly because there aren’t many opportunities for me to use these languages from day-to-day. Everyone I interact with speaks English and many don’t know the three languages I was taught.
Given that there’s some level of upkeep for retaining a language. It doesn’t make sense for me to diversify and try to learn multiple languages. My hope is that by focusing on Spanish, I can at least get to a level where I can hold a simple conversation confidently.
The Common European Framework of Reference (CEFR) for languages provides a list of levels which mark a learner’s proficiency in a language. Roughly speaking, A is a basic user, B is an independent user, and C is a proficient user.
When most people learn a language, they strive to be fluent. They want to hit the C2 level of proficiency. I find this to be a somewhat unrealistic goal for myself as someone who doesn’t interact much with Spanish speakers.
For one, C2 proficiency requires advance analysis on field specific issues. I’m not currently looking to perform academic research in Spanish. Instead, I wish to be able to hold simple conversations with other Spanish speakers.
To help me on this journey, I took the mainstream route and downloaded Duolingo. There’s a lot of criticism online about the app’s focus on spaced repetition and psychological tactics it uses to pressure users to log on everyday.
I think, however, that it’s important to find what works for you. I found it easy to use the app to build a habit of studying Spanish since they create easily digestible lessons.
The app is also not a one-size fits all. I find that the app is a little slow in exposing the learner to new vocabulary. The reading and radio exercises are also not as challenging as watching a TV show or reading a book in Spanish.
What I should really be doing, is watching telenovelas.
]]>The latest change, is that for systems where I use Podman containers, I now no longer use docker-compose but instead rely on Podman Quadlets which are managed by systemd. This means one less dependency on the docker toolchain, and we all accepted the systemd future by now right?
Mo wrote a blog post earlier this year outlining how they work. I won’t reiterate that great work so I encourage you to check it out. Instead, I’ll paste one systemd configuration file of a podman container for future me to easily reference.
Hedgedoc Container file at /etc/containers/systemd/hedgedoc.container
[Container]
ContainerName=hedgedoc
HostName=hedgedoc
Image=lscr.io/linuxserver/hedgedoc:1.9.9
AutoUpdate=registry
Volume=/home/rozek/Volumes/hedgedoc/config:/config
EnvironmentFile=/home/brozek/Volumes/hedgedoc/docker.env
IP=10.77.1.91
Network=rozeknet
[Unit]
Requires=hedgedoc_database.service
After=hedgedoc_database.service
[Service]
Restart=always
[Install]
WantedBy=default.target
The following map (provided by OpenStreetMap) shows some of the highlights of the journey.
Landmarks:
We stayed at a hotel near the Yaletown-Roadhouse station. We started the day by walking across the bridge to Granville market.
This area has a lot of tourist-y souvenir shops. Great place to snag a few postcards. We had an early lunch at the Granville Island Public Market.
Granville Island is situated at False Creek. Luckily the next stop on our journey, the Science Center, is also on this creek. The most fun way to get between the two places is to take a ferry.
The last stop of the ferry is the Science Center. You can’t miss it, there’s a giant steel colored ball on top. The museum is highly interactive and is designed to impress. I learned there that Smart glass exists.
From the Science Center, we took a short walk to Chinatown. We didn’t end up doing too much there, mostly took in the scenery.
We then took a bus to Gastown. Every hour the local steamclock plays its version of the Westminster Chime.
We walked over to the Waterfront station to take a boat over to North Vancouver.^{1} The false creek ferry was small. This boat, however, wasn’t. I walked in and found it hilarious how it looks like your standard metro or bus.^{2}
At North Vancouver we visited the Shipyards. It provides what I think is one of the best views of the Vancouver City Center skyline.
We ate food at a local pizza place, and visited a few of the local bookstores in this area. The local bus system then took us to Stanley Park.
Stanley Park is a must see if you’re ever in Vancouver. The Vancouver Seawall surrounds the perimeter and offers stunning view of the city and the local mountains.
The park is large. To make the most of the journey, I recommend biking the path. When we went there were easily accessible bike rentals.
After exiting the perimeter of the park on the left side, we were looking for a place to return our bikes. Unfortunately a lot of the bike return spots were full! We luckily found exactly two empty spots at the English Bay Beach.
Even though the bike ride was extremely rewarding, it was also physically draining. We didn’t stay too long at the beach, and instead took an Uber to the hotel to rest.
]]>Forever stamps are first class one-ounce stamps. The price to ship these have gone up over time, but Forever stamps are always valid. The USPS fact sheet shows that it started back in 2007.
One of the coolest things about Forever stamps is that their design isn’t static. In fact, they release new designs often and multiple are usually for sale at a time. As of the time of writing, you can purchase them online at this link.
The latest stamps to come out in recent months are:
In recent years, Clare and I’s favorite stamps are:
Though really Clare says the Frog stamps are the best of all. I mean really, can you resist these?
]]>On the other hand some of my self-hosted services (and even my OPNsense router), require me to be slightly more diligent. That is to say, I have enough scars from letting my databases auto-update. I now make sure to check the migration notes.
However promptness-wise, I was not great. Usually a few months would go by before the thought of software updates came to mind. From there, I would manually check the current software version of each software service I was currently running.
Nowadays, that is no more! I am a completely changed person. Thanks to the power of RSS.
GitHub automatically generates RSS feeds for the releases page (as well as tags) for all of their hosted repositories. Plop that into your favorite RSS reader, and you can get notified whenever a new version is released!
]]>Let’s say, for example, that you want to deliver a package that’s currently sitting at location A
. The recipient said that you can drop it off at either their workplace B
or their home C
.
If you drop it off at a some location, then people will be nice enough to let you retrieve the package and place it back at location A
. Graphically, we can represent it as follows:
A state in classical planning is a set of atomic formulas. For our problem it will look like:
(:init
(at-package A)
; Delivery Paths
(CONNECTED A B)
(CONNECTED A C)
(CONNECTED A D)
; Retrieval paths
(CONNECTED B A)
(CONNECTED C A)
(CONNECTED D A)
)
The goal in classical planning is a set of ground atomic formulas and the negations of ground atomic formulas. That means, the following isn’t a “valid” goal in the classical planning model.
(:goal (or (at-package B) (at-package C)))
I put valid in quotation marks since if you use the Fast Downward planner, it will behind the scenes compile it to a valid planning problem. But what if you’re not fortunate enough to have a planner automatically do that for you? We’ll discuss three different techniques for handling disjunctive goals.
A plan that delivers a package to B
is valid for the goal of delivering it to either B
or C
. The same goes for delivering it to C
. Therefore, the set of valid plans for the disjunctive goal is the union of all the plans that satisfy either of the disjuncts.
Domain File:
(define (domain test-disjunct)
(:requirements :typing)
(:types location - object)
(:predicates (at-package ?l - location) (CONNECTED ?l1 ?l2 - location)
(:action move-package
:parameters (?l1 ?l2 - location)
:precondition (and
(at-package ?l1)
(CONNECTED ?l1 ?l2)
)
:effect (and
(at-package ?l2)
(not (at-package ?l1))
)
)
)
Problem File:
(define (problem test-disjunct)
(:domain test-disjunct)
(:requirements :typing)
(:objects
A B C D - location
)
(:init
(at-package A)
(CONNECTED A B)
(CONNECTED A C)
(CONNECTED A D)
(CONNECTED B A)
(CONNECTED C A)
(CONNECTED D A)
)
(:goal (at-package B))
; (:goal (at-package C))
)
Call Planner:
./fast-downward.sif --alias lama-first domain-disjunct.pddl problem-disjunct.pddl
Result:
move-package a b
Axioms or derived predicates are atomic ground formula that can only appear in the precondition of actions and the goal. That is, it cannot appear in the effect.
We can create a new derived predicate done
which when true means that we have satisfied the goal.
(:derived (done) (or (at-package B) (at-package C)))
Domain File:
(define (domain test-derived)
(:requirements :derived-predicates :typing )
(:types location - object)
(:predicates (at-package ?l - location) (CONNECTED ?l1 ?l2 - location) (done))
(:derived (done) (or (at-package B) (at-package C)))
(:action move-package
:parameters (?l1 ?l2 - location)
:precondition (and
(at-package ?l1)
(CONNECTED ?l1 ?l2)
)
:effect (and
(at-package ?l2)
(not (at-package ?l1))
)
)
)
Problem File:
(define (problem test-derived)
(:domain test-derived)
(:requirements :typing)
(:objects
A B C D - location
)
(:init
(at-package A)
(CONNECTED A B)
(CONNECTED A C)
(CONNECTED A D)
(CONNECTED B A)
(CONNECTED C A)
(CONNECTED D A)
)
(:goal (done))
)
Call Planner:
./fast-downward.sif benchmarks/domain-derived.pddl benchmarks/problem-derived.pddl \
--search "astar(blind())"
Result:
move-package a b
Note: Not all heuristics in FastDownward support axioms. In fact this problem is not limited to FastDownward. The planner call above uses the blind heuristic which supports axioms and is safe.
Before replacing that evaluator, however, make sure to read the docs to make sure that the heuristic states that it supports axioms, and that it is safe in the presence of them.
The core idea of this technique is to push the disjunction onto the search space. As with the last technique we create a new predicate done
. However, instead of automatically deriving done
through an axiom, we create an action for every disjunct whose precondition is that disjunct, and whose effect is done
.
Example:
(:action goal-B
:parameters ()
:precondition (at-package B)
:effect (done)
)
(:action goal-C
:parameters ()
:precondition (at-package C)
:effect (done)
)
Our new goal will be (:goal (done))
. After you plan using the compiled version of the problem, drop the actions goal-B
and goal-C
and you have a plan for the disjunctive problem.
This technique works great when you’re grabbing the first optimal plan, however, there are two considerations that need to be made when you grab more than just one plan^{1}.
(1) There’s nothing guarding against an infinite chain of goal-B
action calls. Hence the following is a valid plan:
move-package a b
goal-B
goal-B
goal-B
We can address this by adding (not (done))
to the preconditions of each of the goal actions.
(2) We have no effects that remove done
, so a valid plan can move to a state that satisfies the goal and then move away.
move-package a b
move-package b a
We can address this by adding (not (done))
to the effect of every action that is not the goal actions.
Domain File:
(define (domain test-compile)
(:requirements :typing)
(:types location - object)
(:predicates (at-package ?l - location) (CONNECTED ?l1 ?l2 - location) (done))
(:action move-package
:parameters (?l1 ?l2 - location)
:precondition (and
(at-package ?l1)
(CONNECTED ?l1 ?l2)
)
:effect (and
(at-package ?l2)
(not (at-package ?l1))
(not (done))
)
)
(:action goal-B
:parameters ()
:precondition (and
(at-package B)
(not (done))
)
:effect (done)
)
(:action goal-C
:parameters ()
:precondition (and
(at-package C)
(not (done))
)
:effect (done)
)
)
Problem File:
(define (problem test-compile)
(:domain test-compile)
(:requirements :strips)
(:objects
A B C D - location
)
(:init
(at-package A)
(CONNECTED A B)
(CONNECTED A C)
(CONNECTED A D)
(CONNECTED B A)
(CONNECTED C A)
(CONNECTED D A)
)
(:goal (done))
)
Planner Call:
To retrieve multiple plans, we can use the Kstar package by IBM. In particular, I had success with their Python package.
from kstar_planner import planners
from pathlib import Path
domain_file = Path("domain-compile.pddl")
problem_file = Path("problem-compile.pddl")
plans = planners.plan_topk(
domain_file=domain_file,
problem_file=problem_file,
number_of_plans_bound=3,
timeout=30
)
print(plans)
Output:
===
move-package a b
goal-b
===
move-package a c
goal-c
===
move-package a b
move-package b a
move-package a b
goal-b
Not sure why you would want to execute the last plan, but at least the package got to its destination at the end :D
The field of diverse planning looks at generating multiple plans for a given planning problem. The introduction to “Reshaping Diverse Planning” by Michael Katz and Shirin Sohrabi covers well the motivations behind this. ↩︎
Functions in Lean are required to be total and terminating. This means that there are some mathematical functions that cannot be represented. An example of which is the collatz conjecture.
def collatz_fail (n : Nat) : Nat :=
match n with
| (0: Nat) => (0: Nat)
| (1: Nat) => (1: Nat)
| x =>
if x % 2 = 0 then collatz_fail (x / 2)
else collatz_fail (3 * x + 1)
If you type the above function in Lean, you’ll get the following error:
fail to show termination for
collatz_fail
I can’t tell you how we would go about showing termination. After all, many brilliant mathematicians are attempting to solve this conjecture. Instead, we can represent the collatz procedure as an inductive proposition.
inductive collatz : Nat → Nat → Prop where
| c0 : collatz 0 0
| c1 : collatz 1 1
| ceven (n r : Nat) : ¬(n = 0) → n % 2 = 0 → collatz (n / 2) r → collatz n r
| codd (n r : Nat) : ¬(n = 1) → ¬(n % 2 = 0) → collatz (3 * n + 1) r → collatz n r
Our inductive proposition collatz
is built with the first argument representing our input n
and the second argument representing the result. Note that the first two constructors c0
and c1
denote the base cases. The second two constructors ceven
and codd
have various conditions on them:
ceven
, we first require that $n \ne 0$, and that $n$ is even. If that’s the case, then whatever r
is in the inductive proposition collatz (n / 2) r
, that r
will be the result in collatz n r
.codd
, the inductive proposition requires that $n \ne 1$ and that $n$ is odd. Then r
holds whatever value collatz (3 * n + 1) r
holds.To use this inductive proposition, we need to build up a proof.
example : collatz 5 1 := by
apply collatz.codd; trivial; trivial
show collatz 16 1
apply collatz.ceven; trivial; trivial
show collatz 8 1
apply collatz.ceven; trivial; trivial
show collatz 4 1
apply collatz.ceven; trivial; trivial
show collatz 2 1
apply collatz.ceven; trivial; trivial
show collatz 1 1
apply collatz.c1
For inductive propositions, Lean does not require that it’s definition is total or terminating. In fact, in general perhaps a different ordering of constructors can be called in order to produce two different results for r
! Our specific definition, however, only produces one r
for a given n
, and we can prove that in Lean.
theorem collatz_deterministic (n r1 r2: Nat) (H1: collatz n r1) (H2: collatz n r2) : r1 = r2 := by
revert r2
-- Look at each of the possible
-- constructors for collatz
induction H1
case c0 =>
intro r
intro (H: collatz 0 r)
cases H
case c0 => rfl
-- automatically detects c1 as a contradiction
case ceven => contradiction
case codd => contradiction
case c1 =>
intro r
intro (H: collatz 1 r)
cases H
-- automatically detects c0 as a contradiction
case c1 => rfl
case ceven => contradiction
case codd => contradiction
case ceven n r1 N0 NE H IH =>
-- N0 : n ≠ 0
-- NE : n % 2 = 0
-- IH : ∀ (r2 : ℕ), collatz (n / 2) r2 → r1 = r2
intro r2
intro (H: collatz n r2)
cases H
case c0 => contradiction
case c1 => contradiction
case ceven H2 => apply IH r2 H2
case codd => contradiction
case codd n r1 N1 NO H IH =>
-- N1 : n ≠ 1
-- NO : n % 2 = 1
-- IH : ∀ (r2 : ℕ), collatz (3 * n + 1) r2 → r1 = r2
intro r2
intro (H: collatz n r2)
cases H
case c0 => contradiction
case c1 => contradiction
case ceven => contradiction
case codd H2 => apply IH r2 H2
This means that we don’t have to worry about calling the wrong constructor. If it fails, we backtrack and try a different one. This leads to an effective proof strategy:
example : collatz 5000 1 := by
repeat (first |
apply collatz.c0 |
apply collatz.c1 |
apply collatz.codd; trivial; trivial; simp |
apply collatz.ceven; trivial; trivial; simp
)
Let’s return to our function collatz_fail
. The reason this function didn’t work is that it wasn’t obvious that the function terminates. We can force it to terminate by introducing a new variable t
that denotes the number of remaining steps before giving up and returning none
.
def collatz_fun (n t : Nat) : Option Nat :=
if t = 0 then none else
match n with
| (0: Nat) => (0: Nat)
| (1: Nat) => (1: Nat)
| x =>
if x % 2 = 0 then collatz_fun (x / 2) (t - 1)
else collatz_fun (3 * x + 1) (t - 1)
Instead of having to do the proof search from above, we can call eval
on this function
#eval collatz_fun 5 6
#eval collatz_fun 5000 29
Is there any relationship between the inductive proposition version and the function we just built? Ideally we would want to prove the following relationship: $$ \exists t, collatzfun(n , t) = some(r) \iff collatz(n, r) $$ Since we don’t know how to show whether collatz terminates, we can’t at this time prove the backwards direction. We can, however, prove the forward direction through induction on the number of steps.
theorem collatz_sound (n r : Nat) : (∃ t, collatz_fun n t = some r) → collatz n r := by
intro (H: ∃ t, collatz_fun n t = some r)
obtain ⟨t, H⟩ := H
revert H n r
show ∀ (n r : Nat), collatz_fun n t = some r → collatz n r
let motive := fun x : Nat => ∀ (n r: Nat), collatz_fun n x = some r → collatz n r
apply Nat.recOn (motive := motive) t
-- t = 0
case intro.zero =>
intro r n
intro (HFalse : collatz_fun r 0 = some n)
contradiction
-- t = S t1
case intro.succ =>
intro t
intro (IH : ∀ (n r : ℕ), collatz_fun n t = some r → collatz n r)
intro n r
intro (H : collatz_fun n (t + 1) = some r)
show collatz n r
unfold collatz_fun at H
-- Go into else case since (t1 + 1 ≠ 0)
simp [ite_false] at H
-- Consider each case for n
split at H
-- n = 0
case h_1 =>
-- H : some 0 = some r
have H : 0 = r := by
rewrite [Option.some.injEq] at H; assumption
suffices collatz 0 0 by
rewrite [<- H]; assumption
apply collatz.c0
-- n = 1
case h_2 =>
-- H : some 1 = some r
have H : 1 = r := by
rewrite [Option.some.injEq] at H; assumption
suffices collatz 1 1 by
rewrite [<- H]; assumption
apply collatz.c1
case h_3 N0 N1 =>
-- N0 : n ≠ 0
-- N1 : n ≠ 1
split at H
-- n % 2 = 0
case inl NE =>
-- NE : n % 2 = 0
-- H : collatz_fun (n / 2) t = some r
suffices collatz (n / 2) r by
apply collatz.ceven n r N0 NE; assumption
apply IH (n / 2) r H
-- n % 2 = 1
case inr NO =>
-- NO : n % 2 = 1
-- H : collatz_fun (3 * n + 1) t = some r
suffices collatz (3 * n + 1) r by
apply collatz.codd n r N1 NO; assumption
apply IH (3 * n + 1) r H
]]>I run a Dell PowerEdge R430 with an Intel Xeon E5-2643 v3 CPU. This gives me 12 physical cores, and with hyperthreading this presents itself as 24 logical cores.
Given that I mostly use my server as media storage, most of those CPUs sit idle most of the time. My thought is, how much power can I save if I disable some of these unused cores?
For idle workloads disabling the CPU does not result in any noticeable power savings. The power savings is significant, however, if you analyze the system under load.
To conduct this experiment, I used a Kill a Watt measuring device which monitors the power usage of whatever is plugged into it.
To disable a CPU in Linux, use the following command:
# Disable CPU 23
echo 0 | sudo tee /sys/devices/system/cpu/cpu23/online > /dev/null
Repeat for any CPUs you want to disable. You can see which CPUs are available using ls /sys/devices/system/cpu
. The htop
tool will display in addition to the CPU utilization, which CPUs are offline.
To get the system under load, I used the stress
tool.
# Spin off 24 dummy tasks that max out each CPU
stress -c 24
I ran the stress tool with the corresponding number of logical CPUs I had online to come up with the following table:
# Online Logical CPU | Power (Watts) |
---|---|
24 | 360 |
12 | 295 |
6 | 194 |
4 | 173 |
Cutting down to 4 logical CPUs can cut the power usage under load in half! Do note though, that this exchanges performance for power savings. If you are running a low amount of services on your home server or can wait a bit of extra time for a computation to finish, consider disabling some of your CPUs.
]]>We’ll go over three proofs today:
First, let’s define our set $A$
def A : Set Int := {x : Int | x^2 = 9}
We want to prove the following lemma:
lemma instA : A = ({3, -3} : Finset ℤ)
Recall that for sets, $A = S \iff A \subseteq S \wedge S \subseteq A$.
Since we’re given a specific Finset for $S$, it would be really nice if we can compute whether $S$ is a subset of $A$. In fact we can, as long as we prove a couple theorems first.
instance (n: Int) : Decidable (n ∈ A) := by
suffices Decidable (n^2 = 9) by
rewrite [A, Set.mem_setOf_eq]
assumption
apply inferInstance
First as shown above, we need to decide whether an integer is in $A$. Given how $A$ is defined, this is equivalent to seeing if $n^2 = 9$. Luckily for us, the core of Lean already has a decision procedure for this. We can have Lean apply the appropriate one by calling apply inferInstance
.
With this, we can now use #eval
to see if elements are in $A$ or not.
#eval List.Forall (· ∈ A) [-3, 3]
To say that a finite set $S$ is a subset of $A$, is to say that all the elements of $S$ are in $A$. Using the last theorem, we can prove that checking subsets in this direction is decidable.
instance (S : Finset Int) : Decidable (↑S ⊆ A) := by
rewrite [A]
rewrite [Set.subset_def]
show Decidable (∀ x ∈ S, x ∈ {x | x ^ 2 = 9})
apply inferInstance
Keep in mind that at this point it’s not necessarily decidable if $A \subseteq S$. This is because we haven’t established if $A$ is finite. However instead of trying to prove finiteness, let’s skip to the main proof and show that $A = \{3, -3\}$ classically.
lemma instA : A = ({3, -3} : Finset ℤ) := by
let S : Finset ℤ := {3, -3}
change (A = ↑S)
As stated before, set equality is making sure both are subsets of each other
suffices A ⊆ ↑S ∧ ↑S ⊆ A by
rewrite [Set.Subset.antisymm_iff]
assumption
Lets make use of our decidability proof to show $S \subseteq A$ in one line!
have H2 : ↑S ⊆ A := by decide
For the other side,
have H1 : A ⊆ ↑S := by
intro (n : ℤ)
-- Goal is now (n ∈ A → n ∈ {3, -3})
intro (H1_1: n ∈ A)
Let’s change H1_1
to be in a form easier to work with
have H1_1 : n^2 = 9 := by
rewrite [A, Set.mem_setOf_eq] at H1_1
assumption
To show that $n \in \{3, -3\}$, then it’s the same as saying that $n = 3$ or $n = -3$.
suffices n = 3 ∨ n = -3 by
show n ∈ S
rewrite [Finset.mem_insert, Finset.mem_singleton]
assumption
We can show this using a theorem from mathlib!
exact eq_or_eq_neg_of_sq_eq_sq n 3 H1_1
Lastly, we combine the two subset proofs to show equality
exact And.intro H1 H2
All together the proof looks like:
lemma instA : A = ({3, -3} : Finset ℤ) := by
let S : Finset ℤ := {3, -3}
change (A = ↑S)
suffices A ⊆ ↑S ∧ ↑S ⊆ A by
rewrite [Set.Subset.antisymm_iff]
assumption
have H1 : A ⊆ ↑S := by
intro (n : ℤ)
-- Goal is now (n ∈ A → n ∈ {3, -3})
intro (H1_1: n ∈ A)
have H1_1 : n^2 = 9 := by
rewrite [A, Set.mem_setOf_eq] at H1_1
assumption
suffices n = 3 ∨ n = -3 by
show n ∈ S
rewrite [Finset.mem_insert, Finset.mem_singleton]
assumption
exact eq_or_eq_neg_of_sq_eq_sq n 3 H1_1
have H2 : ↑S ⊆ A := by decide
exact And.intro H1 H2
We got to cheat a little by applying a mathlib
theorem in the last example. This one will require a little more technique. First, let’s start by defining our set $B$.
def B : Set Int := {n | -4 ≤ n ∧ n ≤ 15 ∧ n % 2 = 0}
As before, we can show that set membership in $B$ is decidable. We can make use of the fact that each condition within it is decidable through And.decidable
.
instance (n : Int) : Decidable (n ∈ B) := by
suffices Decidable (-4 ≤ n ∧ n ≤ 15 ∧ n % 2 = 0) by
rewrite [B, Set.mem_setOf_eq]
assumption
exact And.decidable
Sanity check to see that everything works as expected.
#eval List.Forall (· ∈ B) [-4, -2, 0, 2, 4, 6, 8, 10, 12, 14]
We can also show that checking if a finset is a subset of $B$ is decidable.
instance (S : Finset Int) : Decidable (↑S ⊆ B) := by
suffices Decidable (∀ x ∈ S, -4 ≤ x ∧ x ≤ 15 ∧ x % 2 = 0) by
rewrite [Set.subset_def, B]
assumption
apply inferInstance
The beginning of our proof stays the same
lemma instB : B = ({-4, -2, 0, 2, 4, 6, 8, 10, 12, 14} : Finset Int) := by
let S : Finset Int := {-4, -2, 0, 2, 4, 6, 8, 10, 12, 14}
change (B = ↑S)
-- To show equality, we need to show that
-- each is a subset of each other
suffices ((B ⊆ ↑S) ∧ (↑S ⊆ B)) by
rewrite [Set.Subset.antisymm_iff]
assumption
have H2 : ↑S ⊆ B := by decide
For the other direction we want to show that is $n$ meets the condition to be in $B$, then it must be in $S$.
have H1 : B ⊆ ↑S := by
intro (n : ℤ)
intro (H : n ∈ B)
have H1 : -4 ≤ n ∧ n ≤ 15 ∧ n % 2 = 0 := by
rewrite [B] at H; assumption
clear H
show n ∈ S
Since $S$ is a finset, we can say that $n$ must be equal to one of the elements of $S$.
repeat rewrite [Finset.mem_insert]
rewrite [Finset.mem_singleton]
At this point, the problem is integer arithmetic and we can call the omega
tactic to finish the subproof.
omega
With both sides subset proven, we use and introduction to prove the goal
exact show ((B ⊆ ↑S) ∧ (↑S ⊆ B)) from And.intro H1 H2
The full proof is below.
lemma instB : B = ({-4, -2, 0, 2, 4, 6, 8, 10, 12, 14} : Finset Int) := by
let S : Finset Int := {-4, -2, 0, 2, 4, 6, 8, 10, 12, 14}
change (B = ↑S)
-- To show equality, we need to show that
-- each is a subset of each other
suffices ((B ⊆ ↑S) ∧ (↑S ⊆ B)) by
rewrite [Set.Subset.antisymm_iff]
assumption
have H1 : B ⊆ ↑S := by
intro (n : ℤ)
intro (H : n ∈ B)
have H1 : -4 ≤ n ∧ n ≤ 15 ∧ n % 2 = 0 := by
rewrite [B] at H; assumption
clear H
show n ∈ S
repeat rewrite [Finset.mem_insert]
rewrite [Finset.mem_singleton]
omega
have H2 : ↑S ⊆ B := by decide
exact show ((B ⊆ ↑S) ∧ (↑S ⊆ B)) from And.intro H1 H2
Finally let’s define our last set.
def C : Set Int := {n | n^2 = 6}
We’re aiming to show that $C$ is equivalent to the empty set. Therefore, we don’t have a need to show decidability for membership or finite subsets. We can’t make use of the mathlib theorem from Example 1, so how will we go about proving this?
Analyzing the last example more carefully, we had upper and lower bounds to work with. We can similarly identify these bounds for this example.
Let’s define the integer squared to be the upper bound.
lemma IntPow2GeSelf (H : (a : Int)^2 = z) : a ≤ z := by
have H1 : a ≤ a ^ 2 := Int.le_self_sq a
rewrite [H] at H1
assumption
For the lower bound, take its negation.
lemma NegIntPow2LeSelf (H : (a : Int)^2 = z) : a ≥ -z := by
To prove the lower bound, split up between positives and negatives using the law of excluded middle
have H1 : a ≥ 0 ∨ a < 0 := le_or_lt 0 a
For a positive $a$, we can rely on the linarith
tactic
have H_LEFT : a ≥ 0 → a ≥ -z := by
have HL1 : a ≤ z := IntPow2GeSelf H
intro (HL2 : a ≥ 0)
linarith
For a negative $a$, we need to prove it by induction
have H_RIGHT : a < 0 → a ≥ -z := by
intro (HR1: a < 0)
have HR1 : a ≤ -1 := Int.le_sub_one_iff.mpr HR1
revert HR1
suffices a ≤ -1 → a ≥ -(a^2) by
rewrite [<- H]; assumption
let P : ℤ → Prop := fun x => x ≥ -(x^2)
have H_base : P (-1) := by decide
have H_ind : ∀ (n : ℤ), n ≤ -1 → P n → P (n - 1) := by
intro (n : ℤ)
intro (H21 : n ≤ -1)
intro (H22 : P n)
simp_all
linarith
exact Int.le_induction_down H_base H_ind a
We have shown that this lower bound holds for both positive and negative $a$, therefore we can show that it holds for all $a$.
exact Or.elim H1 H_LEFT H_RIGHT
All together the lower bound proof is the following
lemma NegIntPow2LeSelf (H : (a : Int)^2 = z) : a ≥ -z := by
have H1 : a ≥ 0 ∨ a < 0 := by omega
have H_LEFT : a ≥ 0 → a ≥ -z := by
have HL1 : a ≤ z := IntPow2GeSelf H
intro (HL2 : a ≥ 0)
linarith
have H_RIGHT : a < 0 → a ≥ -z := by
intro (HR1: a < 0)
have HR1 : a ≤ -1 := Int.le_sub_one_iff.mpr HR1
revert HR1
suffices a ≤ -1 → a ≥ -(a^2) by
rewrite [<- H]; assumption
let P : ℤ → Prop := fun x => x ≥ -(x^2)
have H_base : P (-1) := by decide
have H_ind : ∀ (n : ℤ), n ≤ -1 → P n → P (n - 1) := by
intro (n : ℤ)
intro (H21 : n ≤ -1)
intro (H22 : P n)
simp_all
linarith
exact Int.le_induction_down H_base H_ind a
exact Or.elim H1 H_LEFT H_RIGHT
With our lower and upper bounds in place, we can look at the original problem. That is, we want to prove that $C = \emptyset$. We’ll start the same proof like before.
example : C = (∅ : Set Int) := by
suffices C ⊆ ∅ ∧ ∅ ⊆ C by
rewrite [Set.Subset.antisymm_iff]
assumption
have H2 : ∅ ⊆ C := Set.empty_subset C
We start the other direction the same as well.
have H1 : C ⊆ ∅ := by
intro (n : ℤ)
intro (H1_1 : n ∈ C)
have H1_1 : n^2 = 6 := by
rewrite [C, Set.mem_setOf_eq] at H1_1
assumption
Now bring in our bounds
have H1_2 : n ≤ 6 := by apply IntPow2GeSelf H1_1
have H2_3 : n ≥ -6 := by apply NegIntPow2LeSelf H1_1
We can use omega
to show that if the integer is within these bounds then $n$ must equal one of the integers.
have H1_4 : n ∈ ({-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6} : Finset ℤ) := by
repeat rewrite [Finset.mem_insert]
rewrite [Finset.mem_singleton]
omega
Turn this set into a disjunction
-- Make H1_4 a disjunction (n = -6 ∨ ... ∨ n = 6)
repeat rewrite [Finset.mem_insert] at H1_4
rewrite [Finset.mem_singleton] at H1_4
Trying to show that $n$ is within the empty set is the same as trying to prove a contradiction
show False
So let’s go through each disjunct in H1_4
and show that we derive a contradiction.
repeat (
cases' H1_4 with H1_4H H1_4
-- Plug in n = ?? to n^2 = 6
rewrite [H1_4H] at H1_1
contradiction
)
The last $n = 6$ is under a different name
rewrite [H1_4] at H1_1
contradiction
We can finally put it all together with and introduction
exact show C ⊆ ∅ ∧ ∅ ⊆ C from And.intro H1 H2
All together it’s the following:
example : C = (∅ : Set Int) := by
suffices C ⊆ ∅ ∧ ∅ ⊆ C by
rewrite [Set.Subset.antisymm_iff]
assumption
have H1 : C ⊆ ∅ := by
intro (n : ℤ)
intro (H1_1 : n ∈ C)
have H1_1 : n^2 = 6 := by
rewrite [C, Set.mem_setOf_eq] at H1_1
assumption
show False
have H1_2 : n ≤ 6 := by apply IntPow2GeSelf H1_1
have H2_3 : n ≥ -6 := by apply NegIntPow2LeSelf H1_1
have H1_4 : n ∈ ({-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6} : Finset ℤ) := by
repeat rewrite [Finset.mem_insert]
rewrite [Finset.mem_singleton]
omega
-- Make H3 a disjunction (n = -6 ∨ ... ∨ n = 6)
repeat rewrite [Finset.mem_insert] at H1_4
rewrite [Finset.mem_singleton] at H1_4
-- Try each one, plug in n = a into n^2 = 6 and show it doesn't work
repeat (
cases' H1_4 with H1_4H H1_4
rewrite [H1_4H] at H1_1
contradiction
)
-- Last n = 6 has a different name
rewrite [H1_4] at H1_1
contradiction
have H2 : ∅ ⊆ C := Set.empty_subset C
exact show C ⊆ ∅ ∧ ∅ ⊆ C from And.intro H1 H2
We gave three examples on working with finite integer sets. The two key lessons from this post are:
1. If the set is equal to a non-empty finite set, then try to prove decidability of membership and if a finset is a subset of it.
If you’re looking at a set of integers and this set is constructed with conditions that are mostly built-in (such as the <
relation), then this is hopefully not too difficult. Unfold the definitions of your set using rewrite
and have Lean auto-infer which decidability instance to call using apply inferInstance
2. Try to establish a lower and upper bound for the elements of your set.
This gives the omega
tactic something additional to work with. In the second example, omega
was able to close out the goal completely given the linear inequalities presented. In our last example, we only used omega
to construct a finite set of possible integers.
If you develop any other general techniques for dealing with integer sets let me know. Otherwise, feel free to get in touch with any questions you have.
]]>Before getting into how to do induction on integers through nat coercion, let’s discuss the proper way of solving this problem using mathlib
.
Let $x$ be an integer greater than some integer $m$. Show that $x$ satisfies some property. For example, prove $x > -5 \rightarrow x > -10$.
Okay, I hear you, linarith
can do this for us. Let’s pretend that doesn’t exist for now and show how we can go about solving this using mathlib
.
example (x : ℤ): x > -5 → x > -10 := by
-- Int.le_induction expects something of the form m ≤ x → ...
show -4 ≤ x → x > -10
-- Int.le_induction doesn't let you specify the
-- motive so you need to make it easy for it to induce
let P : ℤ → Prop := fun x => x > -10
-- Base Case
have H_base : P (-4) := by decide
-- Inductive case
have H_ind : ∀ (n : ℤ), -4 ≤ n → P n → P (n + 1) := by
intro (n : ℤ)
intro (HH1 : -4 ≤ n)
intro (HH2 : n > -10)
show n + 1 > -10
exact lt_add_of_lt_of_pos HH2 (show 0 < 1 by decide)
-- Use the induction principle
exact show (-4 ≤ x → x > -10) from Int.le_induction H_base H_ind x
Int.le_induction
is a nice and clean solution which I highly recommend you use. It makes sense too, prove that the property works for the lower bound and then prove the induction case holds.
Now what if for some reason you couldn’t rely Int.le_induction
and you wanted to find another way to go about this? This is where coercion comes in.
Starting from the beginning of the proof, this time let’s introduce the hypothesis.
intro (H : x > -5)
Now we want to create a natural number which we’ll perform induction over. Similar to the mathlib proof, we want to start with the lower bound as the base case. Therefore, make it so when $x$ is the lower bound our new natural number variable $n$ is 0.
let n : ℕ := Int.toNat (x + 5)
Our goal x > -10
is still written in terms of the original variable $x$. To rewrite this, we’ll need to prove the following relationship:
have H1 : x = n - 5 := by
-- We should start off by saying this relationship
-- holds for the integer version of n
let nz : ℤ := x + 5
have H1_1 : x = nz - 5 := eq_sub_of_add_eq (show nz = x + 5 by rfl)
suffices nz - 5 = n - 5 by rewrite [H1_1]; assumption
-- Through congruence closure we can ignore the (- 5)
suffices nz = n by
let Minus5 : ℤ → ℤ := fun x => x - 5
show Minus5 nz = Minus5 n
exact congrArg Minus5 this
-- An integer coercion is only equal to a nat
-- if the integer was 0 or positive to begin with
have H1_2 : nz ≥ 0 := by
have H : nz - 5 > 0 - 5 := by rewrite [<- H1_1]; assumption
have H1 : nz - 5 ≥ 0 - 5 := Int.le_of_lt H
exact (Int.add_le_add_iff_right (-5)).mp H1
exact show nz = n from (Int.toNat_of_nonneg H1_2).symm
With $x$ written in terms of $n$, we can now rewrite our goal using our natural number.
suffices n - 5 > (-10: ℤ) by rewrite [H1]; assumption
Here’s the rest of the induction. Of course like before, I avoid the use of linarith
so I wouldn’t be cheating ;)
have H_base : 0 - 5 > -10 := by decide
have H_ind : ∀ n : ℕ, n - 5 > (-10 : ℤ) → (n + 1) - 5 > (-10: ℤ) := by
intro n
intro (IH : n - 5 > (-10 : ℤ))
have IH : (-10: ℤ) < n - 5 := IH
show (-10: ℤ) < n + 1 - 5
have IH_2 : n + 1 - (5: ℤ) = n + (-5 : ℤ) + 1 := by calc
n + 1 - (5: ℤ) = n + 1 + (-5 : ℤ) := rfl
_ = n + (1 + (-5: ℤ)) := Int.add_assoc (↑n) 1 (-5)
_ = n + ((-5 : ℤ) + 1) := rfl
_ = n + (-5 : ℤ) + 1 := (Int.add_assoc (↑n) (-5) 1).symm
_ = n - (5: ℤ) + 1 := rfl
suffices -10 < n - (5: ℤ) + 1 by rewrite [IH_2]; assumption
exact lt_add_of_lt_of_pos IH (show 0 < 1 by decide)
exact show n - 5 > (-10: ℤ) from Nat.recOn
(motive := fun x: ℕ => x - (5 : ℤ) > (-10: ℤ))
n
H_base
H_ind
This version is slightly longer than the Int.le_induction
version as we had to carry forward the - 5
portion of the equations. While it might not make sense in the integer case to perform this coercion, I’m hopeful that I can use a technique similar to this on other inductive types in the future.
Let me know if you end up using this or if you have any other cool induction techniques in your tool belt. Until next time.
]]>Last year I wrote a Lean 3 tutorial which showcases how to go about constructing proofs for several small problems. After revisiting Lean 4 recently, I decided that Mathlib support is comprehensive and stable enough to make the switch.
This post is the same as the Lean 3 tutorial that I wrote last year, except that the examples have been updated to the latest syntax.
To prove something in Lean, we need to construct an object of that type.
To start off let us prove the statement $P \wedge Q$ given both $P$ and $Q$ individually.
example {p q : Prop} (H_p : p) (H_q : q) : (p ∧ q) := by
exact show p ∧ q from And.intro H_p H_q
This shows an application of conjunctive introduction. We created the object $P \wedge Q$ by applying the theorem And.intro
to both hypotheses H_p
which contains $P$ and H_q
which contains $Q$.
Another way of going about this proof is by transforming the goal to another equivalent one.
example {p q : Prop} (H_p : p) (H_q : q) : (p ∧ q) := by
constructor
case left =>
exact show p from H_p
case right =>
exact show q from H_q
In this proof, we make use of the constructor
tactic. Given a conjunctive goal $P \wedge Q$, this tactic replaces it with two subgoals $P$ and $Q$. We can then apply the hypotheses to solve the problem.
Given the proof of $P \wedge Q$, we can derive $P$ from it.
example {p q : Prop} (H_pq : p ∧ q) : p := by
exact show p from And.left H_pq
In the example above, we apply the inference And.left
to be able to derive the left side of a conjunct. Similarly to the last inference rule, there is a tactic based way to go about it. Tactic based methods are generally more prevalent within the ITP community.
example {p q : Prop} (H_pq : p ∧ q) : p := by
cases' H_pq with H_p H_q -- p, q
exact show p from H_p
Given a conjunctive hypothesis, the cases'
tactic will create two new hypothesis from it. The two labels after the with
is to specify the names of these hypotheses. The string --
denotes the start of a comment; I left a small comment to remind me the ordering of the conjuncts.
Given any statement $P$, we can introduce an arbitrary formula as a disjunct. More intuitively, if Jarold is above the age of 10, then he is above the age of 10 or he likes lollipops. It does not change the truth value of the statement.
The inference based way of representing this is as follows:
example {p q : Prop} (H_p : p) : (p ∨ q) := by
exact show p ∨ q from Or.intro_left q H_p
In the more popular tactic based notion. We use the tactic left
or right
to denote which side we’re going to attempt to prove. If we’re trying to prove $P \vee Q$ and we have $P$ then we will prove the left side.
example {p q : Prop} (H_p : p) : (p ∨ q) := by
left
exact show p from H_p
The form of disjunctive elimination included in Lean is more commonly known as proof by cases. That is if you know either $A$ or $B$ is true. That is, $A \vee B$. And, you can derive $C$ from $A$ as well as $C$ from $B$. Then it doesn’t matter which of $A$ or $B$ is true, because you can derive $C$ regardless.
Wikipedia has a nice intuitive example:
If I’m inside, I have my wallet on me.
If I’m outside, I have my wallet on me.
It is true that either I’m inside or I’m outside.
Therefore, I have my wallet on me.
To achieve this in Lean using the inference based approach.
example {p q r : Prop} (H_pr : p → r) (H_qr : q → r) (H_pq : p ∨ q) : r := by
exact show r from Or.elim H_pq H_pr H_qr
Alternatively, via the tactic based approach
example {p q r : Prop} (H_pr : p → r) (H_qr : q → r) (H_pq : p ∨ q) : r := by
cases' H_pq with H_p H_q
case inl =>
-- Assume p, apply modus ponens with H_pr
exact show r from H_pr H_p
case inr =>
-- Assume q, apply modus ponens with H_qr
exact show r from H_qr H_q
Traditional interactive theorem provers such as Coq focused on the constructivist approach to theorem proving. This allows them to export proofs as programs in OCaml. Lean places less of an emphasis on this approach and instead supports the proof by contradiction style you see in classical theorem proving.
For this tutorial, I decided to make this distinction
explicit by declaring a new axiom for the law of
excluded middle.
By default, Lean has the axiom of choice which
they use to prove the law of excluded middle
through the proof Classical.em
.
In other words, if you want to perform a proof by contradiction, don’t use the techniques shown in this section and instead use the by_contra
tactic or directly use Classical.em
.
Declare the axiom of law of excluded middle:
axiom LEM {p : Prop}: p ∨ ¬ p
For negation introduction, let’s say we have some proposition $P$. If we can use $P$ to derive a falsity, let’s say $Q \wedge \neg Q$ then $P$ must be false. That is, we introduce a negation to make it $\neg P$.
The structure of the proof will a proof by cases on the law of exclude middle.
lemma negation_intro {p q : Prop} (H_pq : p → q) (H_pnq : p → ¬q) : ¬p := by
have H_LEM : p ∨ ¬p := LEM
-- Assuming ¬p derive ¬p
have H_npp : ¬p → ¬p := by
intro (H_np : ¬p)
exact show ¬p from H_np
-- Assuming p derive ¬p
have H_pp : p → ¬p := by
-- Use hypotheses to obtain q and ¬q
intro (H_p : p)
have H_q : q := H_pq H_p
have H_nq : ¬q := H_pnq H_p
-- We can derive falsity from a direct contradiction
have H_false : False := H_nq H_q
-- You can derive anything from false
have H_UNUSED: p ∧ ¬p := False.elim H_false
-- Including what we want, ¬p
exact show ¬p from False.elim H_false
-- By proof by cases, we derive ¬p
exact show ¬p from Or.elim H_LEM H_pp H_npp
Lean can tell us which axioms our proof depends on as well.
#print axioms negation_intro
This returns 'negation_intro' depends on axioms: [LEM]
.
Alternatively, the tactic based approach
example {p q : Prop} (H_pq : p → q) (H_pnq : p → ¬q) : ¬p := by
have H_LEM : p ∨ ¬p := LEM
cases' H_LEM with H_p H_np
case inl =>
have H_q : q := H_pq H_p
have H_nq : ¬q := H_pnq H_p
have H_false : False := H_nq H_q
exact show ¬p from False.elim H_false
case inr =>
exact show ¬p from H_np
As I mentioned before, we do not have to explicitly declare the axiom of excluded middle. Instead we can make use of Lean’s builtin classical reasoning.
example {p q : Prop} (H_pq : p → q) (H_pnq : p → ¬q) : ¬p := by
by_contra H_p
have H_q : q := H_pq H_p
have H_nq : ¬q := H_pnq H_p
exact show False from H_nq H_q
One common representation of negation elimination is to remove any double negations. That is $\neg \neg P$ becomes $P$.
We’ll similarly show this by performing a proof by cases on the law of excluded middle.
example {p: Prop} (H_nnp : ¬¬p) : p := by
have H_LEM : p ∨ ¬p := LEM
-- Assuming ¬p derive p
have H_np2p : ¬p → p := by
intro (H_np : ¬p)
-- ¬p and ¬¬p are a direct contradiction
have H_false : False := H_nnp H_np
-- Derive our goal from a falsity
exact show p from False.elim H_false
-- Assuming p derive p
have H_p2p : p → p := by
intro (H_p : p)
exact show p from H_p
-- By proof by cases, we derive p
exact show p from Or.elim H_LEM H_p2p H_np2p
Alternatively for the tactic based approach.
example {p: Prop} (H_nnp : ¬¬p) : p := by
have H_LEM : p ∨ ¬p := LEM
cases' H_LEM with H_p H_np
-- Assuming p, derive p
case inl =>
exact show p from H_p
-- Assuming ¬p derive p
case inr =>
-- ¬p and ¬¬p are a direct contradiction
have H_false : False := H_nnp H_np
exact show p from False.elim H_false
Lean has this theorem built-in with Classical.not_not
.
Lean is also capable of reasoning over first order logic. In this section, we’ll start seeing objects/terms and predicates instead of just propositions.
For example {α : Type} {P : α → Prop}
means that $P$ is a predicate of arity one and takes an object of type $\alpha$.
If we have a forall statement, then we can replace the bound variable with an object of that type and remove the forall. Lets say for our example we have the following forall statement: $\forall x \in \mathbb{N}: x \ge 0$. Then we can replace the $x$ with $2$ and get the following formula: $2 \ge 0$.
example {α : Type} {P : α → Prop} {y : α} (H : ∀ x : α, P x) : P y := by
exact show P y from H y
To show that some property holds for all $x$ of a type α, you need to show that it holds
for an arbitrary $x$ of that type. We can introduce this object, by the intro
command.
example {α : Type} {P Q R : α → Prop} (H_pq : ∀ x : α, P x → Q x) (H_qr : ∀ x : α, Q x → R x) : ∀ x : α, P x → R x := by
intro (y: α)
have H_pqx : P y → Q y := H_pq y
have H_qrx : Q y → R y := H_qr y
intro (H_py : P y)
have H_qy : Q y := H_pqx H_py
exact show R y from H_qrx H_qy
To introduce an existential, you need to show that the formula holds for any object of a certain type.
example {α : Type} {P : α → Prop} {y : α} (H: P y) : ∃ x: α, P x := by
exact show ∃ x, P x from Exists.intro y H
In the tactic based approach, this is done via exists
:
example {α : Type} {P : α → Prop} {y : α} (H: P y) : ∃ x: α, P x := by
exact show ∃ x, P x from by exists y
Lets say we have the following forall statement: $\forall a \in \alpha: p a \implies b$.
Now lets say we have the following existential: $\exists x, p x$.
Using these, we can derive $b$.
example {α : Type} {p : α → Prop} {b : Prop} (H_epx : ∃ x, p x) (H_pab : ∀ (a : α), p a → b) : b := by
exact show b from Exists.elim H_epx H_pab
Alternatively for the tactic based approach:
example {α : Type} {p : α → Prop} {b : Prop} (H_epx : ∃ x, p x) (H_pab : ∀ (a : α), p a → b) : b := by
cases' H_epx with c H_pc
have H_pcb : p c → b := H_pab c
exact show b from H_pcb H_pc
One of the biggest use cases of an interactive theorem prover is in program verification. To help represent recursive data structures, we have the notion of an inductive type.
Let’s create a custom representation of a list.
A list can either by empty (cnil
) or be an element hd
combined with the rest of some list tl
.
inductive CustomList (T : Type) where
| cnil : CustomList T
| ccons (hd : T) -> (tl : CustomList T) : CustomList T
Some examples of a list here include cnil
,
ccons(0, cnil)
, and ccons(1, ccons(0, cnil))
.
For convenience, we’ll open the CustomList
namespace
so that we don’t have to refer each constructor
(cnil
/ccons
) by it.
open CustomList
To define a function over an inductive type, we need to cover each of the constructors.
For example, let’s consider the notion of a list’s length.
cnil
then the length is $0$.ccons
then we add 1 to the length of the tail tl
.def clength {α : Type}: CustomList α → Nat
| cnil => 0
| (ccons _ as) => 1 + clength as
We can see the output of a function via the #eval
command.
#eval clength (@cnil Nat)
#eval clength (ccons 2 (ccons 1 cnil))
For another example, let us look at appending two lists. As an example if we have the list [1, 2, 3]
and the list [4, 5, 6]
, then appending those two lists will create [1, 2, 3, 4, 5, 6]
.
def cappend {α : Type} (as bs : CustomList α) : CustomList α :=
match as with
| cnil => bs
| ccons a as => ccons a (cappend as bs)
Example evaluations, make sure these come out to what you expect.
#eval cappend (ccons 1 cnil) (ccons 2 cnil)
#eval clength (cappend (ccons 1 cnil) (ccons 2 cnil))
Now that we have a data structure and some methods over it. We can now prove some interesting properties.
First let’s start off with the following theorem.
Appending
cnil
to a listas
is equivalent to the listas
.
When instantiating an inductive type, the inductive hypothesis is created for it via recOn
.
Therefore, we can rely on that for the proof.
theorem append_nil {α : Type} (as : CustomList α) : cappend as cnil = as := by
-- Base Case
have H_BASE : (cappend cnil cnil = (@cnil α)) := by rfl
-- Inductive Step
have H_Inducive : ∀ (hd : α) (tl : CustomList α), cappend tl cnil = tl → cappend (ccons hd tl) cnil = ccons hd tl := by
intro (hd : α)
intro (tl : CustomList α)
intro (IH : cappend tl cnil = tl)
calc
cappend (ccons hd tl) cnil = ccons hd (cappend tl cnil) := by rw [cappend]
_ = ccons hd tl := by rw [IH]
-- Apply induction principle
exact CustomList.recOn (motive := fun x => cappend x cnil = x)
as
H_BASE
H_Inducive
The rewrite
/rw
command allows us to replace instances of functions with their definitions. The goal is to get both sides of the equality to be syntactically the same.
The calc
environment allows us to perform multiple rewrites in order to get closer to that end.
Instead of explicitly making use of recOn
, we can use the induction
tactic.
theorem append_nil2 {α : Type} (as : CustomList α) : cappend as cnil = as := by
induction as
case cnil =>
calc
cappend cnil cnil = cnil := by rw [cappend]
_ = cnil := by exact rfl
case ccons hd tl IH =>
calc
cappend (ccons hd tl) cnil = ccons hd (cappend tl cnil) := by rw [cappend]
_ = ccons hd tl := by rw [IH]
For our next example, we’ll need to perform induction on two lists.
Given two lists
as
andbs
. The length of their append is the same as the length of each individual list added together.
theorem length_append_sum {α : Type} (as bs : CustomList α) : clength (cappend as bs) = clength as + clength bs := by
induction as
induction bs
case cnil.cnil =>
calc
clength (cappend cnil cnil) = clength cnil := by rw [cappend]
_ = 0 := by rw [clength]
_ = 0 + 0 := by linarith
_ = (clength cnil) + (clength cnil) := by rw [clength]
case cnil.ccons hd tl _ =>
calc
clength (cappend cnil (ccons hd tl)) = clength (ccons hd tl) := by rw [cappend]
_ = 0 + clength (ccons hd tl) := by linarith
_ = clength cnil + clength (ccons hd tl) := by rw [clength]
case ccons hd tl IH =>
calc
clength (cappend (ccons hd tl) bs) = clength (ccons hd (cappend tl bs)) := by rw [cappend]
_ = 1 + clength (cappend tl bs) := by rw [clength]
_ = 1 + (clength tl + clength bs) := by rw [IH]
_ = (1 + clength tl) + clength bs := by linarith
_ = clength (ccons hd tl) + clength bs := by rw [clength]
-- Lean intelligently wlogs the fourth case
Notice that a couple times in the proof, make use of a tactic called linarith
. This stands for “linear arithmetic” and helps solve goals involving numbers. I generally employ this rather than figuring out which definitions I need to perform commutativity and associativity rules.
One of my favorite things about Lean is the ability to make proofs
more readable by making use of the have
, calc
, and rewrite
tactics. I do find it easier, however, to use the induction
tactic
for inductive proofs. Especially when dealing with nested
inductions as writing out the cases explicitly can be daunting.
If you catch any mistakes in me converting this post, let me know. Otherwise feel free to email me if you have any questions.
Lastly, I want to give my thanks to James Oswald for helping proofread this post and making it better.
]]>For example we know that $\{ P, P \rightarrow Q \} \vdash Q$ holds. In classical first order logic, it is also the case that $\{ P, P \rightarrow Q, R\} \vdash Q$ holds.
That is to say, adding additional formulae does not affect the result. In fact in classical first order logic, adding contradictory formulae also does not affect the provability of a given statement. This is due to the principle of explosion which states that from a contradiction we can derive anything.
What can we say about the other direction? How do we know when something does not hold? Let’s say we know the following statement: $\{ R, Z, P \rightarrow Q \} \not\vdash Q$. What can we say about $\{ Z, P \rightarrow Q \} \not\vdash Q$?
Intuitively, if we cannot show a formula $Q$ given some set of information. Then in classical first order logic, it goes to show that we cannot show $Q$ with less information.
Let’s discuss how this works in practice. Let $P$ be the set of assumptions which prove a corresponding goal. That is, given $(\Gamma_i, G_i) \in P, \Gamma_i \vdash G_i$. On the other hand, let $N$ be the set of assumptions which don’t prove a corresponding goal, i.e. $(\Gamma_i, G_i) \in N, \Gamma_i \not\vdash G_i$.
Given a problem $\Gamma \vdash G$. We perform two caching checks before calling the full automated theorem prover.
If either of those two conditions don’t get matched, then we can call the full automated theorem prover to determine the result and cache it in either $P$ or $N$.
This caching technique might not work well in your application. It works well in Spectra since we’re trying to prove a small set of goals and action preconditions and the assumptions consist of state variables which don’t vary by much between steps.
Though keep in mind that this technique will give you flawed answers if the corresponding logic is non-monotonic. Classical first-order and propositional logic is monotonic, however, so this caching technique is safe to use in those settings.
In non-monotonic or defeasible logic, you could have a statement $B^2(\neg P)$ which defeats another statement $B^1(P)$. We can read the last example as “I have a strong belief that P does not holds and a weak belief that P holds”. This depending on the defeasible logic, can change whether of not given $B^\sigma(P \rightarrow Q)$, if $B^{\sigma_i}(Q)$ holds.
]]>For the whole animated version, check out my tilde.club page.
The picture above is a PNG screenshot, but for the card itself I wanted to use SVGs as the primary graphics format to create the heart.
If you were to ask me why, I would claim it’s because SVGs are flexible when it comes to size. Though if I was to be honest, it’s beause SVGs are a plaintext format that I can edit.
Looking around for a tool to create this, I came across a SVG Pixel Art project from Jero Soler.
The editor provides a very familiar experience to Gimp and Photoshop. It includes advance features like layering which I didn’t feel the need to use for this quick drawing.
The available colors leave some to be desired. After downloading the SVG, I was able to edit the fill
attribute with the hex values of the tacky colors I wanted to use instead.
I recommend trying the website out yourself next time you want to make some pixel art. For me, it’s less daunting and more familiar to use than Inkscape, the more prevalent svg editing tool.
]]>try:
for _ in range(5):
sleep(1)
except KeyboardInterrupt:
# Awesome task 1
# Awesome task 2...
pass
Especially if you end up capturing the same exceptions and handling it the same way.
try:
for _ in range(5):
sleep(1)
except KeyboardInterrupt:
# Awesome task 1
# Awesome task 2...
pass
try:
for _ in range(2):
sleep(1)
except KeyboardInterrupt:
# Awesome task 1
# Awesome task 2...
pass
One way to make our code more DRY (don’t-repeat-yourself) is to make use of Python’s context managers.
@contextmanager
def handle_sigint():
try:
yield
except KeyboardInterrupt:
# Awesome task 1
# Awesome task 2...
pass
Using the context manager, everything within the indented block gets executed within the try block.
with handle_sigint():
for _ in range(5):
sleep(1)
with handle_sigint():
for _ in range(2):
sleep(1)
In fact, we can write this in a generic way to give us an alternative syntax for handling exceptions.
@contextmanager
def handle_exception(f, *exceptions):
try:
yield
except exceptions as e:
f(e)
For example, let’s tell the user that we’re explicitly ignoring their exception
def ignore(e):
print("Ignoring", e.__class__.__name__)
with handle_exception(ignore, NotImplementedError, KeyboardInterrupt):
for _ in range(5):
sleep(1)
Instead of presenting the states in a list, I thought it would be very cool to visually show it on a map. Ideally the map would be encoded in a SVG so that:
Now imagine my surprise after searching on Kagi to come across a SVG in the public domain on Wikipedia. Even better, the following is shown near the top of the file
/* Individual states can be colored as follows: */
.ks,.mt,.pa {fill:#0000FF}
.ca,.de {fill:#FF0000}
/* In this example, Kansas, Montana and Pennsylvania are colored blue,
and California and Delaware are colored red.
Place this code in the empty space below. */
Such an elegant way of coloring in the states! Now it’s up to me to come up with some categories. As of the time of writing, I decided on:
Here’s how I colored by map:
For the last couple weeks, I’ve been brainstorming new pages for my website and came up with https://brandonrozek.com/visited.
At the time of writing, it only has the colored in states and a list of cities I’ve been to. Though I’m welcome to any suggestions you might have. Feel free to get in touch!
]]>https://brandonrozek.com/blog/wordguess/
It's currently up on tilde.club where once a day players can guess the word and compare scores with other players.
]]>Three games that are featured in the message-of-the-day in Tilde.club are:
These games are designed to be multiplayer, but it doesn’t need to in order to be a social game. A large example of this recently, is the game Wordle.
Wordle is designed to be played by oneself. A challenge is issued once a day, and the player gets a score based on their performance. The players can then compare the scores with each other and strive to beat each other on future days.
WordGuess is a Wordle inspired game I made designed to run on a pubnix. It relies on the Linux permission system to authenticate players and keeps a leaderboard showing players scores for each of the days.
Below is how it looks like for a player to SSH into the pubnix and play the game:
For those on the Tilde.club server, it’s already setup and ready to play. Run the following command to start:
python /home/brozek/WordGuess/client.py
After playing the game you’re welcome to see the scores of others that day:
python /home/brozek/WordGuess/leaderboard.py
If you’re not on Tilde.club. You’re welcome to set it up yourself on any Linux server or pubnix. The instructions are listed on the Github.
]]>One of my favorite pastimes of late is watching my university’s hockey team. RPI Men’s hockey compete in the ECAC hockey league which is a division 1 league under the NCAA. In other words, hockey is a pretty big deal at the university,
Hockey is a full-contact sport with players going back and forth on the ice at high speeds. You can tell it’s a strenuous sport because the players only stay on the ice for a shift of on average 45 seconds.
Part of the fun is the camaraderie among the attendees. We have a full set of vocal cheers that we scream when penalties are announced, goals are obtained, and other in-game events. We call them vocal cheers, however, the majority of them consists of yelling that the other team sucks.
I’ve attended at least 5 games at this point. I wouldn’t say that I’m an expert at the rules of hockey, but I’ve picked up on the most common calls.
For resets:
It’s also not hockey without talking about penalties.
Most of these penalties (unless injury occurs) result in a 2 minute timeout for the player that committed the infraction. The player does not get replaced, resulting in the team that committed the infraction having one less active player. This time period is marked as a power play for the other team.
There are 3 periods in ice hockey with each period lasting 20 minutes. If there are any special performances at RPI, it typically happens after the first period. For example, we’ve had several ice skating performances. After the second period, RPI holds the “chuck the puck” competition and the 50/50 raffle. Zambonis come out after these events to resurface the ice.
Lastly, we cannot forget about the pep band and our good ol’ mascot Puckman. They don’t come out every game but it’s great when they do.
]]>(At this point you can freeze for easy cooking later. Make sure to defrost before continuing to the next step.)
We’ll consider the following message for the rest of the post:
from dataclasses import dataclass
@dataclass
class QueryUserMessage:
auth_key: str
username: str
Let’s say we have a message we want to send:
message = QueryUserMessage("lkajdfsas", "brozek")
We first need to get its dictionary representation. Luckily the standard library has us there:
from dataclasses import asdict
message_dict = asdict(message)
Then we can use the json
module to give us a string representation
import json
message_str = json.dumps(message_dict)
Finally, we can encode it into bytes and send it away:
# Default encoding is "utf-8"
message_bytes = message_str.encode()
# Assuming connetion is defined...
connection.sendall(message_bytes)
To make this easier for myself, I create a custom json
encoder and a function that uses the connection to send off the message
class DataclassEncoder(json.JSONEncoder):
def default(self, o):
return asdict(o)
def send_message(connection, message_dataclass):
contents = json.dumps(message_dataclass, cls=DataclassEncoder).encode()
connection.sendall(contents)
On the other end, let us receive the bytes and decode it into a string:
MESSAGE_BUFFER_LEN = 1024
message_bytes = connection.recv(MESSAGE_BUFFER_LEN)
message_str = message_bytes.decode()
We can use the json
module to turn it into a Python dictionary
message_dict = json.loads(message_str)
In this post, we can make use of the fact that we only have one message class. In other cases, you would either want to rely on some protocol or pass in the message type ahead of time. Therefore, we can pass the fields of the dictionary straight to the constructor.
message = QueryUserMessage(**message_dict)
In production use cases, we’ll need to introduce a gambit of error-handling to capture failures in json de-serialization and class instantiation. I hope, however, that this serves as a good starting point.
Some things to consider:
https://ognjen.io/using-robots-txt-to-discover-hidden-content/
Like why does the verge not want to index the iPhone 15 event? 🤔
]]>For example, consider one user has the following in ~/.cadastre/home.txt
:
0 0
####################
# ________ #
# /~\__\___\ #
# | | | | ,,,, #
# ,,,,, #
# ~~~~~ ,,,,, #
# ~~~~~ _ #
# ~~~~~ (*) #
# ~deepend | #
# #
####################
While another user has:
0 1
~~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
~~ ~~~~~~~~~~~~~
~ ~~~ ~~~~~~~~~~
~~~~~~~~~ ~~~ ~
~~~~~~~~~~~ ~~~
~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
~~~~~~~~~~~~~~~~~~~~~~~
Then the stitched together town will be:
#################### ~~~~~~~~~~~~~~~~~~~~~~~~
# ________ # ~~~~~~~~~~~~~~~~~~~~~~~
# /~\__\___\ # ~~~~~~~~~~~~~~~~~~~~~~~
# | | | | ,,,, # ~~~~~~~~~~~~~~~~~~~~~~~
# ,,,,, # ~~ ~~~~~~~~~~~~~
# ~~~~~ ,,,,, # ~ ~~~ ~~~~~~~~~~
# ~~~~~ _ # ~~~~~~~~~ ~~~ ~
# ~~~~~ (*) # ~~~~~~~~~~~ ~~~
# ~deepend | # ~~~~~~~~~~~~~~~~~~~~~~~
# # ~~~~~~~~~~~~~~~~~~~~~~~
#################### ~~~~~~~~~~~~~~~~~~~~~~~
Ascii art is something that always interested me, but I never took the time to make. Cadastre limits us to 24x12 characters which serves as a good starting canvas for myself.
You might be able to tell, but I’m a big fan of mountains.
+=======================+
. /\ .
. / \ /\ .
. / \/ \/\ .
. / \ _ \ \ .
. ____// .
. ___/____/ @@@ .
. ___/ @@@@@@@ .
. @@@@@@@@@ .
. @@@@@@@ .
. ~brozek | | .
.........................
Check out my plot among the village over at tilde.club. For a more lively instance check out tilde.town.
Thanks to ~troido for creating this cool piece of software!
]]>