Rasad · رَصَد
Measure how much of your simulation's result is a property of the model, and how much is the random seed.
Stochastic simulations are usually reported without error bars. A paper says "the civilisation collapsed in year 240" and never answers two questions:
- If the random seed changes, is it still year 240?
- Is this a property of the model, or an accident of one run?
Rasad answers both by measurement.
Why this exists
This tool was not designed in the abstract. It was built because another project needed it.
Omran is a simulation of Ibn Khaldun's theory of asabiyyah. Ibn Khaldun (1332–1406) was a historian who argued in the Muqaddimah that civilisations rise and fall on a single force: asabiyyah, the cohesion that lets a group act as one. A group with strong cohesion overtakes a settled, comfortable one; then comfort erodes its own cohesion over generations, and it is overtaken in turn. The cycle repeats. He called the study of this ʿilm al-ʿumran — the science of human social organisation — which is where the project takes its name.
Omran models that computationally: nations on a grid, populations that grow and starve, ideas that spread between neighbours like an infection, wars along contested borders. Run it and you get numbers — a final population, a count of wars, a year the collapse happened.
The problem was that nobody knew whether those numbers meant anything.
Change the random seed and they change. Was a collapse in year 240 a property of the model, or an accident of one run? There was no way to answer without measuring, and no tool that measured it — the frameworks that do sensitivity analysis are built for engineering and physics models, and the agent-based modelling field is repeatedly criticised for publishing results without error bars.
So Rasad was written to answer that question, under one hard constraint: it must not touch Omran. It observes from the outside and modifies nothing, the way an instrument measures a specimen without altering it. That constraint shaped the architecture — and it is why Rasad works on any simulation, not just the one it was written for.
What it found is in FINDINGS.md, and it was not what anyone expected.
Install
pip install rasad-sim
The distribution is rasad-sim because rasad collides with an existing name on
PyPI. The import is unaffected:
import rasad
Python 3.12+ · numpy · plotly
Use
Describe your simulation as one function:
def run(params: dict, seed: int) -> dict:
"""One run. A number is a final result; a list of numbers is a time series."""
...
Then:
import rasad
report = rasad.measure(run, params={"growth": 0.03}, runs=100)
print(report.summary())
report.plot().write_html("divergence.html")
You get, for every output: mean, standard deviation, a 90% interval, and a variability class — low, moderate, or high — plus a curve showing how far the runs drift apart over time.
What it found in practice
A simulation that could not reproduce itself
Applied to Omran, an agent-based model of Ibn Khaldun's theory of asabiyyah, without modifying a line of it:
final_total_population: mean 4,649.39 | cv 0.2841 | p05-p95 [2,704.75, 6,774.30] | high
survivors: mean 1.14 | cv 0.3059 | p05-p95 [1.00, 2.00] | high
total_wars: mean 13.44 | cv 0.3986 | p05-p95 [6.00, 22.10] | high
total_famines: mean 0.00 | cv 0.0000 | p05-p95 [0.00, 0.00] | low
No numeric output of the model had low variability. Worse, the measurement exposed something the author did not know: the same seed produced different results in different processes.
Six runs with seed 1, thirty simulated years:
1428 · 1428 · 1512 · 1426 · 1426 · 1512
Comparing the population curve year by year located the split: the runs are identical for nineteen years, then diverge at year twenty by one individual — which becomes hundreds by year one hundred. That is error propagation, measured.
This was then confirmed with Rasad out of the path entirely: Omran's own main.py carries a
hard-coded random.seed(42), and running that file directly eight times gave four distinct
results.
Full write-up: FINDINGS.md
Aggregates can be stable while their parts are noise
On a SimPy machine-shop simulation
(examples/simpy_machine_shop.py):
| Output | cv | Verdict |
|---|---|---|
| total parts produced | 0.014 | low |
| best machine · worst machine | 0.017 | low |
| gap between best and worst | 0.31 | high |
The shop's total output is stable. The gap between machines is pure noise. Anyone looking at one run and saying "machine 7 is underperforming, investigate it" is chasing a random seed.
Averages hide fragility. That alone is a reason to measure what you publish.
Verified against simulations it was not written for
| Framework | Models | Result |
|---|---|---|
| Mesa | Schelling, WolfSheep, Boltzmann | works; WolfSheep's sheep population has high variability (cv 2.95 — usually extinct, occasionally not) |
| SimPy | machine shop | works; see above |
| EoN | SIR on a network | works; epidemic duration has high variability (7.5 → 14.7) |
| Omran | asabiyyah model | works; see above |
Examples: examples/
A control worth stating: the SimPy and Mesa examples reproduce byte-identically across separate processes. That establishes that Omran's non-reproducibility is a bug in Omran, and that Rasad's own pipeline is deterministic.
The class is a convention. The numbers are the result.
The default cutoffs — 0.05 and 0.20 — are a choice, not a theory. An output at cv 0.21 reads high; raise the cutoff to 0.25 and the same data reads moderate.
So every report prints the thresholds it used and labels them as a convention:
thresholds (a convention, not a rule): low < 0.0500 <= moderate <= 0.2000 < high
And they belong to the caller:
rasad.measure(run, params={}, runs=100,
thresholds={"low": 0.01, "moderate": 0.05})
The real result is the interval. p05-p95 [7.46, 14.74] says the duration may double,
without needing a word on top of it.
Limitations
- Only the seed varies. Parameters are held fixed, so "which parameter drives the result?" is not answered yet. Sensitivity analysis is the next version.
- Execution is sequential. No parallelism.
- One value per output name per run. A model whose keys change between runs is rejected.
Accepted outputs: Python numbers, numpy scalars, 1-D numeric numpy arrays, lists and tuples. Booleans are always rejected — alone or inside a list — because an average of ones and zeros means nothing.
Development
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest tests -v
73 tests · 97% line coverage · 357 of 452 mutants killed · linted with ruff
The coverage figure is the weakest of the three. Mutation testing is what showed why:
validate_thresholds was fully covered and still accepted two equal cutoffs, because
no test passed the one input that separates < from <=.
The statistics are tested against models whose answers are known analytically: a constant must give a standard deviation of exactly zero; a random walk's divergence must grow as the square root of time. Those tests caught real bugs — including a collapsed axis that would have produced plausible, meaningless numbers.
Notes on the implementation
Three analyses of the code, in Arabic:
- فئات المدخلات — the five input classes
summarize()actually distinguishes, each with the test that covers it, and why branch order decides which class a list falls into. - كم تشغيلة تكفي؟ — the standard error of the mean applied to Omran: 100 runs pin the mean to 2.8%, and reaching 1% costs 784.
- حدود الوحدات — the deletion test applied to each of Rasad's own modules, separating splits forced by something that happened from splits made on a guess.
بالعربية
رَصَد أداة تقيس صلابة نتائج المحاكاة: أيُّ المخرجات خاصيةٌ في النموذج، وأيُّها أثرٌ للبذرة العشوائية.
تُنشر نتائج المحاكاة العشوائية غالبًا بلا حدود خطأ. يُقال «انهارت الحضارة في السنة ٢٤٠» دون الإجابة على سؤالين: هل يظل الرقم ٢٤٠ لو تغيّرت البذرة؟ وهل هذه خاصية في النموذج أم صدفة في تشغيلة واحدة؟ يجيب رَصَد عنهما بالقياس لا بالتقدير.
يوصّف المستخدم محاكاته بدالةٍ واحدة تستقبل المعاملات والبذرة وترجّع قاموس مخرجات. يشغّلها رَصَد مرارًا ببذورٍ مختلفة، ثم يعرض لكل مخرَج متوسطه وانحرافه ومدى تسعين بالمئة وتصنيفًا لتغايره — low أو moderate أو high — مع منحنى يبيّن اتساع التباعد بين التشغيلات عبر الزمن.
طُبِّق على أربعة مشاريع لم يُكتب لأجلها، فكشف في أحدها — محاكاة لنظرية العصبية عند ابن خلدون — أنها لا تعيد إنتاج نتائجها بالبذرة نفسها: تشغيلتان متطابقتان تفترقان عند السنة العشرين بفارق فردٍ واحد، يصير مئاتٍ بحلول السنة المئة. وهذا انتشار الخطأ في صورته المقيسة.
لماذا كُتب
لم تُصمَّم هذه الأداة في الفراغ، بل كُتبت لأن مشروعًا آخر احتاجها.
عُمران محاكاةٌ لنظرية العصبية عند ابن خلدون (١٣٣٢–١٤٠٦)، الذي رأى في المقدمة أن الحضارات تنهض وتسقط بقوةٍ واحدة: العصبية، أي التماسك الذي يجعل الجماعة تفعل كأنها واحد. جماعةٌ عصبيتها قوية تغلب جماعةً مستقرةً مترفة، ثم يُفسد الترف عصبيتها هي عبر الأجيال، فتُغلَب بدورها. وسمّى ابن خلدون دراسة ذلك علم العمران، ومنه أخذ المشروع اسمه.
يحاكي عُمران هذا حاسوبيًّا: دولٌ على شبكة، وسكانٌ ينمون ويجوعون، وأفكارٌ تنتقل بين الجيران كالعدوى، وحروبٌ على الحدود المتنازعة. تشغّله فتخرج لك أرقام — سكانٌ في النهاية، وعدد حروب، وسنةٌ وقع فيها الانهيار.
والمشكلة أن أحدًا لم يكن يعرف: هل لهذه الأرقام معنى؟ غيّر البذرة العشوائية تتغيّر. فهل الانهيار في السنة ٢٤٠ خاصيةٌ في النموذج أم صدفةُ تشغيلةٍ واحدة؟ لا سبيل إلى الجواب إلا بالقياس، ولم تكن هناك أداةٌ تقيسه.
فكُتب رَصَد لهذا السؤال، بقيدٍ واحدٍ صارم: ألّا يمسّ عُمران. يراقبه من خارجه ولا يعدّل فيه حرفًا، كما يقيس المِجهر عيّنةً دون أن يغيّرها. وهذا القيد هو الذي شكّل بنية الأداة — وهو سبب عملها على أي محاكاة، لا على التي كُتبت لأجلها وحدها.
التصنيف
وحدود التصنيف الافتراضية اصطلاحٌ لا قاعدة، ولذلك يعلنها كل تقرير ويتركها بيد المستخدم. النتيجة الحقيقية هي المدى، لا التصنيف الذي يعلوه.
How this was built — full disclosure
Most of the code here was written by AI models under explicit delegation and human review.
| Stage | Owner |
|---|---|
| Specification and architecture | Ahmed, in dialogue with Claude (Opus 5) |
| Test authoring | Written into each task brief before implementation; the implementer was forbidden from altering a character |
| Implementation | DeepSeek V4 Flash, via opencode + AgentRouter — seven tasks |
| Review gates | Claude (Opus 5): every diff read, tests run independently of the implementer's claim |
| Decisions and merges | Ahmed — every commit after review |
The ordering is what matters, not the tooling: the tests came first and were the specification. The model was never asked to write code and then write the thing that proves it correct. It was given a written contract and held to it.
What the gates actually caught: a dead condition in a type check, a deprecated import, a missing return annotation, unreadable number formatting. No logic error got through — credit to the tests, not to the model.
And what none of them caught: an output that was constantly zero was classified as high variability when it was the most stable number in the report. Neither the reference models nor the review found it — the real data did, on the first run against Omran.
That is the boundary. Tests prove the arithmetic is right; only real data reveals the case nobody thought to write a test for.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file rasad_sim-0.1.0.tar.gz.
File metadata
- Download URL: rasad_sim-0.1.0.tar.gz
- Upload date:
- Size: 27.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0c0deb8e7785a65820e3e4033c857b2fcc5d62c16633300a479d6703a43c91eb
|
|
| MD5 |
1e6af9782bd790076501e751b55c9c81
|
|
| BLAKE2b-256 |
0afd80aeb6c2b6e372bc1645d94c83293f303c1f7609f3ad3cd7bfcf2a833965
|
Provenance
The following attestation bundles were made for rasad_sim-0.1.0.tar.gz:
Publisher:
publish.yml on ahmedaly0904-bit64/rasad
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rasad_sim-0.1.0.tar.gz -
Subject digest:
0c0deb8e7785a65820e3e4033c857b2fcc5d62c16633300a479d6703a43c91eb - Sigstore transparency entry: 2753434900
- Sigstore integration time:
-
Permalink:
ahmedaly0904-bit64/rasad@0a6f6c61fab580743d9a4ed2c2d07a3f595f2395 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ahmedaly0904-bit64
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0a6f6c61fab580743d9a4ed2c2d07a3f595f2395 -
Trigger Event:
release
-
Statement type:
File details
Details for the file rasad_sim-0.1.0-py3-none-any.whl.
File metadata
- Download URL: rasad_sim-0.1.0-py3-none-any.whl
- Upload date:
- Size: 19.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7f7c67045475ee78bc38333f1f5607cf1a4ee121edfb689b6d7d6287dbac4879
|
|
| MD5 |
e9076c99ab5bf74607a33b81eafb1a5a
|
|
| BLAKE2b-256 |
526c855b47e1ab7d4958795165bd7dd1985200e948428332468fb3f179499cc9
|
Provenance
The following attestation bundles were made for rasad_sim-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on ahmedaly0904-bit64/rasad
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rasad_sim-0.1.0-py3-none-any.whl -
Subject digest:
7f7c67045475ee78bc38333f1f5607cf1a4ee121edfb689b6d7d6287dbac4879 - Sigstore transparency entry: 2753434937
- Sigstore integration time:
-
Permalink:
ahmedaly0904-bit64/rasad@0a6f6c61fab580743d9a4ed2c2d07a3f595f2395 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ahmedaly0904-bit64
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@0a6f6c61fab580743d9a4ed2c2d07a3f595f2395 -
Trigger Event:
release
-
Statement type: