disaster-fund-calculation-engine
Turn a validated disaster plan plus beneficiary counts into exact rupee figures: what a household receives, what each government owes, and what each district collects and forwards.
It is the calculation half of the system. The plan itself (structure, grant rates, liability split) comes from disaster-fund-plan, which is installed automatically.
- Python 3.10 or newer
- Dependencies:
disaster-fund-plan>=0.1.1,pydantic>=2 - Import name:
fm_calculation_engine(the PyPI name isdisaster-fund-calculation-engine)
pip install disaster-fund-calculation-engine
The three outputs at a glance
| Question you are asking | Function | Who asks it |
|---|---|---|
| "What does one household receive, tranche by tranche?" | receivable_amounts(plan, profile) |
Caseworker, beneficiary-facing app |
| "How much does the Federal / a Province / a Local government owe, after donations?" | calculate_government_liability(plan, profile, counts) |
Finance office of that government |
| "How much must a district collect from each level, and how much does it forward to each local government?" | calculate_district_fund(plan, profile, counts) |
District coordination office |
All three read the same internal schedule, so they can never disagree with each other (see Guarantees).
Inputs
1. The plan: loaded with disaster-fund-plan:
from fm_plan import load_plan
plan = load_plan("example_plan.json")
2. Beneficiary counts (outputs 2 and 3): how many households fall in each category. A list of dictionaries:
counts = [
{"district": "Gorkha", "local_level": "Gorkha Municipality",
"disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10},
{"district": "Sindhupalchok", "local_level": "Melamchi Municipality",
"disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 20},
{"district": "Sindhupalchok", "local_level": "Helambu Rural Municipality",
"disaster_type": "Earthquake", "solution_type": "Retrofitting", "count": 5},
]
You can also use codes instead of names — the engine accepts either:
counts = [
{"district": "36", "local_level": "36001", "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10},
{"district": "23", "local_level": "23001", "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 20},
]
| Field | Required | Notes |
|---|---|---|
district |
yes | Name or code ("Sindhupalchok" or "23") |
disaster_type, solution_type |
yes | Must exist in the plan; capital letters do not matter |
count |
yes | Number of households, 0 or more |
local_level |
no | Name or code. If omitted, see use case 6 |
region |
no | Taken from the plan's district. If you supply it, it must match the plan |
district_code |
no | Optional cross-check (non-empty string). If given, must match the plan's code for that district |
local_level_code |
no | Optional cross-check (non-empty string). If given, must match the plan's code for that local level (requires local_level to be set) |
3. A profile for the question you are asking (shown in each use case below). Every profile carries the plan_id, which must match the loaded plan: this stops results being calculated against the wrong plan version.
Use cases
1. What does one household receive?
Given a household's region, disaster type and solution type, get its tranche schedule.
from fm_calculation_engine import receivable_amounts
out = receivable_amounts(plan, {
"plan_id": plan.plan_id,
"region": "Pahad",
"disaster_type": "Earthquake",
"solution_type": "Reconstruction",
})
print(out.total) # 300000
for t in out.tranches:
print(t.tranche, t.stage, t.amount)
# 1 shelter 15000
# 2 shelter 15000
# 3 main 100000
# 4 main 100000
# 5 main 70000
2. What does a government owe? (Federal, Province or Local)
The same function works for every level. The engine works out which level you mean from the name or code.
from fm_calculation_engine import calculate_government_liability
fed = calculate_government_liability(
plan,
{"plan_id": plan.plan_id, "gov_entity_name": "Government of Nepal"}, # or "FED"
counts,
)
print(fed.gov_type, fed.liability_percentage) # federal 50.0
print(fed.total_gross_liability) # 4750000
print(fed.total_net_payable) # 4750000 (no donations given)
Other levels, using the plan from the example file:
gov_entity_name |
Resolves to | Pays |
|---|---|---|
"Government of Nepal" or "FED" |
federal | 50% of every district's cost |
"Gandaki Province" or "P4" |
province | 30%, only for districts inside that province (Gorkha) |
"Melamchi Municipality" or "23001" |
local | 20% of its district's cost, split between the district's local levels in proportion to what each needs |
Each result has a per-tranche, per-district breakdown (fed.tranches[i].districts) and a summary of who the beneficiaries are (fed.beneficiary_summary).
3. Apply donations, with automatic rollover
INGO, NGO and other donors give money to a government. It reduces that government's cash outflow, starting at tranche 1; whatever is not used rolls over to the next tranche.
fed = calculate_government_liability(
plan,
{
"plan_id": plan.plan_id,
"gov_entity_name": "Government of Nepal",
"donations": [
{"donor_name": "Red Cross", "amount": 250000, "received_date": "2015-10-01"},
{"donor_name": "UNICEF", "amount": 150000, "received_date": "2015-10-05"},
],
},
counts,
)
for t in fed.tranches[:3]:
print(t.tranche, t.gross_liability, t.donation_applied, t.net_payable, t.remaining_donation)
tranche gross donation applied net payable donation left
1 350,000 350,000 0 50,000 ← 400,000 donated, tranche 1 consumes 350,000
2 350,000 50,000 300,000 0 ← the 50,000 left over rolls into tranche 2
3 1,500,000 0 1,500,000 0 ← donations are used up
Totals: Rs. 400,000 received, Rs. 4,750,000 gross, Rs. 4,350,000 net cash payable. The relief inside each tranche is shared across districts in proportion to what each district needs (tranches[i].districts[j].donation_offset).
Notes: received_date is optional and only affects the order donations are listed (oldest first). It does not decide which tranche a donation can pay for, because the plan has no tranche due dates.
4. What must a district collect, and whom does it forward to?
A district office needs two answers per tranche: how much to collect from Federal, Province and Local, and how much to forward to each local government.
from fm_calculation_engine import calculate_district_fund
d = calculate_district_fund(
plan,
{"plan_id": plan.plan_id, "district_name": "Sindhupalchok"}, # or "23"
counts,
)
print(d.province_name, d.total_beneficiaries, d.grand_total) # Bagmati Province 25 6500000
t1 = d.tranches[0]
print(t1.collectible) # federal=275000 province=165000 local=110000 total=550000
for x in t1.disbursements:
print(x.local_gov, x.amount)
# Melamchi Municipality 300000
# Helambu Rural Municipality 250000
Tranche 1 here is 550,000 in total: Rs. 275,000 (50%) from Federal, Rs. 165,000 (30%) from the Province and Rs. 110,000 (20%) from Local, which is then forwarded as 300,000 and 250,000 to the two local governments according to their own beneficiaries.
5. Reconcile everything (audit and sanity check)
Because all outputs come from one schedule, they must add up. This is a useful check for auditors and for your own integration tests:
levels = ["FED", "P3", "P4", "Gorkha Municipality", "Melamchi Municipality", "Helambu Rural Municipality"]
total_by_government = sum(
calculate_government_liability(plan, {"plan_id": plan.plan_id, "gov_entity_name": g}, counts).total_gross_liability
for g in levels
)
total_by_district = sum(
calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": d}, counts).grand_total
for d in ("Gorkha", "Sindhupalchok")
)
assert total_by_government == total_by_district == 9_500_000
6. Counts without a local level
Sometimes only district-level totals are known. Leave local_level out:
calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": "23"},
[{"district": "23", "disaster_type": "Earthquake", "solution_type": "Reconstruction", "count": 10}])
# tranche 1 -> Melamchi Municipality 75000, Helambu Rural Municipality 75000
The money for such households is shared equally between the district's local levels (odd rupees go to the first ones in the plan). For a local government's own report, these households are shown separately as unassigned_beneficiaries and are not included in total_beneficiaries.
7. Catch bad input with readable errors
Every problem in the input is reported at once, in plain words, as an EngineInputError:
from fm_calculation_engine import EngineInputError
try:
calculate_district_fund(plan, {"plan_id": plan.plan_id, "district_name": "Gorkha"}, bad_counts)
except EngineInputError as error:
print(error)
3 problem(s) in beneficiary counts:
- item 0: district 'Kathmandu' is not in plan's districts ['Gorkha', 'Sindhupalchok']
- item 0: disaster_type 'Flood' is not in plan's disaster types ['Earthquake']
- item 1: region 'Terai' does not match district 'Gorkha', which is in region 'Pahad' in this plan
You can also validate a count list on its own, before calculating, and get it back normalised to the plan's spelling and codes:
from fm_calculation_engine import validate_beneficiary_counts
clean = validate_beneficiary_counts(plan, counts)
8. A local government whose name exists in two districts
Nepal has repeated municipality names. If a local government's name appears in more than one district, the engine refuses to guess; say which district you mean:
calculate_government_liability(plan, {
"plan_id": plan.plan_id,
"gov_entity_name": "Some Municipality",
"district": "Sindhupalchok",
}, counts)
(district is only accepted for local governments. Using the official code, such as "23001", avoids the ambiguity altogether.)
Guarantees
- Exact rupees, no drift. Every split (liability shares, local-level shares, donation offsets) uses the largest remainder method on whole rupees: each part gets what it is owed, and leftover rupees go to the parts with the largest fractions. The parts always add up to the total;
round()and floating point are never used for money. - One schedule per district. District total = Federal + Province + Local share, and the local share = the sum of the shares given to its local levels, exactly. Outputs 2 and 3 therefore always reconcile.
- Deterministic. The same input always gives the same output (ties are broken by plan order).
- Wrong plan is rejected. A
plan_idthat does not match the loaded plan is an error. - Names or codes everywhere. Districts, provinces, local levels and the Federal government can all be given by name or official code, in any capitalisation, in beneficiary counts and profiles. Only the plan file itself requires codes in boundary-link fields.
API summary
| Name | Purpose |
|---|---|
receivable_amounts(plan, profile) → ReceivableAmounts |
Output 1: tranche schedule for one household |
calculate_government_liability(plan, profile, counts) → GovernmentLiabilityOutput |
Output 2: what a government owes, with donation offsets and rollover |
calculate_district_fund(plan, profile, counts) → DistrictOutput |
Output 3: what a district collects and forwards |
validate_beneficiary_counts(plan, counts) |
Check counts against the plan; returns normalised BeneficiaryCount objects |
EngineInputError |
Raised for any invalid input; the message lists every problem |
BeneficiaryProfile, GovEntityProfile, DistrictProfile, BeneficiaryCount, Donation |
Input models (a plain dict with the same keys works too) |
Profiles and counts can be passed as dictionaries or as these model objects. Unknown keys are rejected, so a typo is an error rather than being silently ignored.
Limits
- Amounts are whole rupees.
- The Federal/Province/Local split is the same for every tranche and every stage (shelter and main).
- Donations are pooled per government and used from tranche 1 onward; they are not tied to a particular tranche or district.
- A tranche round exists up to the longest grant in the plan. A grant with fewer tranches simply has Rs. 0 in the later rounds.
License
MIT
Metadata
Release files for disaster-fund-calculation-engine 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| disaster_fund_calculation_engine-0.1.1.tar.gz | 16.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| disaster_fund_calculation_engine-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 33.6 kB
Release files / disaster_fund_calculation_engine-0.1.1.tar.gz
| Download URL | disaster_fund_calculation_engine-0.1.1.tar.gz |
|---|---|
| Size | 16.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
11f2e179d592c5353df19a641c53e96deb7e7320c40f038e9e09e325ddd214bd
|
|
BLAKE2b-256 checksum How to use checksums |
fcda9cfabac38b4757df50fd07b01c3acdd1cf9d38bf0740e868654c82b0f1f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|
Release files / disaster_fund_calculation_engine-0.1.1-py3-none-any.whl
| Download URL | disaster_fund_calculation_engine-0.1.1-py3-none-any.whl |
|---|---|
| Size | 17.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bb9dd9398f156b104b6d54ec55af65ea6c9e9b64f2322480130b0dd2d3f8cdc7
|
|
BLAKE2b-256 checksum How to use checksums |
b57534cb63768761d2992fee08c1c8ca1d4164f12281960dca96acc7eb827208
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.6
|