I've been using blogger for the Physics Derivation Graph since 2015. That's 11 years and 297 posts, with many of those in 2020 due to the pandemic.
Blogger has a lot of good features, but the downside is that traffic associated with the content does not result in readers finding the primary website.
The first few thousand lines set the patterns for the project. When starting a new project, pay attention to getting the process, guidelines, and guardrails right from the start. Whenever something is being done for the first time make sure it's done clean. Those early patterns are what the agent replicates across for the rest of the project. Getting design wrong early and the whole project turns to garbage. If your codebase is clean, AI makes it cleaner and faster. If it's a mess, AI makes it messier faster.
Producing code does not imply value
The temporary dopamine hit from shipping with AI agents makes you blind. You think you're going fast, but zoom out and you actually go slower because of constant refactors from technical debt ignored early.
Project health and one-shot prompts
Measure of project health: when you want to do something, can you do it in 1 shot? If no then either the code is becoming a mess, you don't understand some part of the system well enough to craft a good prompt, or the problem is too big to tackle all at once and needs breaking down.
Skill and expertise still matter
There's a big difference between technical and non-technical people using LLMs to build production apps. Engineers who built projects before LLMs know what to watch out for and can detect when things go sideways. Non-technical people can't. Architecture, system design, security, and infra decisions will bite them later.
Design decisions are significant investments
Choosing the right framework, dependencies, or database schema, the foundation everything else is built on can't be done by giving your LLM a one-liner prompt. These decisions deserve more time than adding a feature.
LLM code is not optimized by default
LLM-generated code is not optimized for security, performance, or scalability by default. You have to explicitly ask for it and verify it yourself.
Review code changes
The LLM might use created_at as a fallback for birth_date. That won't be caught with just testing whether the feature works or not. The LLM is an actor pretending to fulfill a role.
LLMs are not databases of facts
Avoid using LLMs for facts and things that could be evaluated with a deterministic function (e.g., 1+1)
Having just configured the Oracle cloud server, and having used DigitalOcean for years, Hetzner's interface for initial configuration is closer to DigitalOcean than Oracle cloud. And the price of $3.50/month for 2 CPUs and 4GB of RAM with 40GB of storage is mindblowing compared to DigitalOcean's 2 CPUs with 1 GB of RAM and 25GB of storage for $6/month.
The -a (append) and -G (groups) options ensure the user is added to the new group without being removed from their other groups.
Validating,
# groups pdg
pdg : pdg sudo users pdg_grp
# id pdg
uid=1000(pdg) gid=1000(pdg) groups=1000(pdg),27(sudo),100(users),1001(pdg_grp)
SSH
Next, disable root from logging in via SSH.
First, copy my public cert from my laptop to the server
$ ssh-copy-id pdg@SERVERIPHERE
On the server I exited the root SSH session and logged back in as pdg:
$ ssh -i ~/.ssh/KEYNAMEHERE pdg@SERVERIPHERE
$ sudo vi /etc/ssh/sshd_config
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
AuthorizedKeysFile .ssh/authorized_keys .ssh/authorized_keys2
$ mkdir ~/.ssh
$ chmod 700 ~/.ssh
$ cat ~/.ssh/authorized_keys
$ sudo systemctl daemon-reload
$ sudo systemctl restart ssh.socket
Updating bash configuration
Added to ~/.bash_aliases
lias vi='vim'
alias s='git status'
alias p='git push'
# do not overwrite existing files
set -o noclobber
alias nothing='docker ps; git pull; git push; git status'
alias ll="ls -hal"
alias ..="cd .."
alias grin="grep -R -i -n --color"
and to ~/.bashrc
# HISTSIZE determines the number of commands remembered in memory during the current session.
# HISTFILESIZE determines the maximum number of lines allowed in the history file
export HISTSIZE=100000
export HISTFILESIZE=200000
export HISTTIMEFORMAT="%h %d %H:%M:%S "
shopt -s histappend
shopt -s cmdhist
$ sudo ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] Anywhere DENY IN 156.59.198.0/24
[ 2] Anywhere DENY IN 114.119.147.0/24
[ 3] Anywhere DENY IN 104.210.140.0/24
[ 4] 22/tcp ALLOW IN Anywhere
[ 5] 443 ALLOW IN Anywhere
[ 6] 80 ALLOW IN Anywhere
[ 7] 22/tcp (v6) ALLOW IN Anywhere (v6)
[ 8] 443 (v6) ALLOW IN Anywhere (v6)
[ 9] 80 (v6) ALLOW IN Anywhere (v6)
Update OS
Running Ubuntu 24.04.3 LTS, default load is
System information as of Sat Jan 31 02:24:09 AM UTC 2026
System load: 0.04 Processes: 123
Usage of /: 3.0% of 37.23GB Users logged in: 1
Memory usage: 5% IPv4 address for eth0: 65.21.252.29
Swap usage: 0% IPv6 address for eth0: 2a01:4f9:c013:5897::1
Since my nginx isn't running baremetal, I can't use the recommended sudo certbot certonly --nginx.
Instead, I used
sudo certbot certonly --manual --preferred-challenges dns \
--server https://acme-v02.api.letsencrypt.org/directory \
-d derivationmap.net -d www.derivationmap.net \
-d allofphysics.com -d www.allofphysics.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for derivationmap.net and 3 more
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:
_acme-challenge.allofphysics.com.
with the following value:
ARANDOMLOOKINGSTRINGHERE
Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.allofphysics.com.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/derivationmap.net/fullchain.pem
Key is saved at: /etc/letsencrypt/live/derivationmap.net/privkey.pem
This certificate expires on 2026-05-01.
These files will be updated when the certificate renews.
NEXT STEPS:
- This certificate will not be renewed automatically.
Autorenewal of --manual certificates requires the use of
an authentication hook script (--manual-auth-hook) but one was not provided.
To renew this certificate, repeat this same certbot command
before the certificate's expiry date.
The renewal instructions on https://physicsderivationgraph.blogspot.com/2021/10/periodic-renewal-of-https-letsencrypt.html
should be valid when these certs expire?
To have the full range of options when setting up the VPS, first create a VCN (virtual cloud network) and Create a Subnet. That way, when you are setting up the VPS (virtual private server) you can select an existing VCN and subnet.
From the user dashboard, on the right-side "Build" menu select "Set up a network with a wizard"
"Create VCN" and then use the wizard for "Create VCN with internet connectivity"
VCN name: pdg_test_vcn
Then "Reserved public IPv4 address" > Create
Configuring my First VPS
From the user dashboard, on the right-side "Build" menu select "Create compute instance" wizard,
For "placement" I selected AD2 (no reason not to go with AD1 or AD3 as far as I can tell)
Under "Advanced Options" I selected "On-demand capacity" out of the options; see
https://docs.oracle.com/en-us/iaas/Content/Compute/Concepts/computeoverview.htm#capacity_types
for descriptions
Under "Image and shape" the default image is "Oracle Linux 9". My Digital Ocean droplet is currently "Ubuntu 24.04.3 LTS".
Oracle offers 8 version of Ubuntu, so I selected 24.04
For "Shape" I selected "virtual machine" (rather than "bare metal")
For "Shape series" I selected "Ampere" (rather than AMD or Intel) and "VM.Standard.A1.Flex" which is eligble for Free Tier.
In the Networking section I set up a Primary VNIC (virtual network interface card) which connects to a VCN (virtual cloud network). This is required for a public IP for the Internet.
Primary VNIC name: pdg_test_VNIC
Primary network: "create new virtual cloud network"
new virtual cloud network name: vcn-20260127-0933_pdg
Subnet: create new public subset; name: subnet-20260127-0933_pdg. CIDR block 10.0.0.0/24
Private
Result: Oracle doesn't have capacity in the AD
"Out of capacity for shape VM.Standard.A1.Flex in availability domain AD-1. Create the instance in a different availability domain or try again later.If you specified a fault domain, try creating the instance without specifying a fault domain. If that doesn’t work, please try again later.Learn more about host capacity."
as of 2024-09-02, lowest cost relevant configuration: "VPS 250 G11s"
3.35 eur is currently $3.71 usd
2 vCore (x86)
2 GB ECC RAM
64 GB SSD
Traffic included
Unfortunately, the US-based servers are only available for beefy configurations. The "VPS 250 G11s" is hosted in Germany; see https://www.netcup.com/en/server/vps
"AMD Compute Instance" has 1/8 OCPU and 1 GB memory
"Arm Compute Instance" has 4 Arm-based Ampere A1 cores and 24 GB of memory usable as 1 VM or up to 4 VMs Always Free 3,000 OCPU hours and 18,000 GB hours per month
https://docs.cloud.google.com/free/docs/free-cloud-features says "1 non-preemptible e2-micro VM instance per month in one of the following US regions: Oregon: us-west1. Iowa: us-central1. South Carolina: us-east1. 30 GB-months standard persistent disk. 1 GB of outbound data transfer from North America to all region destinations (excluding China and Australia) per month."
I have a VPS that runs Ubuntu 24.04.1 LTS (GNU/Linux 6.8.0-85-generic x86_64). The system was out-of-memory, causing the website to be unavailable. The post documents my troubleshooting and diagnosis.
When I logged into the server with the website not responding, the stats were
System information as of Fri Jan 16 10:12:48 UTC 2026
System load: 0.18
Usage of /: 87.9% of 23.17GB
Memory usage: 84%
Swap usage: 0%
Processes: 117
Users logged in: 0
I used top to see what processes were causing the load. "Shift+M" sorts by memory used.
I used sudo reboot and then, once the system was up, the usage was
System information as of Fri Jan 16 10:25:22 UTC 2026
System load: 0.06
Usage of /: 87.7% of 23.17GB
Memory usage: 51%
Swap usage: 0%
Processes: 108
Users logged in: 0
I received an email alert this morning with subject "Wachete error notification" which indicated my website wasn't responsive.
In a browser I verified that https://allofphysics.com/ is hanging. (No immediate error response.) After a few minutes I got "504 Gateway Time-out nginx/1.17.9"
I ssh'd into the VPS (virtual private server) and ran docker ps to verify the containers were running.
I used top to verify the CPU load and memory load. Two instances of gunicorn are using 2% of the CPU each and 10% of the RAM each. That's expected.
Next I logged into the VPS web portal to review system usage for the past 7 days. There is certainly a noticeable change of metrics that started suddenly yesterday:
The last interaction I had with the server (more than a week ago) was to update the HTTPS certificates using Let's Encrypt. Although the website had returned a 504 error I could check the certificate expiration in the browser. The certs were valid.
In the logs directory on the server
-rwxrwxrwx 1 usr usr 0 Sep 3 2024 auth.log*
-rw-r--r-- 1 usr usr 6194913 Dec 3 14:38 flask_critical_and_error_and_warning.log
-rw-r--r-- 1 usr usr 383596 Nov 26 06:27 flask_critical_and_error_and_warning.log.1
-rw-r--r-- 1 usr usr 9999931 Nov 23 18:02 flask_critical_and_error_and_warning.log.2
-rw-r--r-- 1 usr usr 9945758 Dec 3 14:42 flask_critical_and_error_and_warning_and_info.log
-rw-r--r-- 1 usr usr 9999983 Dec 2 16:14 flask_critical_and_error_and_warning_and_info.log.1
-rw-r--r-- 1 usr usr 9999938 Dec 1 16:40 flask_critical_and_error_and_warning_and_info.log.2
-rw-r--r-- 1 usr usr 1206714 Dec 3 14:42 flask_critical_and_error_and_warning_and_info_and_debug.log
-rw-r--r-- 1 usr usr 9999916 Dec 3 12:46 flask_critical_and_error_and_warning_and_info_and_debug.log.1
-rw-r--r-- 1 usr usr 9999926 Dec 3 00:53 flask_critical_and_error_and_warning_and_info_and_debug.log.2
-rw-r--r-- 1 usr usr 125459598 Dec 3 14:42 gunicorn_access.log
-rw-r--r-- 1 usr usr 166722892 Dec 3 14:42 gunicorn_error.log
-rw-r--r-- 1 root root 126147128 Dec 4 11:01 nginx_access.log
-rw-r--r-- 1 root root 28785863 Dec 4 11:01 nginx_error.log
Only nginx logs have today's date. That's consistent with the blocker being nginx.
Using tail -f nginx_access.log I see the latest entries are associated with https://webmaster.petalsearch.com/site/petalbot which says the crawler
"establish an index database which enables users to search the content of your site in Petal search engine and present content recommendations for the user in Huawei Assistant and AI Search services"
I'm running a webserver that uses nginx and runs on linux. I am interested in blocking certain IP address ranges. Should I configure nginx to filter IP ranges or should I filter using the linux firewall? I want to use the software I already have rather than add yet another tool for this blocking.
and learn that linux firewall is recommended over nginx.
Next question for Gemini 2.5 Flash LLM is
I'm using Ubuntu for a webserver. How do I determine what firewall is being used from the command line?
I then run the following on my VPS:
$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
443 ALLOW IN Anywhere
80 ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)
443 (v6) ALLOW IN Anywhere (v6)
80 (v6) ALLOW IN Anywhere (v6)
I then also verify that nft exists using
$ sudo nft list ruleset
# Warning: table ip filter is managed by iptables-nft, do not touch!
table ip filter {
chain ufw-before-logging-input {
}
...long output, snipped...
From a few minutes of reviewing tail -f nginx_access.log the major offenders for this denial-of-service (DOS) attack appear to be
The LLM had warned me that "If you find that the new deny rules are at the bottom of the list, you may need to use the insert function to put them at the top (e.g., position 1 and 2)."
Gemini 2.5 says the general best practice for firewall rules is:
Specific DENY rules (blocking known bad actors).
Specific ALLOW rules (allowing trusted hosts/networks).
General ALLOW rules (allowing public services).
General DENY rules (the default policy, often implied).
Gemini 2.5's advice was almost correct. The LLM got the rule indices wrong. Here are the commands I ran:
$ sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
443 ALLOW Anywhere
80 ALLOW Anywhere
Anywhere DENY 156.59.198.0/24
Anywhere DENY 114.119.147.0/24
Anywhere DENY 104.210.140.0/24
OpenSSH (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)
80 (v6) ALLOW Anywhere (v6)
$ sudo ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] OpenSSH ALLOW IN Anywhere
[ 2] 443 ALLOW IN Anywhere
[ 3] 80 ALLOW IN Anywhere
[ 4] Anywhere DENY IN 156.59.198.0/24
[ 5] Anywhere DENY IN 114.119.147.0/24
[ 6] Anywhere DENY IN 104.210.140.0/24
[ 7] OpenSSH (v6) ALLOW IN Anywhere (v6)
[ 8] 443 (v6) ALLOW IN Anywhere (v6)
[ 9] 80 (v6) ALLOW IN Anywhere (v6)
$ sudo ufw delete 4
Deleting:
deny from 156.59.198.0/24
Proceed with operation (y|n)? y
Rule deleted
$ sudo ufw insert 1 deny from 156.59.198.0/24 to any
Rule inserted
$ sudo ufw delete 4
Deleting:
allow 80
Proceed with operation (y|n)? n
Aborted
$ sudo ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] Anywhere DENY IN 156.59.198.0/24
[ 2] OpenSSH ALLOW IN Anywhere
[ 3] 443 ALLOW IN Anywhere
[ 4] 80 ALLOW IN Anywhere
[ 5] Anywhere DENY IN 114.119.147.0/24
[ 6] Anywhere DENY IN 104.210.140.0/24
[ 7] OpenSSH (v6) ALLOW IN Anywhere (v6)
[ 8] 443 (v6) ALLOW IN Anywhere (v6)
[ 9] 80 (v6) ALLOW IN Anywhere (v6)
$ sudo ufw delete 5
Deleting:
deny from 114.119.147.0/24
Proceed with operation (y|n)? y
Rule deleted
$ sudo ufw insert 2 deny from 114.119.147.0/24 to any
Rule inserted
$ sudo ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] Anywhere DENY IN 156.59.198.0/24
[ 2] Anywhere DENY IN 114.119.147.0/24
[ 3] OpenSSH ALLOW IN Anywhere
[ 4] 443 ALLOW IN Anywhere
[ 5] 80 ALLOW IN Anywhere
[ 6] Anywhere DENY IN 104.210.140.0/24
[ 7] OpenSSH (v6) ALLOW IN Anywhere (v6)
[ 8] 443 (v6) ALLOW IN Anywhere (v6)
[ 9] 80 (v6) ALLOW IN Anywhere (v6)
$ sudo ufw delete 6
Deleting:
deny from 104.210.140.0/24
Proceed with operation (y|n)? y
Rule deleted
$ sudo ufw insert 3 deny from 104.210.140.0/24 to any
Rule inserted
$ sudo ufw status numbered
Status: active
To Action From
-- ------ ----
[ 1] Anywhere DENY IN 156.59.198.0/24
[ 2] Anywhere DENY IN 114.119.147.0/24
[ 3] Anywhere DENY IN 104.210.140.0/24
[ 4] OpenSSH ALLOW IN Anywhere
[ 5] 443 ALLOW IN Anywhere
[ 6] 80 ALLOW IN Anywhere
[ 7] OpenSSH (v6) ALLOW IN Anywhere (v6)
[ 8] 443 (v6) ALLOW IN Anywhere (v6)
[ 9] 80 (v6) ALLOW IN Anywhere (v6)
where T is period of oscillation and f is frequency of oscillation.
A transformation would be to multiply both sides by f to get
f T = 1
Verification of a step using a Computer Algebra System like SymPy
The single step above could be verified using a Computer Algebra System like SymPy. The generic form of the inference rule is "multiply both sides of (LHS=RHS) by feed to get (LHS*feed=RHS*feed)". To show the inference rule was correctly applied, we want to show that
LHS_in*feed == LHS_out
and
RHS_in*feed == RHS_out
Another way to describe the equivalence is that the difference should be zero:
Wahoo! The step has been validated using SymPy to show the inference rule is applied correctly.
The cool part is that the "multiply_both_sides()" Python function is generic to any input expression. The same check can be used for many different steps that use the inference rule. Using SymPy we can gain confidence that the expressions associated with a step were correctly transformed.
Consistency of dimensionality using SymPy
In addition to evaluating the transformation of symbols in a step, we can verify the consistency of dimensions for each expression. That requires more than just the symbols -- the user will have to specify the dimensions of each symbol.
additional constraints are needed for reasoning around division by 0.
If you expect to avoid negative or zero frequency or period, you could define f and T to have be "positive real numbers" (which exclude zero; non-negative reals include zero). This does define the context more precisely, but there is a price - we won’t have nearly as many proofs for positive reals as we have for reals. The alternative is to add additional hypotheses as constraints. The latter case (additional hypotheses) is favored.
import Mathlib.Data.Real.Basic
import Mathlib.Tactic -- Import standard tactics, specifically import Mathlib.Tactic.FieldSimp
theorem inversion_amultbeq1
(a b : Real)
(hb_ne_zero : b ≠ 0) :
a = 1 / b <-> a * b = 1 := by
-- field_simp clears the denominator 'b' on the LHS,
-- turning (a = 1 / b) into (a * b = 1) automatically.
field_simp [hb_ne_zero]
->
I had been using Neo4j's property graph for the Physics Derivation Graph for a few years and recently stopped due to the 1GB of memory used by the server that is constantly running. (That 1GB of RAM is unrelated to the size of the underlying database, which in my case was a few hundred nodes.)
I realized that what I want is a SQLite-like "offline" database that only is noticeable to CPU and RAM use when there's a query. I'm ok with the database's initialization cost resulting slower queries.
Search for Alternatives
https://jgrapht.org/ and NetworkX are irrelevant to the needs of the Physics Derivation Graph
However, the most reliable approach may be to switch to SQLite and use LLMs to craft Cypher-like queries. This is inelegant (I had been exposing read-only Cypher capabilities on the web) and requires coming up with a schema (something I have no experience with).
Convert Property Graph to Relational Database for SQL
Here's the prompt I provided Gemini 2.5 Pro:
Response from Gemini:
Converting a property graph to a relational model involves translating nodes into tables and edges into relationships (either through foreign keys or dedicated join tables). Here is a comprehensive SQLite schema designed to represent your Neo4j graph structure, balancing relational purity with the flexibility of the original graph.
Design Principles
Nodes as Tables: Each distinct node type (`derivation`, `step`, `expression`, etc.) will become its own table.
Properties as Columns: The properties of each node will become columns in the corresponding table. The node's `id` will serve as the `PRIMARY KEY`.
Edges as Relationships:
One-to-Many / One-to-One Edges: These are modeled using a foreign key column in the "many" or "child" table. For example, since a `step` belongs to one `derivation`, the `steps` table will have a `derivation_id` column.
Many-to-Many Edges: These are modeled using a separate "join table" (or "link table") that contains foreign keys to the two connected tables. For example, a `step` can have many `expression` inputs, and an `expression` can be an input to many `steps`. This requires a `step_inputs` join table.
Polymorphic Edges: The `HAS_SYMBOL` edge is polymorphic because it can connect to multiple different types of symbol nodes (`scalar`, `vector`, etc.). To handle this cleanly without losing foreign key integrity, we will create a dedicated join table for each specific relationship (e.g., `expression_scalars`, `expression_vectors`). A more "graph-like" but less relationally pure alternative is discussed in the rationale section.
SQLite Schema Definition
Here are the `CREATE TABLE` statements for the SQLite database.
Node Tables. These tables store the data for each type of node.
-- The top-level container for a mathematical derivation.
CREATE TABLE derivations (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
abstract_latex TEXT,
reference_latex TEXT,
created_datetime TEXT NOT NULL
);
-- Represents a single logical step in a derivation.
-- It connects to a derivation (one-to-many) and an inference_rule (many-to-one).
CREATE TABLE steps (
id TEXT PRIMARY KEY,
derivation_id TEXT NOT NULL,
inference_rule_id TEXT NOT NULL,
sequence_index INTEGER NOT NULL, -- From the 'HAS_STEP' edge property
author_name_latex TEXT,
note_before_step_latex TEXT,
note_after_step_latex TEXT,
created_datetime TEXT NOT NULL,
FOREIGN KEY (derivation_id) REFERENCES derivations(id) ON DELETE CASCADE,
FOREIGN KEY (inference_rule_id) REFERENCES inference_rules(id) ON DELETE RESTRICT,
UNIQUE (derivation_id, sequence_index) -- A derivation cannot have two steps at the same index
);
-- Represents the mathematical content of an expression.
CREATE TABLE expressions (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
description_latex TEXT,
reference_latex TEXT,
latex TEXT,
lean TEXT,
sympy TEXT
);
-- Represents feed-in values or assumptions for a step.
CREATE TABLE feeds (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
sympy TEXT,
lean TEXT,
latex TEXT
);
-- Represents a rule of logic or mathematics used in a step.
CREATE TABLE inference_rules (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
latex TEXT,
number_of_inputs INTEGER,
number_of_outputs INTEGER,
number_of_feeds INTEGER
);
-- Represents a mathematical operation like addition or differentiation.
CREATE TABLE operations (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
description_latex TEXT,
reference_latex TEXT,
latex TEXT,
argument_count INTEGER
);
-- Represents a scalar value (a single number).
CREATE TABLE scalars (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
description_latex TEXT,
reference_latex TEXT,
latex TEXT,
scope TEXT,
variable_or_constant TEXT,
domain TEXT,
dimension_length INTEGER,
dimension_time INTEGER,
dimension_mass INTEGER,
dimension_temperature INTEGER,
dimension_electric_charge INTEGER,
dimension_amount_of_substance INTEGER,
dimension_luminous_intensity INTEGER
);
-- Represents a vector.
CREATE TABLE vectors (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
description_latex TEXT,
reference_latex TEXT,
latex TEXT
);
-- Represents a matrix.
CREATE TABLE matrices (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
description_latex TEXT,
reference_latex TEXT,
latex TEXT
);
-- Represents a quantum mechanical operator.
CREATE TABLE quantum_operators (
id TEXT PRIMARY KEY,
author_name_latex TEXT,
name_latex TEXT,
description_latex TEXT,
reference_latex TEXT,
latex TEXT,
requires_arguments INTEGER -- Using INTEGER for BOOLEAN (0=false, 1=true)
);
-- Represents the numerical value and units for a scalar.
-- This has a one-to-one relationship with a scalar.
CREATE TABLE values_and_units (
id TEXT PRIMARY KEY,
scalar_id TEXT NOT NULL UNIQUE, -- The UNIQUE constraint enforces the 1-to-1 relationship
author_name_latex TEXT,
dimension_length_unit INTEGER,
dimension_time_unit INTEGER,
dimension_mass_unit INTEGER,
dimension_temperature_unit INTEGER,
dimension_electric_charge_unit INTEGER,
dimension_amount_of_substance_unit INTEGER,
dimension_luminous_intensity_unit INTEGER,
FOREIGN KEY (scalar_id) REFERENCES scalars(id) ON DELETE CASCADE
);
Relationship (Join) Tables. These tables model the many-to-many relationships defined by the edges.
-- Links steps to their input expressions.
CREATE TABLE step_inputs (
step_id TEXT NOT NULL,
expression_id TEXT NOT NULL,
sequence_index INTEGER NOT NULL,
PRIMARY KEY (step_id, sequence_index),
FOREIGN KEY (step_id) REFERENCES steps(id) ON DELETE CASCADE,
FOREIGN KEY (expression_id) REFERENCES expressions(id) ON DELETE CASCADE
);
-- Links steps to their output expressions.
CREATE TABLE step_outputs (
step_id TEXT NOT NULL,
expression_id TEXT NOT NULL,
sequence_index INTEGER NOT NULL,
PRIMARY KEY (step_id, sequence_index),
FOREIGN KEY (step_id) REFERENCES steps(id) ON DELETE CASCADE,
FOREIGN KEY (expression_id) REFERENCES expressions(id) ON DELETE CASCADE
);
-- Links steps to their feed-in values.
CREATE TABLE step_feeds (
step_id TEXT NOT NULL,
feed_id TEXT NOT NULL,
sequence_index INTEGER NOT NULL,
PRIMARY KEY (step_id, sequence_index),
FOREIGN KEY (step_id) REFERENCES steps(id) ON DELETE CASCADE,
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE
);
-- Join tables for the polymorphic 'HAS_SYMBOL' relationships
-- These tables link expressions and feeds to the various symbol types they contain.
-- Expression-to-Symbol Links
CREATE TABLE expression_symbols (
expression_id TEXT NOT NULL,
symbol_id TEXT NOT NULL,
symbol_type TEXT NOT NULL, -- e.g., 'scalar', 'vector', 'matrix', 'operation', 'quantum_operator'
PRIMARY KEY (expression_id, symbol_id),
FOREIGN KEY (expression_id) REFERENCES expressions(id) ON DELETE CASCADE
);
-- Feed-to-Symbol Links
CREATE TABLE feed_symbols (
feed_id TEXT NOT NULL,
symbol_id TEXT NOT NULL,
symbol_type TEXT NOT NULL, -- e.g., 'scalar', 'vector', 'matrix', 'operation', 'quantum_operator'
PRIMARY KEY (feed_id, symbol_id),
FOREIGN KEY (feed_id) REFERENCES feeds(id) ON DELETE CASCADE
);
Rationale and Explanation
Direct Relationships (`derivation` -> `step`, `step` -> `inference_rule`): The `HAS_STEP` edge connects one derivation to many steps. This is a classic one-to-many relationship, best modeled by adding a `derivation_id` foreign key to the `steps` table. The `sequence_index` from the edge property is also placed here. The same logic applies to the `HAS_INFERENCE_RULE` edge.
Step Inputs/Outputs/Feeds: A `step` can have multiple ordered inputs, outputs, and feeds. These are many-to-many relationships modeled with the join tables `step_inputs`, `step_outputs`, and `step_feeds`. The `sequence_index` property from the edge is a crucial part of the primary key to maintain order and uniqueness.
One-to-One Relationship (`scalar` -> `value_and_units`): The `HAS_VALUE` edge is modeled by adding a `scalar_id` foreign key to the `values_and_units` table. Adding a `UNIQUE` constraint on `scalar_id` ensures that each scalar can have at most one `value_and_units` entry, perfectly modeling the one-to-one relationship.
Handling Polymorphism (`HAS_SYMBOL`): This is the most complex relationship to model. An `expression` or a `feed` can contain many symbols of different types.
Chosen Approach: The schema uses two tables, `expression_symbols` and `feed_symbols`. Each table stores the ID of the source (`expression_id` or `feed_id`), the ID of the target symbol (`symbol_id`), and a `symbol_type` column to tell you which table to join with to get the symbol's details (e.g., `scalars`, `vectors`).
Advantage: This approach is pragmatic. It keeps the number of tables manageable.
Trade-off: It sacrifices full relational integrity because you cannot create a native `FOREIGN KEY` on `symbol_id` that points to five different tables. You would enforce this relationship at the application layer (e.g., when you retrieve an `expression`'s symbols, you read the `symbol_type` and then query the appropriate table).
In my assessment, the SQL schema is inelegant compared to the property graph representation.
The use of `ON DELETE CASCADE` is a guess by the LLM and is not always the desired behavior. (The `ON DELETE CASCADE` means that if the parent gets deleted then automatically delete the linked child.) The alternative is `ON DELETE RESTRICT` (or `NO ACTION`). It prevents deletion of the parent row as long as any child rows exist. The database would return an error. This is used when you want to protect the child data.
The above tailored-to-PDG schema is complicated. A simpler schema like the one below incurs a higher query cost.
From Gemini I learned that the EAV (Entity-Attribute-Value) model is a normalized and traditional approach that is portable across almost any relational database.
"Long time derivation of the Boltzmann equation from hard sphere dynamics" by Yu Deng, Zaher Hani, Xiao Ma
https://arxiv.org/abs/2408.07818
"Hilbert's sixth problem: derivation of fluid equations via Boltzmann's kinetic theory" by Yu Deng, Zaher Hani, Xiao Ma
https://arxiv.org/abs/2503.01800
I'm currently paying $12/month for my VPS (virtual private server) that has 2GB of RAM and 25GB of storage. I had been paying $6/month previously, but including Neo4j bumped the memory usage to nearly 100%. (CPU usage is averaging around 10% and spikes to 30%.)
I have a few iterations of the source code for the webserver so I'll need to figure out which is actually in use. After logging into the VPS, I see I have two folders:
allofphysics.com
ui_v7_website_flask_json
Using docker images I see the latest image (from 7 months ago) is allofphysicscom-flask. That doesn't help me figure out which repo is in use.
allofphysics.com/docker-compose.yaml has neo4j, whereas ui_v7_website_flask_json/docker-compose.yaml does not. Therefore I'm currently operating out of allofphysics.com/docker-compose.yaml
I have two options: either revert to the "v7" or disable neo4j in "allofphysics.com".
Going with option 1,
~/allofphysics.com$ docker compose down
~/allofphysics.com$ cd ~/ui_v7_website_flask_json
~/ui_v7_website_flask_json$ git pull
~/ui_v7_website_flask_json$ make
Turns out the packages are pretty old. The opencv package wasn't compatible. After I reverted all pip packages to known good version numbers I found the crypto libraries weren't happy. Eventually I was able to get a docker image built.
Next the certs weren't present so I copied those from ~/allofphysics.com/certs/ and that worked.
The LLM is not a database of facts. Historical events, dates, places are not stored as exact references. LLMs generate their response based on statistical probabilities derived from patterns.
The more widely documented something is, the better the LLM knows it
The LLM's training is roughly proportional to the representation of the information on the Internet. An LLM is more reliable and detailed when discussing common knowledge.
Precise questions using relevant jargon with context yields useful output
Poorly worded questions that do not use domain-specific terminology are less likely to produce clear answers.
Do not trust citations
The LLM does not have citations hard-coded into the network. Citations are most likely to be hallucinations
Decompose complex tasks and questions into a sequence of iterative prompts
There is a limited amount of "thinking" by the LLM per prompt, so simpler tasks are more likely to produce relevant answers.
Structure your question to produce a page or less of output
Producing a 200 page book from a single prompt devolves into hallucinations after a few pages. Shorter answers are more likely to remain lucid, so phrase your question in a way that can be answered with a small amount of text.
LLMs default to the average
While LLM output can be creative (in unexpected ways), seeking exceptional insight yields the mundane
Simplify your question to a one-shot prompt
Iterative questions are more likely to yield hallucinations
Delegation to an intern who doesn't learn
This can be confusing, as the LLM occasionally knows more than you do.
ERROR: Multi-platform build is not supported for the docker driver.
Switch to a different driver, or turn on the containerd image store, and try again.
Learn more at https://docs.docker.com/go/build-multi-platform/
BLUF/tl;dr: methods of requirements generation are described in this post: "one-shot think hard and brainstorm", learn from others, iterative adversarial feedback, and formal models.
As a solo hobby developer I get to do what feels right with regard to my project. That autonomy and freedom applies to both prioritization and scope. Part of the joy of working is based on being able to follow my curiosity, and to do so at a time and rate of my choosing.
When I work with another person on a shared goal, then there is value in identifying how to divide tasks. For example, by skill, by interests, by availability.
A vision distills into use cases, and these use cases determine requirements which determine tasks. Then sequencing and parallelization of tasks can happen. Let's refer to this as the "think hard and brainstorm" waterfall method. The success of waterfall relies on the ability of planners to identify all contingencies before taking action. Use of an LLM for generating requirements fits in this category as an alternative to thinking hard.
If someone else has a similar situation, learning from their requirements is a valid way of making progress. Plagiarism is acceptable; no need for being original.
The optimistic waterfall method described above assumes the alignment of incentives for participants doing the tasks. If the participants doing tasks are looking for the easiest solution to the requirement they may provided results that don't satisfy the vision.
If the folks satisfying a requirement may be adversarial, that can be accounted for in an iterative manner.
think hard and brainstorm to come up with an initial draft of requirements
provide the draft requirements to adversarial works with the instructions, "provide a solution in a day." Leverage their creativity to provide an insufficient result.
Determine why the adversarial solutions (which do meet the requirements) don't satisfy the vision. Use that insight to develop better requirements.
Repeat the iterations until requirements are "fool proof" for the sample pool of fools.
A third method of coming up with requirements is to use formal methods. For example,
"Program Derivation is the practice of beginning with a specification of a function, and by a series of mechanical steps, deriving an efficient implementation." (source: https://www.youtube.com/watch?v=JDqD6RZpnZA)
And here's an implementation in Python that I think provides that capability:
When I run the above script I get
$ python3 add_one.py 4
4 plus one is 5
$ python3 add_one.py 4.2
4.2 plus one is 5.2
$ python3 add_one.py cat
Error: 'cat' is not a valid float.
Usage: python add_one.py <number>
Next I'm going to intentionally add a few bugs and then ask how to prove the implementation has no bugs:
I've added three bugs in v2: a deterministic bug, a random bug, and bug that depends on the user's environment. A brute force test would be expensive but could identify the first two bugs.
There are a couple problems with v1 of the program requirements to "add one to a user-provided value."
The input range (unstated) is negative infinity to positive infinity.
Python does not have a built-in limit for the size of integers. The maximum integer value is restricted only by the available memory of the system.
Time-out conditions are unspecified. So if the program doesn't respond for 5 minutes, the requirements have nothing to say about that.
Rewrite the program requirements to be more specific:
If the "computer" doing this calculation has a 1Hz CPU clock frequency with 1byte of RAM, that might result in the Python program being "right" but the hardware being inadequate.
Also, let's make explicit the assumption that we are operating in base 10 numbers.
To be safe with the input string, let's bound that to be less than 1000 characters.
The revised implementation is
Normal testing involves evaluating pre-determined cases, like "input 5, get 6" and "input 5.4, get 6.4" and "input 'cat', get error" and "input (nothing), get error."
Property-based testing (e.g., https://hypothesis.readthedocs.io/en/latest/) is where you "write tests which should pass for all inputs in whatever range you describe, and let Hypothesis randomly choose which of those inputs to check - including edge cases you might not have thought about."
https://github.com/pschanely/CrossHair: a static verification tool for Python using symbolic execution. "Repeatedly calls your functions with symbolic inputs. It uses an SMT solver (a kind of theorem prover) to explore viable execution paths and find counterexamples for you."
https://github.com/formal-land/coq-of-python - Translate Python code to Coq code for formal verification. "formal-land" is a commercial company selling verification-as-a-service:
Design by Contract (https://en.wikipedia.org/wiki/Design_by_contract) approaches for Python include Dafny, Deal, and icontract. For Dafny you write the program in Dafny and compile to Python. For Deal you write Python and provide decorators.
"Dafny lifts the burden of writing bug-free code into that of writing bug-free annotations." Dafny was created by Rustan Leino at Microsoft Research. Dafny uses the Z3 automated theorem prover and Boogie.
Boogie is a simple programming language that is meant to be
an easy compile target (think "like JVM bytecode, but for proving code correct")
easy to analyze soundly
not actually intended to be executable
Instead of running Boogie programs, the Boogie compiler looks through the Boogie code to find assertions. For each assertion, the compiler generates a "verification condition", which is a formula based on a (symbolic) analysis of the program; the verification condition formula is constructed so that if the verification condition is true, the assertion holds.
It then hands those verification conditions, along with annotations in the program like assumptions, preconditions, postconditions, and loop invariants, to an SMT solver (Boogie uses Z3 by default). The SMT solver determines whether or not the assumptions definitely ensure the verification condition holds; Boogie complains about the assertions whose verification-conditions haven't been shown to hold.
Prototypes are impactful and efficient when they feature only the essential features. The consequence of that claim is that the prototype is janky (not easy to use), fragile (not robust), shared prematurely (not "professional" looking). For software, a prototype might act on fake data and produce incorrect results.
After stakeholders provide feedback, then the RoI has been confirmed and the direction for where to invest more effort is clarified -- what else is essential? Correctness is typically of interest, but that competes with ease-of-use, speed, and robustness.