researchium 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.12", "3.14"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: uv sync --extra dev
20
+ - run: uv run pytest
21
+ - run: uv run ruff check .
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .eggs/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ .mypy_cache/
13
+ *.log
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Samarth Javagal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,475 @@
1
+ Metadata-Version: 2.5
2
+ Name: researchium
3
+ Version: 0.1.0
4
+ Summary: A research module for organizing and analyzing scientific work.
5
+ Project-URL: Homepage, https://github.com/samTheComputerArchitect/researchium
6
+ Project-URL: Repository, https://github.com/samTheComputerArchitect/researchium
7
+ Author: Samarth Javagal
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: analysis,experiments,research,science
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering
23
+ Requires-Python: >=3.10
24
+ Provides-Extra: dev
25
+ Requires-Dist: build==1.6.1; extra == 'dev'
26
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Requires-Dist: ruff>=0.6; extra == 'dev'
29
+ Requires-Dist: twine==7.0.0; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Researchium
33
+
34
+ [![PyPI - Version](https://img.shields.io/pypi/v/researchium.svg)](https://pypi.org/project/researchium) [![PyPI - Python Versions](https://img.shields.io/pypi/pyversions/researchium.svg)](https://pypi.org/project/researchium) [![PyPI - License](https://img.shields.io/pypi/l/researchium.svg)](LICENSE) [![Build Status](https://github.com/samTheComputerArchitect/researchium/actions/workflows/ci.yml/badge.svg)](https://github.com/samTheComputerArchitect/researchium/actions/workflows/ci.yml) [![PyPI - Downloads](https://img.shields.io/pypi/dm/researchium.svg)](https://pypi.org/project/researchium)
35
+
36
+ **Version 0.1.0**
37
+
38
+ Researchium is a small, opinionated toolkit for organizing scientific work into
39
+ self-contained **environments** and tracking **experiments** within them. It
40
+ gives every experiment a stable ID, a home folder, a runnable script, and a
41
+ place for its configuration and results — all stored as plain, human-readable
42
+ files so nothing is locked in a database.
43
+
44
+ It is built around three ideas:
45
+
46
+ 1. **An environment owns a directory.** `researchium init` lays down a standard
47
+ structure (mirroring what `git` does with `.git` and `uv` with `uv.lock`).
48
+ Every subsequent command discovers the environment by walking up from the
49
+ current directory, so you can work from any subfolder.
50
+ 2. **Experiments are first-class.** Each experiment is a folder in
51
+ `experiments/`, has a unique short ID, and keeps its configuration and
52
+ results in its own location.
53
+ 3. **Run scripts fill config and results.** Write a normal Python script that
54
+ imports `Config` and `Results` from `researchium`. `researchium run` executes
55
+ it and snapshots both registries into Markdown *and* JSON files — ready for
56
+ humans and machines.
57
+
58
+ ---
59
+
60
+ ## Table of contents
61
+
62
+ - [Quick start](#quick-start)
63
+ - [The environment layout](#the-environment-layout)
64
+ - [Command-line usage](#command-line-usage)
65
+ - [Writing run scripts](#writing-run-scripts)
66
+ - [Python API](#python-api)
67
+ - [File formats](#file-formats)
68
+ - [Error handling](#error-handling)
69
+ - [Development](#development)
70
+ - [Project layout](#project-layout)
71
+
72
+ ---
73
+
74
+ ## Quick start
75
+
76
+ ```bash
77
+ researchium init "Climate Genomics" # lay down an environment
78
+ researchium create alpine-transcriptomics \
79
+ "RNA-seq of alpine vs lowland populations." # register an experiment
80
+ ```
81
+
82
+ Write a run script:
83
+
84
+ ```python
85
+ # experiments/alpine-transcriptomics/run.py
86
+ from researchium import Config, Results
87
+
88
+ Config["epochs"] = 10
89
+ Config["learning_rate"] = 0.001
90
+
91
+ Results["mse"] = 0.032
92
+ Results["r2"] = 0.91
93
+ ```
94
+
95
+ Run it and inspect:
96
+
97
+ ```bash
98
+ researchium run alpine-transcriptomics # or by ID: researchium run 1e70f7c3
99
+ researchium config alpine-transcriptomics # epochs: 10
100
+ researchium results alpine-transcriptomics # mse: 0.032
101
+ researchium search alpine # find experiments and their IDs
102
+ ```
103
+
104
+ ---
105
+
106
+ ## The environment layout
107
+
108
+ `researchium init NAME` creates a directory with this structure:
109
+
110
+ ```
111
+ my-study/
112
+ ├── .researchium/ # marker + environment metadata (like .git/)
113
+ │ └── environment.json
114
+ ├── experiments/ # one folder per experiment
115
+ ├── results/ # one folder per experiment, mirroring experiments/
116
+ ├── src/ # reusable code for the project
117
+ ├── experiments.md # human log of every experiment
118
+ └── experiments.json # machine-readable registry of experiments
119
+ ```
120
+
121
+ After registering and running an experiment, an experiment looks like:
122
+
123
+ ```
124
+ experiments/
125
+ └── alpine-transcriptomics/
126
+ ├── run.py # you write this
127
+ ├── config.md # written by `run`
128
+ └── config.json
129
+ results/
130
+ └── alpine-transcriptomics/
131
+ ├── results.md
132
+ └── results.json
133
+ ```
134
+
135
+ Key details:
136
+
137
+ - **The `.researchium/` marker is what makes a directory an environment.**
138
+ `discover_environment()` walks up parent directories to find it, so commands
139
+ work from `results/alpine-transcriptomics/`, `src/`, or anywhere below the
140
+ root.
141
+ - **Result folders are keyed by experiment name** (`results/<name>/`), which
142
+ keeps them readable while `researchium results <name_or_id>` still resolves
143
+ either form.
144
+ - **Results and config are stored as both Markdown (for humans) and JSON (for
145
+ tooling).**
146
+
147
+ ---
148
+
149
+ ## Command-line usage
150
+
151
+ Every command accepts an experiment either by **exact name** or by **ID**.
152
+
153
+ | Command | Description |
154
+ | -------------------------------- | ------------------------------------------------------------------ |
155
+ | `researchium init NAME` | Create an environment named `NAME` in the current directory. |
156
+ | `researchium create NAME DESC` | Register an experiment: folder + log entry + registry record. |
157
+ | `researchium search QUERY` | Find experiments matching name or description (case-insensitive). |
158
+ | `researchium run NAME_OR_ID` | Execute `experiments/<name>/run.py`, snapshot config + results. |
159
+ | `researchium config NAME_OR_ID` | Print the experiment config as `key: value` lines. |
160
+ | `researchium results NAME_OR_ID` | Print the experiment results as `key: value` lines. |
161
+
162
+ ### `init`
163
+
164
+ ```bash
165
+ $ researchium init "Climate Genomics"
166
+ Initialized researchium environment at /path/to/project
167
+ ```
168
+
169
+ Creating an environment where one already exists fails with exit code `1`.
170
+
171
+ ### `create`
172
+
173
+ ```bash
174
+ $ researchium create alpine-transcriptomics "RNA-seq of alpine vs lowland populations."
175
+ Created experiment alpine-transcriptomics (ID 1e70f7c3) at .../experiments/alpine-transcriptomics
176
+ ```
177
+
178
+ `create` refuses empty or path-traversal names (e.g. `../x`) and duplicate
179
+ names. It also creates the experiment's `results/<name>/` folder and appends a
180
+ date/description/ID entry to `experiments.md`.
181
+
182
+ ### `search`
183
+
184
+ ```bash
185
+ $ researchium search alpine
186
+ - alpine-transcriptomics (ID 1e70f7c3): RNA-seq of alpine vs lowland populations.
187
+ ```
188
+
189
+ Matching is case-insensitive over both name and description. No matches prints
190
+ `No experiments matching 'query'.` and still exits `0`.
191
+
192
+ ### `run`
193
+
194
+ ```bash
195
+ $ researchium run alpine-transcriptomics
196
+ Ran experiment alpine-transcriptomics (ID 1e70f7c3)
197
+ config -> experiments/alpine-transcriptomics/config.md, config.json
198
+ results -> results/alpine-transcriptomics/results.md, results.json
199
+ ```
200
+
201
+ `run` clears the previous `Config`/`Results`, executes
202
+ `experiments/<name>/run.py`, then writes four files. It fails with exit code
203
+ `1` if the script is missing or the script raises. See
204
+ [Writing run scripts](#writing-run-scripts).
205
+
206
+ ### `config` and `results`
207
+
208
+ ```bash
209
+ $ researchium config alpine
210
+ epochs: 10
211
+ learning_rate: 0.001
212
+
213
+ $ researchium results alpine
214
+ mse: 0.032
215
+ r2: 0.91
216
+ ```
217
+
218
+ Structured values (lists, dicts) print as compact JSON, e.g.
219
+ `metrics: {"loss": 0.2}`. Before the first run, `config` prints
220
+ `(no configuration yet)`; `results` falls back to a file listing of the
221
+ results folder.
222
+
223
+ ---
224
+
225
+ ## Writing run scripts
226
+
227
+ A run script is any Python file named `run.py` inside an experiment folder. It
228
+ should import `Config`/`Results` and set values by key:
229
+
230
+ ```python
231
+ from researchium import Config, Results
232
+
233
+ import numpy as np
234
+ from sklearn.model_selection import train_test_split
235
+
236
+ Config["epochs"] = 10
237
+ Config["optimizer"] = "adam"
238
+ Config["layers"] = [256, 128, 64]
239
+
240
+ # ... your training code ...
241
+ accuracy = 0.93
242
+ Results["accuracy"] = accuracy
243
+ Results["metrics"] = {"precision": 0.91, "recall": 0.89}
244
+ ```
245
+
246
+ - `Config` and `Results` are plain mutable registries (dict-like). Use any key
247
+ you like; order of the printed/`.md` output follows insertion order.
248
+ - Values that are lists, tuples, or dicts are stored as JSON; any other object
249
+ is stored via `str()` — so non-JSON values (e.g. numpy types) never crash a
250
+ `run`.
251
+ - The registries are **cleared before every run**, so runs never leak state.
252
+ - `run` executes the script with `runpy`, as `__main__`. Anything the script
253
+ `print()`s appears on the CLI output.
254
+
255
+ ---
256
+
257
+ ## Python API
258
+
259
+ Everything below is importable from the top-level `researchium` package.
260
+
261
+ ### Environments
262
+
263
+ | Function | Description |
264
+ | ----------------------------------------- | -------------------------------------------------- |
265
+ | `initialize_environment(path, name=None)` | Create the standard structure + `.researchium/` marker. Returns `Environment`. |
266
+ | `is_initialized(path)` | `True` if `path` is directly an environment root. |
267
+ | `discover_environment(path)` | Nearest environment at/above `path`. Raises `EnvironmentNotFoundError`. |
268
+ | `Environment` (dataclass) | `root`, `name`, `created`, `researchium_version`; properties `experiments_dir`, `src_dir`, `results_dir`, `experiments_notebook`, `experiments_registry`. |
269
+ | `load_experiments_registry(path)` | Read experiment records from `experiments.json`. |
270
+ | `save_experiments_registry(path, records)`| Write experiment records to `experiments.json`. |
271
+
272
+ ### Experiments
273
+
274
+ | Function | Description |
275
+ | ---------------------------------------------- | -------------------------------------------------- |
276
+ | `create_experiment(name, description, path)` | Register an experiment and return it. Raises `ExperimentExistsError`. |
277
+ | `list_experiments(path)` | All experiments, sorted by name. |
278
+ | `search_experiments(query, path)` | Experiments matching name/description. |
279
+ | `get_experiment(identifier, path)` | Look up by ID or name. Raises `ExperimentNotFoundError`. |
280
+ | `get_experiment_results(identifier, path)` | Result files (paths) of an experiment. |
281
+ | `Experiment` (dataclass) | `id`, `name`, `description`, `created`, `environment`; properties `path`, `results_dir`. |
282
+
283
+ ### Runtime
284
+
285
+ | Function | Description |
286
+ | ------------------------------------- | -------------------------------------------------- |
287
+ | `Config`, `Results` | Mutable registries your run script fills in. |
288
+ | `run_experiment(identifier, path)` | Execute the run script and persist outputs. Raises `RunScriptNotFoundError` / `RunFailedError`. |
289
+ | `format_configuration(identifier)` | Config as `key: value` lines. |
290
+ | `format_run_results(identifier)` | Results as `key: value` lines (listing fallback). |
291
+
292
+ ### Example
293
+
294
+ ```python
295
+ from researchium import (
296
+ Config,
297
+ Results,
298
+ create_experiment,
299
+ discover_environment,
300
+ initialize_environment,
301
+ run_experiment,
302
+ )
303
+
304
+ env = initialize_environment("~/study", name="Climate Genomics")
305
+ experiment = create_experiment(
306
+ "climate-model", "Temperature projections for 2050.", "~/study"
307
+ )
308
+
309
+ experiment.path.joinpath("run.py").write_text(
310
+ "from researchium import Config, Results\n"
311
+ "Config['seed'] = 42\n"
312
+ "Results['mae'] = 1.7\n"
313
+ )
314
+
315
+ run_experiment(experiment.name, "~/study") # runs + writes files
316
+ with open(experiment.path / "config.json") as fh:
317
+ print(fh.read())
318
+
319
+ # From anywhere inside the project:
320
+ env = discover_environment(".") # finds it by walking up
321
+ ```
322
+
323
+ ---
324
+
325
+ ## File formats
326
+
327
+ ### `experiments.md`
328
+
329
+ A human log. `create` appends a section per experiment:
330
+
331
+ ```markdown
332
+ ### alpine-transcriptomics
333
+
334
+ RNA-seq of alpine vs lowland populations.
335
+
336
+ - ID: `1e70f7c3`
337
+ - Created: 2026-09-22
338
+ - Folder: `experiments/alpine-transcriptomics`
339
+ ```
340
+
341
+ ### `experiments.json`
342
+
343
+ The machine-readable registry (created empty by `init`, appended by `create`):
344
+
345
+ ```json
346
+ {
347
+ "version": 1,
348
+ "experiments": [
349
+ {
350
+ "id": "1e70f7c3",
351
+ "name": "alpine-transcriptomics",
352
+ "description": "RNA-seq of alpine vs lowland populations.",
353
+ "created": "2026-09-22"
354
+ }
355
+ ]
356
+ }
357
+ ```
358
+
359
+ ### `config.md` / `results.md`
360
+
361
+ Human-formatted snapshots written by `run`:
362
+
363
+ ```markdown
364
+ # alpine-transcriptomics
365
+
366
+ Last run: 2026-09-22 23:08
367
+
368
+ ## Configuration
369
+
370
+ - **epochs**: 10
371
+ - **learning_rate**: 0.001
372
+ ```
373
+
374
+ ### `config.json` / `results.json`
375
+
376
+ The same data as JSON, suitable for programmatic consumption:
377
+
378
+ ```json
379
+ {
380
+ "epochs": 10,
381
+ "learning_rate": 0.001
382
+ }
383
+ ```
384
+
385
+ Note: a stale `readme.md` produced by a development build is removed
386
+ automatically on the next `run`.
387
+
388
+ ### `.researchium/environment.json`
389
+
390
+ Environment metadata; this directory is the environment marker:
391
+
392
+ ```json
393
+ {
394
+ "name": "Climate Genomics",
395
+ "created": "2026-09-22",
396
+ "researchium": "0.1.0"
397
+ }
398
+ ```
399
+
400
+ ---
401
+
402
+ ## Error handling
403
+
404
+ All domain errors subclass `researchium.ResearchiumError`:
405
+
406
+ | Error | Raised when |
407
+ | ---------------------------- | ----------------------------------------- |
408
+ | `EnvironmentExistsError` | `init` on an already-initialized directory |
409
+ | `EnvironmentNotFoundError` | no environment at/above the given path |
410
+ | `ExperimentExistsError` | creating a duplicate experiment name |
411
+ | `ExperimentNotFoundError` | no experiment matches the ID/name |
412
+ | `InvalidExperimentNameError` | empty or path-traversal experiment name |
413
+ | `RunScriptNotFoundError` | experiment has no `run.py` |
414
+ | `RunFailedError` | the run script raised |
415
+
416
+ The CLI catches these, prints `error: <message>` to stderr, and exits `1`.
417
+
418
+ ---
419
+
420
+ ## Development
421
+
422
+ ```bash
423
+ uv sync --extra dev # install the package + dev tools
424
+ uv run pytest # 44 tests
425
+ uv run pytest -q # keep it quiet
426
+ uv run ruff check . # lint (E, F, W, I, UP on src + tests)
427
+ ```
428
+
429
+ Test coverage is configured in `pyproject.toml` (`--cov=researchium
430
+ --cov-report=term-missing`) and is kept at 100%. The GitHub Actions workflow in
431
+ `.github/workflows/ci.yml` runs tests and lint on Python 3.10 / 3.12 / 3.14.
432
+
433
+ ### Adding a feature
434
+
435
+ 1. Add the function to the relevant module under `src/researchium/`.
436
+ 2. Export it from `researchium/__init__.py` and add it to `__all__`.
437
+ 3. Add tests under `tests/`; keep coverage at 100% (`uv run pytest` will show
438
+ any missed lines).
439
+ 4. Run `uv run ruff check . && uv run pytest` and reinstall the global tool if
440
+ you use it: `uv tool install --force /path/to/researchium`.
441
+
442
+ ---
443
+
444
+ ## Project layout
445
+
446
+ ```
447
+ researchium/
448
+ ├── pyproject.toml # PEP 621 metadata, hatchling build, ruff/pytest config
449
+ ├── uv.lock # locked development dependencies
450
+ ├── README.md
451
+ ├── .python-version
452
+ ├── .github/workflows/ci.yml # lint + tests matrix
453
+ ├── src/researchium/
454
+ │ ├── __init__.py # public API surface
455
+ │ ├── environment.py # init, discovery, environment metadata + registry
456
+ │ ├── experiments.py # experiment lifecycle, search, lookup
457
+ │ ├── runtime.py # Config/Results registries + runner + formatters
458
+ │ └── cli.py # argparse-based CLI (entry point `researchium`)
459
+ └── tests/
460
+ ├── test_environment.py
461
+ ├── test_experiments.py
462
+ ├── test_runtime.py
463
+ └── test_cli.py
464
+ ```
465
+
466
+ ---
467
+
468
+ ## Roadmap
469
+
470
+ Planned directions, in rough order:
471
+
472
+ - `researchium scaffold <name>` to generate a starter `run.py`.
473
+ - Experiment status (pending / running / completed) tracked in the registry.
474
+ - Deleting and renaming experiments with safe migration of results.
475
+ - A smaller `researchium` output format flag (`--json`) for scripting.