- Will it rain tomorrow? (real data)
- Read this first: three of the four verification stages could not be checked
- What is still established, which is not nothing
- What this does NOT prove
- The data
- The measured numbers
- Reproducing it
- Using it without MatrixAI
- The model and its training contract
- What is in the package
- Licence and links
- Make your own
- Read this first: three of the four verification stages could not be checked
Will it rain tomorrow? (real data)
The case that CANNOT be fully proven β which is why it is here.
A dense network with six inputs β temp_max, humidity, pressure, pressure_delta,
cloud_cover, precip_hours β one hidden layer of 16 ReLU units and a 2-way softmax, which
returns a probability for si (it rains the next day) and no. It was fitted to a 2,189-row file
of real daily weather observations β 1,751 rows used for training, 438 held out β and it is
published because it is the MatrixAI case that does not come out clean: most of what
matrixai verify normally checks cannot be checked here, and the package says so itself.
It is one of the three reproducible cases at matrixaistudio.org/casos.
Read this first: three of the four verification stages could not be checked
Every MatrixAI package carries a manifest, and matrixai verify reports four stages separately,
with four possible verdicts (PASS, FAIL, INCOMPARABLE, NOT_RUN). Run against this package,
this is the entire output, unedited:
$ matrixai verify paquete.zip --retrain --locale en
manifest PASS
R1 INCOMPARABLE β the package itself declares R1 is not possible: "Cannot regenerate the dataset from the recipe and compare its full sha256: this model has no data recipe, so its dataset cannot be regenerated and compared; why there is none is not recorded in the package; the dataset generation seed is unknown."
training INCOMPARABLE β the dataset could not be regenerated, so there is nothing to train on
R3 NOT_RUN β the retraining run did not complete, so there is no fresh value to contrast: the dataset could not be regenerated, so there is nothing to train on
$ echo $?
3
The exit code is 3, measured here on 2026-08-30 by re-running the published paquete.zip with
matrixai-core 1.7.0: output and exit code identical to the published salida.txt. The scale is
0 nothing failed Β· 2 something does not match Β· 3 it could not be checked β that line is
printed in the README of the two reproducible packages in this set; this bundle's README has
no verification section at all, because there is nothing here for verify to rebuild.
| Stage | What it would check | Verdict here | Why |
|---|---|---|---|
manifest |
every artifact matches the sha256 the manifest declares, and nothing travels that the manifest does not name | PASS | β |
R1 |
the dataset rebuilt from the recipe has the full sha256 the package declares | INCOMPARABLE | there is no recipe, so there is nothing to rebuild and compare |
training |
training runs to completion with what the package carries inside | INCOMPARABLE | with no regenerated dataset there is nothing to train on |
R3 |
the metrics of that fresh run fall inside their declared tolerance | NOT_RUN | the retraining never started, so there is no fresh number to contrast |
Verdicts and reasons: the output above, which matches salida.txt of the published case.
INCOMPARABLE means "I could not check this" β a distinct verdict from FAIL, and the wording is
quoted from https://matrixaistudio.org/manual/proof, where the four stages are described.
The package declares the same about itself before anyone runs anything. From reproduce.json:
"reproducible": false,
"reproducible_reason": "Not reproducible: this model has no data recipe, so its dataset cannot be regenerated and compared; why there is none is not recorded in the package; the dataset generation seed is unknown.",
"missing": ["recipe", "seed_dataset"]
and its verifiable block records "possible": true for manifest alone, and "possible": false
for r1, training and r3, each with the reason quoted above.
On the count. The project page phrases this as "two of the four stages come back
INCOMPARABLE", which is the count of INCOMPARABLE verdicts. The fourth, R3, comes back
NOT_RUN β a different word for a consequence of the same cause. Counted by what they establish
about this model: one stage passes, three establish nothing, and reproduce.json marks all three
"possible": false.
Why there is no recipe. In MatrixAI a recipe is a rule that generates the data β the other
two cases at /casos have one (predicted_kelvin = 1*celsius + 273.15, for instance), it travels
inside the package, and verify re-runs it, rebuilds the dataset, compares its digest, retrains
and contrasts the metric. This package has none, and it records that why there is none is
itself not written down: "why there is none is not recorded in the package". The reason the
authors give, on the project page, is that no rule was invented so the four stages would go green.
"No recipe" is not "no data". The 2,189 rows are published in full
(lluvia_nombrada.csv, 168,756
bytes). What is missing is a rule that regenerates them.
What is still established, which is not nothing
Each of these was re-measured on 2026-08-30 against the published files, with matrixai-core
1.7.0 (the package was built with 1.6.0). Every command is in Reproducing it.
- The package is internally consistent β
manifestPASS. Recomputing sha256 over the 15 files named inreproduce.json: 15 of 15 match. The only file in the zip not named by the manifest isreproduce.jsonitself, which is the manifest β a manifest cannot carry its own digest inside. - It predicts with no MatrixAI installed.
predict.pyneeds onlynumpyandonnxruntime. Re-run with imports ofmatrixaiblocked atsys.meta_path, it returned the published probabilities to the last digit. - The exported ONNX matches the reference implementation.
export_manifest.json:equivalence_check.passed: true,max_abs_diff7.45e-08,max_rel_diff4.81e-07, over 20 samples, againstatol1e-05 /rtol1e-04. Those 20 vectors come from a generator with a fixed default seed of 42 (matrixai/export/equivalence.py) β the same 20 points on every export, not a fresh sample. - The published weights do come from that published CSV, and you can check it by hand. This
is what
verifywill not do for you, because it will not guess where a dataset came from. But feeding the published CSV back through the published.mxaiand.mxtrainreproducedBest epoch: 11,Best validation loss: 0.533861,Accuracy: 0.762557, and aparams.best.jsonbyte-identical to the one in the package (sha256 2d8273df56be1b305e17504691ef3cce79550990fbee0dee146a8a663968f0cf). Re-exporting produced 9 of the 16 bundle files byte-identical, includingmodel.mxai,model.mxtrain,predict.py,params.best.json,example_input.jsonandexpected_output.json. The other seven differ βREADME.md,export_manifest.json,inference_spec.json,model.onnx,model_manifest.json,reproduce.jsonandspace/requirements.txtβ and all seven differ only in the version and timestamp they stamp on themselves (matrixai_version1.6.0 β 1.7.0,created_at/exported_at, thematrixai==pin inspace/requirements.txt, and theenvironment_sha256andmanifest_sha256derived from them). The ONNX graph itself serializes identically: one byte differs in the whole file, the6of the version string in its metadata. - The published metric can be recomputed from the two published files. See The measured numbers.
None of that says the model is good, or that the rows are a faithful record of anything. It says the package holds together and that the number on it is attached to a file with a digest.
What this does NOT prove
That the data are what they claim to be. This is the one that matters. reproduce.json
declares a dataset digest, and the published CSV matches it, and retraining from that CSV
reproduces the weights β so the chain from this file to these weights closes. What no digest
can establish is that those 2,189 rows are a faithful record of real weather, at a real place, over
a real period. That step is trust in whoever downloaded them, not evidence from the package. The
report writes it down instead of covering it with a PASS.
That 76.26% is how often it is right about tomorrow. It is accuracy over one contiguous 20%
block of the same file. Measured against that same block: always answering si scores 64.61%
(283 si / 155 no in those 438 rows). The same model scores 71.36% over the whole file and
70.13% over the 1,751 rows it trained on β so the held-out block is not a harder sample than the
rest, and the headline number is the best of the three, not a typical one.
That it is even-handed between the two answers. On that block it answers si for 363 of 438
days: recall 95.8% on si (271 of 283), recall 40.6% on no (63 of 155). It misses most dry days.
Confusion counts are in The measured numbers. Calibration was not measured
at all β the probabilities have not been checked against observed frequencies.
Where and when this weather happened. The CSV has no date and no location column, so which
station and which years are unrecoverable from the published files. What the file does show,
measured: llueve_manana on row i equals rain > 0 on row i+1 for 2,188 of 2,188
consecutive pairs. So the label rule is recoverable exactly from the published CSV even though no
case file states it, the rows are in sequence rather than shuffled, and the held-out block is the
later end of that sequence, not an interleaved sample. Whether consecutive rows are consecutive
calendar days cannot be checked β there is no date column.
That the model saw everything the file holds. The CSV carries 13 feature columns; the training
contract uses 6. temp_min, temp_mean, rain, wind_speed, dew_point, wind_dir_sin and
wind_dir_cos were not fed to the model. rain in particular is the column the label is derived
from, one row ahead.
Several things the package explicitly leaves empty, and an absent value is not a zero:
generation.epochs_ran and epochs_effective are null β the contract declares 40 epochs and the
training output reports epoch 11 as the best, but the package does not record how many ran
(re-running the published files writes epochs: 40 into runs/v1/training_trace.json, so all 40
did); artifacts.dataset.rows_used is null β and that field is the number of rows the training
consumed, not the size of the validation split: the core documents the distinction where it builds
the block (matrixai/export/reproduce.py, the comment on artifacts.dataset β rows are the raw
CSV's rows, the ones R1 has to regenerate; rows_used the ones training consumed; paraphrased
here, the source comment is in Spanish). Re-running the published files gives 1,751 training rows
and 438 held out, so 1,751 is the number that would fill that hole;
seeds.dataset is null; and each metric names its own gaps in an incomplete list β accuracy:
evaluator, evaluator_version, tolerance_rel; best_validation_loss adds aggregation, which
is itself null.
Authorship or authenticity. In the manifest's own words: "It does NOT prove authorship or authenticity: signatures are out of scope here." Nothing here is signed.
Fitness for any use. This is not a weather service and not a forecast product. Nothing on this page is a claim of regulatory conformity of any kind, in any jurisdiction.
This repository deliberately carries no model-index metrics block. That widget presents a
number as a checked result; the point of this card is that this particular number is one you have
to recompute yourself, by hand, from files whose provenance stops at a person.
The data
| Fact | Value | Source |
|---|---|---|
| Rows | 2,189 | reproduce.json β artifacts.dataset.rows; counted in the CSV |
| Columns | 14 β 13 features + label llueve_manana |
header of lluvia_nombrada.csv |
| Class counts | si 1,388 Β· no 801 |
salida.txt step 1; recounted from the CSV |
| Dataset sha256 | b0bb8777559188aad2dc9e817cbd10cf65c54fa2de8a445f900e0230a93480bd |
reproduce.json β artifacts.dataset.sha256 |
| Prepared-dataset sha256 | null β not recorded |
reproduce.json |
Origin, as stated on the project page: "Daily observations from Open-Meteo (open-meteo.com), ERA5 reanalysis. Its API is free for non-commercial use and its data derives from ERA5 (Copernicus/ECMWF), published under CC BY 4.0. The CSV you download here is a selection of columns plus a derived label β Β«will it rain tomorrowΒ» β computed by us; it is not the original data." The data licence is not the code licence: the AGPL-3.0 below covers MatrixAI and this package, not the observations.
One digest detail worth knowing before you check it
The published CSV has CRLF line endings, so sha256sum on the file as downloaded returns
ddec894dee537801eee8cb2eab82a221503b9deef1afa91451827a05a2ce4023, which is not the digest in
reproduce.json. That digest matches the same file with its CRLF newlines normalised to LF:
python3 -c "import hashlib;print(hashlib.sha256(open('lluvia_nombrada.csv','rb').read().replace(b'\r\n',b'\n')).hexdigest())"
# b0bb8777559188aad2dc9e817cbd10cf65c54fa2de8a445f900e0230a93480bd
The measured numbers
Everything below is either quoted from a published file or was measured by re-running the published files; the "source" column says which, and no number appears here that is not one of the two.
| Number | Value | Source |
|---|---|---|
| Validation accuracy | 0.7625570776255708 | reproduce.json β metrics[accuracy], split validation; also salida.txt step 2 |
| Best validation loss | 0.5338612217384824 | reproduce.json β metrics[best_validation_loss] |
| Best epoch | 11 | salida.txt step 2 (epochs_ran is null in the manifest) |
| Epochs declared | 40 | reproduce.json β generation.epochs_declared; RUN EPOCHS 40 in the .mxtrain |
| ONNX equivalence | PASS, max_abs_diff 7.45e-08 over 20 fixed samples |
export_manifest.json β equivalence_check |
| Metric tolerance | tolerance_abs 0.0, scope same_environment_same_seed, tolerance_rel null |
reproduce.json β metrics[*] |
| Backend / device | stdlib / cpu |
reproduce.json β generation |
| Seeds | split 42, init 42, dataset null |
reproduce.json β generation.seeds |
| Built with | matrixai 1.6.0, Python 3.12.3, numpy 2.4.4, onnx 1.21.0, onnxruntime 1.26.0, torch 2.11.0+cpu, Linux x86_64 |
reproduce.json β environment |
Measured on 2026-08-30 by re-running the published bundle over the published CSV (script below), not read from any file:
| Number | Value |
|---|---|
| Validation block | the last 438 rows of the CSV, contiguous, not shuffled |
| Accuracy on it | 334 / 438 = 0.7625570776255708 β identical to the published value |
| Accuracy over the 1,751 training rows | 0.7013135351227869 |
| Accuracy over all 2,189 rows | 0.7135678391959799 |
| Majority-class baseline on the validation block | 283 / 438 = 0.6461 |
| Majority-class baseline over the whole file | 1,388 / 2,189 = 0.6341 |
| Confusion on the validation block | true si β si 271, si β no 12; true no β no 63, no β si 92 |
Precision / recall, si |
0.7466 / 0.9576 |
Precision / recall, no |
0.8400 / 0.4065 |
reproduce.json records seeds.split: 42; the cut that produced this run is the contiguous last
20% of the file, which is what reproduces the published accuracy exactly.
# recompute the published accuracy. Run from inside the bundle, with a copy of
# lluvia_nombrada.csv next to predict.py. No MatrixAI needed.
import csv, sys, collections
sys.path.insert(0, ".") # run from inside the bundle
from predict import MatrixAIModel
rows = list(csv.DictReader(open("lluvia_nombrada.csv", newline="", encoding="utf-8")))
cut = int(len(rows) * 0.8)
model = MatrixAIModel()
fields = ["temp_max", "humidity", "pressure", "pressure_delta", "cloud_cover", "precip_hours"]
cm = collections.Counter()
for r in rows[cut:]:
p = model.predict({k: float(r[k]) for k in fields})
cm[(r["llueve_manana"], max(p, key=p.get))] += 1
ok = cm[("si", "si")] + cm[("no", "no")]
print(ok, "/", sum(cm.values()), "=", ok / sum(cm.values()), dict(cm))
# 334 / 438 = 0.7625570776255708
Importing predict.py from inside the bundle, as that script does, writes a __pycache__/ next to
it β and matrixai verify counts that as a file the manifest does not name, which turns
manifest PASS into INCOMPARABLE. Delete it, or work on a copy, before verifying.
Reproducing it
Python 3.10 or newer (requires-python = ">=3.10"). Nothing else: no account, no API key, no GPU.
pip install "matrixai-core[export]"
# the published inputs
mkdir -p caso/datos && cd caso
curl -O https://matrixaistudio.org/casos/lluvia/lluvia.mxai
curl -O https://matrixaistudio.org/casos/lluvia/lluvia.mxtrain
curl -O https://matrixaistudio.org/casos/lluvia/dia.json
curl -o datos/lluvia_nombrada.csv https://matrixaistudio.org/casos/lluvia/lluvia_nombrada.csv
# 1. train (the .mxtrain points at datos/lluvia_nombrada.csv)
matrixai train lluvia.mxai --training lluvia.mxtrain --output runs/v1
# 2. export the self-contained bundle
matrixai export-bundle lluvia.mxai --params runs/v1/params.best.json \
--outdir paquete --training lluvia.mxtrain --from-run runs/v1
# 3. predict one day, without MatrixAI on the path
cd paquete && python3 predict.py --input ../dia.json && cd ..
# 4. verify β on the ZIP, not on the folder you just built
zip -r paquete.zip paquete
matrixai verify paquete.zip --retrain --locale en # exit code 3
export-bundle writes a directory and has no zip option, so the zip line is a real step, not a
convenience.
Step 4 verifies the zip on purpose: verifying the freshly built folder leaves datos/ on disk
next to it, and the retraining stage would quietly pick it up. The project's build script records
that this shortcut is what hid a verifier defect β its comment says so in one sentence, in Spanish,
and it names one defect, not a number of them. That script is not part of the published case, so
this is the one statement on this page you cannot check from the files in this repository.
What that produced here on 2026-08-30, with matrixai-core 1.7.0 rather than the 1.6.0 recorded in
the package: identical training line (Best epoch: 11, Best validation loss: 0.533861,
Accuracy: 0.762557), byte-identical params.best.json, identical predict.py output to the last
digit, and the identical four-stage report with exit code 3. Only the run id differs (it is per
run), plus the seven files listed in What is still established.
The published bundle, its literal salida.txt and this exact report are also downloadable, already
run, at https://matrixaistudio.org/casos/lluvia/paquete.zip and
https://matrixaistudio.org/casos/lluvia/salida.txt.
Using it without MatrixAI
The bundle predicts on its own: ONNX plus a standalone wrapper. requirements.txt is
numpy>=1.24 and onnxruntime>=1.16 β MatrixAI is not among them.
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
python predict.py --input example_input.json # reproduces expected_output.json
from predict import MatrixAIModel
model = MatrixAIModel() # reads inference_spec.json next to predict.py
model.predict({"temp_max": 18.0, "humidity": 92.0, "pressure": 1004.0,
"pressure_delta": -6.0, "cloud_cover": 95.0, "precip_hours": 8.0})
Feed raw, human-readable values: predict.py applies the normalisation and label mapping the
model was trained with, from inference_spec.json. Measured outputs, identical from the published
bundle and from a freshly rebuilt one:
| Input | Output |
|---|---|
example_input.json (22.5 Β°C, 65% RH, 1010 hPa, Ξ0, 50% cloud, 12 h precip) |
{"no": 0.35993796586990356, "si": 0.6400620341300964} |
dia.json β humid, pressure falling (18 Β°C, 92%, 1004 hPa, Ξβ6, 95% cloud, 8 h) |
{"no": 0.05672343820333481, "si": 0.9432765245437622} |
dia_seco.json β dry, high pressure (28 Β°C, 35%, 1026 hPa, Ξ+3, 10% cloud, 0 h) |
{"no": 0.7396246790885925, "si": 0.26037532091140747} |
The raw graph, if you prefer it, expects an already-normalised float32 vector of shape
[batch, 6] on input Parte, in the order temp_max, humidity, pressure, pressure_delta, cloud_cover, precip_hours, and returns [batch, 2] on Pronostico_out2, ordered no, si.
Input ranges declared in inference_spec.json: temp_max [0, 45] Β°C, humidity [30, 100] %,
pressure [980, 1040] hPa, pressure_delta [β20, 20] hPa, cloud_cover [0, 100] %,
precip_hours [0, 24] h. A value outside its range is clamped to the edge, not rejected β
measured: temp_max: 60.0 returns exactly the same probabilities as temp_max: 45.0. It is not
silent, though: predict.py --meta reports
"clipped": [{"field": "temp_max", "raw_value": 60.0, "normalized_value": 1.0}] (the command
prints that JSON with indent=2; it is quoted inline here). Read that metadata if your inputs can
leave the range.
The model and its training contract
Both travel inside the bundle and are published as text. This is the whole model:
PROJECT LluviaManana
VECTOR Parte[6]
temp_max: Scalar[0, 45]
humidity: Scalar[30, 100]
pressure: Scalar[980, 1040]
pressure_delta: Scalar[-20, 20]
cloud_cover: Scalar[0, 100]
precip_hours: Scalar[0, 24]
END
NETWORK Pronostico
INPUT Parte
LAYER Dense units=16 activation=relu
LAYER Dense units=2 activation=softmax
OUTPUT llueve_manana: ProbabilityMap[no, si]
END
GRAPH
Parte -> Pronostico
END
and this is the whole training contract:
MODEL lluvia.mxai
DATASET Observaciones
SOURCE csv("datos/lluvia_nombrada.csv")
INPUT Parte FROM COLUMNS [temp_max, humidity, pressure, pressure_delta, cloud_cover, precip_hours]
TARGET llueve_manana: Label[no, si]
SPLIT train=0.8 validation=0.2 seed=42
BATCH size=32
END
LOSS Perdida
TYPE cross_entropy
PREDICTION Pronostico
TARGET llueve_manana
END
OPTIMIZER Optimizador
TYPE sgd
LEARNING_RATE 0.1
UPDATE Pronostico.*
END
RUN
EPOCHS 40
END
Identifiers are in Spanish because that is what the published files contain; renaming them here would break the digests they are checked against.
MatrixAI can also build a model from a plain-language description. This case was not made that
way: it was made from the .mxai and the .mxtrain published above, over a CSV.
What is in the package
model_hash mxai_7b9f3359d895e818 Β· parameter_schema_hash params_91da9c484c855d71 Β·
manifest sha256 93fe1497e44e6e3b17a6ef820e8916781079159a8abd8c57c0f64ab99b9f9762
| File | What it is | sha256 (first 16) |
|---|---|---|
model.mxai |
the model, as text | 791ab6213862f3cc |
model.mxtrain |
the training contract | 1a213aae841d4f55 |
params.best.json |
the trained weights | 2d8273df56be1b30 |
model.onnx |
ONNX graph, opset 17 | 158fe786d19b87bd |
inference_spec.json |
how a raw record maps onto the model input | 4e15cc301aba9d8d |
predict.py |
standalone wrapper: raw values in, labelled probabilities out | 4ff7568981a68350 |
model_manifest.json |
model metadata and hashes | d72eb3a54db526e8 |
export_manifest.json |
export metadata, tolerance and the equivalence check | 86a2aeaa6c2c8502 |
reproduce.json |
what can and cannot be rebuilt, and every digest β the manifest itself | (self) |
example_input.json / expected_output.json |
a ready-to-run example and its output | c99e2339986882e0 / c6f261a69adee8a0 |
requirements.txt |
numpy + onnxruntime | b69df72618af95a0 |
README.md |
the bundle's own readme | b1b1b99c12fe05ef |
space/ |
a Gradio Space template the exporter emits, for whoever wants one | β |
Full digests are in reproduce.json; manifest PASS means all 15 of them were checked and matched.
There is no data_recipe.txt in this package β that absence is the whole point of the case.
Licence and links
- Code and model: AGPL-3.0-only. MatrixAI is open source;
predict.pycarries the same header inside the bundle. - Data: not AGPL. Open-Meteo's API is free for non-commercial use; the underlying ERA5 data (Copernicus/ECMWF) is published under CC BY 4.0. The published CSV is a selection of columns plus a label derived by the project's authors, and it is not the original data.
- This case, with its recipe-less report, the whole bundle and every artefact to download: https://matrixaistudio.org/casos#lluvia
- The other two cases: Celsius to Kelvin Β· Readmission risk (synthetic)
- What a receipt proves and what it does not, stage by stage: https://matrixaistudio.org/manual/proof
- Site: https://matrixaistudio.org
- Source (AGPL-3.0): https://github.com/robertollweb/matrixAI
- PyPI: https://pypi.org/project/matrixai-core/ β
pip install "matrixai-core[export]"
MatrixAI builds, trains, audits and deploys a neural network from a plain-language description or a CSV, and emits a cryptographic receipt at each step. It is open source under AGPL-3.0, and every exported package predicts with no MatrixAI installed. This is the case where the receipt has to admit what it could not check.
Make your own
This model is not a demo to look at β it is a case you can redo, and the tool that produced it is free.
MatrixAI Studio turns a written description, or a CSV you already have, into a neural network you can question: it builds it, trains it, and emits a cryptographic receipt at every step, so anybody can re-check what you claim. It runs on your own machine β no account, no cloud, no API key.
- Download it and build your own: https://matrixaistudio.org/
- Try it in the browser first, installing nothing: https://matrixaistudio.org/demo/
- This case, with every file and command: https://matrixaistudio.org/casos#lluvia
- What it does β and what it does not: https://matrixaistudio.org/what-you-get
- How the receipts work: https://matrixaistudio.org/manual/proof
- Source (AGPL-3.0): https://github.com/robertollweb/matrixAI Β· PyPI:
pip install matrixai-core
If something on this page is not true, it should be visible from the outside. That is the whole point of publishing the package and not just the numbers.