Skip to content

Commit f95075e

Browse files
gnshbclaude
andcommitted
add polyprotic acid/base support
New define_polyprotic_particle function with explicit n and pka_list parameters. Existing define_particle remains untouched for monoprotic. Includes polyprotic states, reactions, updated setup_cpH/grxmc filters, get_pka_set, load_pka_set, tests (15 new), tutorial section, and changelog. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3147a11 commit f95075e

5 files changed

Lines changed: 830 additions & 2480 deletions

File tree

polyprotic_changelog.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Polyprotic Acid/Base Support — Change Log
2+
3+
## Stage 1: Storage Layer
4+
**Status:** No changes needed.
5+
6+
`define_particle_states` already accepts arbitrary lists of `{"name", "z"}` dicts.
7+
`Reaction` and `ReactionParticipant` are fully generic. The database can hold N states and N-1 reactions as-is.
8+
9+
---
10+
11+
## Stage 2: `define_polyprotic_particle_states`
12+
**Status:** Done.
13+
**File:** `pyMBE/pyMBE.py` (inserted after `define_monoprototic_particle_states`)
14+
15+
New method generates N+1 states with auto-naming:
16+
- Acidic n=3, particle "A": `H3A` (z=0), `H2A` (z=-1), `HA` (z=-2), `A` (z=-3)
17+
- Basic n=2, particle "B": `H2B` (z=+2), `HB` (z=+1), `B` (z=0)
18+
19+
Returns list of state names for use in reaction definition.
20+
21+
```diff
22+
+ def define_polyprotic_particle_states(self, particle_name, n, acidity):
23+
+ ...
24+
+ states = []
25+
+ for k in range(n, -1, -1):
26+
+ if k == 0:
27+
+ name = particle_name
28+
+ elif k == 1:
29+
+ name = f"H{particle_name}"
30+
+ else:
31+
+ name = f"H{k}{particle_name}"
32+
+ if acidity == "acidic":
33+
+ z = -(n - k)
34+
+ else:
35+
+ z = k
36+
+ states.append({"name": name, "z": z})
37+
+ self.define_particle_states(particle_name=particle_name, states=states)
38+
+ return [s["name"] for s in states]
39+
```
40+
41+
---
42+
43+
## Stage 3: `define_polyprotic_acidbase_reactions`
44+
**Status:** Done.
45+
**File:** `pyMBE/pyMBE.py` (inserted after `define_monoprototic_acidbase_reaction`)
46+
47+
Creates N stepwise reactions from N+1 ordered states. Each reaction pairs adjacent states
48+
(e.g. `H3A → H2A`, `H2A → HA`, `HA → A`) with its corresponding pKa. Uses
49+
`reaction_type="polyprotic_acid"` or `"polyprotic_base"`.
50+
51+
Takes `state_names` (from Stage 2) and `pka_list` as input, validates
52+
`len(pka_list) == len(state_names) - 1`.
53+
54+
```diff
55+
+ def define_polyprotic_acidbase_reactions(self, particle_name, state_names, pka_list, acidity, metadata=None):
56+
+ ...
57+
+ for i, pka in enumerate(pka_list):
58+
+ reactant_state = state_names[i]
59+
+ product_state = state_names[i + 1]
60+
+ reaction = Reaction(participants=[
61+
+ ReactionParticipant(..., state_name=reactant_state, coefficient=-1),
62+
+ ReactionParticipant(..., state_name=product_state, coefficient=1)],
63+
+ reaction_type=reaction_type, pK=pka, metadata=metadata)
64+
+ self.db._register_reaction(reaction)
65+
```
66+
67+
---
68+
69+
## Stage 4: New `define_polyprotic_particle`
70+
**Status:** Done.
71+
**File:** `pyMBE/pyMBE.py` (new method, inserted after `define_particle`)
72+
73+
Separate function with explicit `n` and `pka_list` parameters.
74+
`define_particle` is **untouched** — monoprotic behavior fully preserved.
75+
76+
```diff
77+
+ def define_polyprotic_particle(self, name, sigma, epsilon, n, acidity, pka_list, cutoff=pd.NA, offset=pd.NA):
78+
+ if len(pka_list) != n:
79+
+ raise ValueError(...)
80+
+ state_names = self.define_polyprotic_particle_states(particle_name=name, n=n, acidity=acidity)
81+
+ initial_state = state_names[0]
82+
+ self.define_polyprotic_acidbase_reactions(particle_name=name, state_names=state_names,
83+
+ pka_list=pka_list, acidity=acidity)
84+
+ tpl = ParticleTemplate(name=name, sigma=..., epsilon=..., cutoff=..., offset=...,
85+
+ initial_state=initial_state)
86+
+ self.db._register_template(tpl)
87+
```
88+
89+
---
90+
91+
## Stage 5: Extend `setup_cpH` filter
92+
**Status:** Done.
93+
**File:** `pyMBE/pyMBE.py` line ~2701
94+
95+
```diff
96+
+ acidbase_reaction_types = ["monoprotic_acid", "monoprotic_base",
97+
+ "polyprotic_acid", "polyprotic_base"]
98+
for reaction in self.db.get_reactions():
99+
- if reaction.reaction_type not in ["monoprotic_acid", "monoprotic_base"]:
100+
+ if reaction.reaction_type not in acidbase_reaction_types:
101+
```
102+
103+
---
104+
105+
## Stage 6: Extend `setup_grxmc_reactions` and `setup_grxmc_unified` filters
106+
**Status:** Done.
107+
**File:** `pyMBE/pyMBE.py` lines ~3017, ~3208
108+
109+
Same change as Stage 5 applied to both methods.
110+
111+
---
112+
113+
## Stage 7: Rework `get_pka_set`
114+
**Status:** Done.
115+
**File:** `pyMBE/pyMBE.py` (modified `get_pka_set`)
116+
117+
- Now returns `"pka_values": [list]` for polyprotic (vs `"pka_value": float` for monoprotic)
118+
- Polyprotic particles accumulate pKa values across multiple reactions
119+
- Monoprotic still raises on duplicates
120+
121+
```diff
122+
+ if reaction.reaction_type in ["monoprotic_acid", "monoprotic_base"]:
123+
+ # single pka_value (unchanged behavior)
124+
+ else:
125+
+ # accumulate into pka_values list
126+
```
127+
128+
---
129+
130+
## Stage 8: Extend `load_pka_set` and `_check_pka_set`
131+
**Status:** Done.
132+
**File:** `pyMBE/pyMBE.py`
133+
134+
`_check_pka_set` now accepts either `"pka_value"` (float) or `"pka_values"` (list), rejects both/neither.
135+
136+
`load_pka_set` routes by key:
137+
- `"pka_values"` present → calls `define_polyprotic_particle_states` + `define_polyprotic_acidbase_reactions`
138+
- `"pka_value"` present → calls `define_monoprototic_acidbase_reaction` (unchanged)
139+
140+
New JSON format for polyprotic:
141+
```json
142+
{"data": {"PO4": {"acidity": "acidic", "pka_values": [2.15, 7.20, 12.35]}}}
143+
```
144+
145+
---
146+
147+
## Tests
148+
**File:** `testsuite/polyprotic_acidbase_tests.py` (new, 15 tests)
149+
150+
Covers: triprotic acid states/reactions/initial_state, diprotic base, monoprotic backward compat,
151+
reaction participant pairing, auto-naming convention, get_pka_set (polyprotic/mixed),
152+
load_pka_set (polyprotic/mixed), _check_pka_set validation, error handling.
153+
154+
All polyprotic tests use `define_polyprotic_particle(n=..., pka_list=[...])`.
155+
Monoprotic backward compat test uses unchanged `define_particle(pka=float)`.
156+
157+
All 24 tests pass (15 new + 9 existing acidity).
158+
159+
---
160+
161+
## Tutorial
162+
**File:** `tutorials/pyMBE_tutorial.ipynb`
163+
164+
Added new section "How to define polyprotic particles" between the polyampholyte exercise
165+
and the peptides section. Contains:
166+
- Explanation of polyprotic vs monoprotic
167+
- Triprotic phosphoric acid example using `define_polyprotic_particle`
168+
- Side-by-side comparison with monoprotic `define_particle`
169+
- Auto-naming convention table

0 commit comments

Comments
 (0)