websemantic 0.2.5__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.
- websemantic-0.2.5/LICENSE +135 -0
- websemantic-0.2.5/PKG-INFO +64 -0
- websemantic-0.2.5/README.md +65 -0
- websemantic-0.2.5/docs/pypi.md +15 -0
- websemantic-0.2.5/pyproject.toml +56 -0
- websemantic-0.2.5/setup.cfg +4 -0
- websemantic-0.2.5/src/websemantic/__init__.py +3 -0
- websemantic-0.2.5/src/websemantic/adapters/__init__.py +1 -0
- websemantic-0.2.5/src/websemantic/adapters/lql.py +118 -0
- websemantic-0.2.5/src/websemantic/adapters/pyrcel.py +104 -0
- websemantic-0.2.5/src/websemantic/adapters/tls.py +130 -0
- websemantic-0.2.5/src/websemantic/adapters/tls_outputs.py +70 -0
- websemantic-0.2.5/src/websemantic/cli.py +592 -0
- websemantic-0.2.5/src/websemantic/comparison.py +223 -0
- websemantic-0.2.5/src/websemantic/conversation.py +44 -0
- websemantic-0.2.5/src/websemantic/core/__init__.py +1 -0
- websemantic-0.2.5/src/websemantic/core/validation.py +172 -0
- websemantic-0.2.5/src/websemantic/environments.json +23 -0
- websemantic-0.2.5/src/websemantic/gemini.py +222 -0
- websemantic-0.2.5/src/websemantic/geography.py +223 -0
- websemantic-0.2.5/src/websemantic/lql_comparison.py +138 -0
- websemantic-0.2.5/src/websemantic/qualitative.py +60 -0
- websemantic-0.2.5/src/websemantic/registry.py +45 -0
- websemantic-0.2.5/src/websemantic/replay.py +68 -0
- websemantic-0.2.5/src/websemantic/semantics.py +266 -0
- websemantic-0.2.5/src/websemantic/session.py +271 -0
- websemantic-0.2.5/src/websemantic/units.py +92 -0
- websemantic-0.2.5/src/websemantic/web_research.py +146 -0
- websemantic-0.2.5/src/websemantic.egg-info/PKG-INFO +64 -0
- websemantic-0.2.5/src/websemantic.egg-info/SOURCES.txt +71 -0
- websemantic-0.2.5/src/websemantic.egg-info/dependency_links.txt +1 -0
- websemantic-0.2.5/src/websemantic.egg-info/entry_points.txt +2 -0
- websemantic-0.2.5/src/websemantic.egg-info/requires.txt +33 -0
- websemantic-0.2.5/src/websemantic.egg-info/top_level.txt +1 -0
- websemantic-0.2.5/tests/test_affine_units.py +22 -0
- websemantic-0.2.5/tests/test_agent_common.py +55 -0
- websemantic-0.2.5/tests/test_agent_portable.py +149 -0
- websemantic-0.2.5/tests/test_automatic_results.py +62 -0
- websemantic-0.2.5/tests/test_backends_smoke.py +36 -0
- websemantic-0.2.5/tests/test_comparison.py +210 -0
- websemantic-0.2.5/tests/test_core_ontology.py +176 -0
- websemantic-0.2.5/tests/test_daily_outputs.py +32 -0
- websemantic-0.2.5/tests/test_descriptor_transfer.py +156 -0
- websemantic-0.2.5/tests/test_e1_candidate.py +49 -0
- websemantic-0.2.5/tests/test_e1_review_regressions.py +47 -0
- websemantic-0.2.5/tests/test_e1_scoring.py +43 -0
- websemantic-0.2.5/tests/test_e1_scoring_v2.py +124 -0
- websemantic-0.2.5/tests/test_e3_bench.py +155 -0
- websemantic-0.2.5/tests/test_environment_menu.py +26 -0
- websemantic-0.2.5/tests/test_geography.py +79 -0
- websemantic-0.2.5/tests/test_guided_study.py +98 -0
- websemantic-0.2.5/tests/test_installation.py +55 -0
- websemantic-0.2.5/tests/test_length_controls.py +63 -0
- websemantic-0.2.5/tests/test_lql_adapter.py +127 -0
- websemantic-0.2.5/tests/test_lql_comparison.py +87 -0
- websemantic-0.2.5/tests/test_natural_conversation.py +71 -0
- websemantic-0.2.5/tests/test_number_words.py +41 -0
- websemantic-0.2.5/tests/test_operational_seed.py +47 -0
- websemantic-0.2.5/tests/test_output_contract_links.py +22 -0
- websemantic-0.2.5/tests/test_private_key.py +26 -0
- websemantic-0.2.5/tests/test_profile_acceptance_safety.py +110 -0
- websemantic-0.2.5/tests/test_published_contract.py +89 -0
- websemantic-0.2.5/tests/test_pyrcel_adapter.py +108 -0
- websemantic-0.2.5/tests/test_qualitative.py +100 -0
- websemantic-0.2.5/tests/test_semantic_pack.py +56 -0
- websemantic-0.2.5/tests/test_semantic_structure.py +54 -0
- websemantic-0.2.5/tests/test_session.py +136 -0
- websemantic-0.2.5/tests/test_shapes.py +136 -0
- websemantic-0.2.5/tests/test_unit_labels.py +48 -0
- websemantic-0.2.5/tests/test_validate_answers.py +94 -0
- websemantic-0.2.5/tests/test_validation.py +159 -0
- websemantic-0.2.5/tests/test_web_research.py +90 -0
- websemantic-0.2.5/tests/test_workflow.py +179 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# PolyForm Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree
|
|
8
|
+
to them as both strict obligations and conditions to all
|
|
9
|
+
your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the
|
|
14
|
+
software to do everything you might do with the software
|
|
15
|
+
that would otherwise infringe the licensor's copyright
|
|
16
|
+
in it for any permitted purpose. However, you may
|
|
17
|
+
only distribute the software according to [Distribution
|
|
18
|
+
License](#distribution-license) and make changes or new works
|
|
19
|
+
based on the software according to [Changes and New Works
|
|
20
|
+
License](#changes-and-new-works-license).
|
|
21
|
+
|
|
22
|
+
## Distribution License
|
|
23
|
+
|
|
24
|
+
The licensor grants you an additional copyright license
|
|
25
|
+
to distribute copies of the software. Your license
|
|
26
|
+
to distribute covers distributing the software with
|
|
27
|
+
changes and new works permitted by [Changes and New Works
|
|
28
|
+
License](#changes-and-new-works-license).
|
|
29
|
+
|
|
30
|
+
## Notices
|
|
31
|
+
|
|
32
|
+
You must ensure that anyone who gets a copy of any part of
|
|
33
|
+
the software from you also gets a copy of these terms or the
|
|
34
|
+
URL for them above, as well as copies of any plain-text lines
|
|
35
|
+
beginning with `Required Notice:` that the licensor provided
|
|
36
|
+
with the software. For example:
|
|
37
|
+
|
|
38
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
39
|
+
|
|
40
|
+
## Changes and New Works License
|
|
41
|
+
|
|
42
|
+
The licensor grants you an additional copyright license to
|
|
43
|
+
make changes and new works based on the software for any
|
|
44
|
+
permitted purpose.
|
|
45
|
+
|
|
46
|
+
## Patent License
|
|
47
|
+
|
|
48
|
+
The licensor grants you a patent license for the software that
|
|
49
|
+
covers patent claims the licensor can license, or becomes able
|
|
50
|
+
to license, that you would infringe by using the software.
|
|
51
|
+
|
|
52
|
+
## Noncommercial Purposes
|
|
53
|
+
|
|
54
|
+
Any noncommercial purpose is a permitted purpose.
|
|
55
|
+
|
|
56
|
+
## Personal Uses
|
|
57
|
+
|
|
58
|
+
Personal use for research, experiment, and testing for
|
|
59
|
+
the benefit of public knowledge, personal study, private
|
|
60
|
+
entertainment, hobby projects, amateur pursuits, or religious
|
|
61
|
+
observance, without any anticipated commercial application,
|
|
62
|
+
is use for a permitted purpose.
|
|
63
|
+
|
|
64
|
+
## Noncommercial Organizations
|
|
65
|
+
|
|
66
|
+
Use by any charitable organization, educational institution,
|
|
67
|
+
public research organization, public safety or health
|
|
68
|
+
organization, environmental protection organization,
|
|
69
|
+
or government institution is use for a permitted purpose
|
|
70
|
+
regardless of the source of funding or obligations resulting
|
|
71
|
+
from the funding.
|
|
72
|
+
|
|
73
|
+
## Fair Use
|
|
74
|
+
|
|
75
|
+
You may have "fair use" rights for the software under the
|
|
76
|
+
law. These terms do not limit them.
|
|
77
|
+
|
|
78
|
+
## No Other Rights
|
|
79
|
+
|
|
80
|
+
These terms do not allow you to sublicense or transfer any of
|
|
81
|
+
your licenses to anyone else, or prevent the licensor from
|
|
82
|
+
granting licenses to anyone else. These terms do not imply
|
|
83
|
+
any other licenses.
|
|
84
|
+
|
|
85
|
+
## Patent Defense
|
|
86
|
+
|
|
87
|
+
If you make any written claim that the software infringes or
|
|
88
|
+
contributes to infringement of any patent, your patent license
|
|
89
|
+
for the software granted under these terms ends immediately. If
|
|
90
|
+
your company makes such a claim, your patent license ends
|
|
91
|
+
immediately for work on behalf of your company.
|
|
92
|
+
|
|
93
|
+
## Violations
|
|
94
|
+
|
|
95
|
+
The first time you are notified in writing that you have
|
|
96
|
+
violated any of these terms, or done anything with the software
|
|
97
|
+
not covered by your licenses, your licenses can nonetheless
|
|
98
|
+
continue if you come into full compliance with these terms,
|
|
99
|
+
and take practical steps to correct past violations, within
|
|
100
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
101
|
+
end immediately.
|
|
102
|
+
|
|
103
|
+
## No Liability
|
|
104
|
+
|
|
105
|
+
***As far as the law allows, the software comes as is, without
|
|
106
|
+
any warranty or condition, and the licensor will not be liable
|
|
107
|
+
to you for any damages arising out of these terms or the use
|
|
108
|
+
or nature of the software, under any kind of legal claim.***
|
|
109
|
+
|
|
110
|
+
## Definitions
|
|
111
|
+
|
|
112
|
+
The **licensor** is the individual or entity offering these
|
|
113
|
+
terms, and the **software** is the software the licensor makes
|
|
114
|
+
available under these terms.
|
|
115
|
+
|
|
116
|
+
**You** refers to the individual or entity agreeing to these
|
|
117
|
+
terms.
|
|
118
|
+
|
|
119
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
120
|
+
or other kind of organization that you work for, plus all
|
|
121
|
+
organizations that have control over, are under the control of,
|
|
122
|
+
or are under common control with that organization. **Control**
|
|
123
|
+
means ownership of substantially all the assets of an entity,
|
|
124
|
+
or the power to direct its management and policies by vote,
|
|
125
|
+
contract, or otherwise. Control can be direct or indirect.
|
|
126
|
+
|
|
127
|
+
**Your licenses** are all the licenses granted to you for the
|
|
128
|
+
software under these terms.
|
|
129
|
+
|
|
130
|
+
**Use** means anything you do with the software requiring one
|
|
131
|
+
of your licenses.
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
Required Notice: Copyright (c) 2026 Cyril Voyant.
|
|
135
|
+
Required Notice: WebSemantic_TLS — https://github.com/cyrilvoyant/websemantic
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: websemantic
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: Software-agnostic semantic layer from intent to qualified simulated data (research prototype)
|
|
5
|
+
Author: Cyril Voyant
|
|
6
|
+
License-Expression: PolyForm-Noncommercial-1.0.0
|
|
7
|
+
Project-URL: Homepage, https://www.cyrilvoyant.com
|
|
8
|
+
Project-URL: Repository, https://github.com/cyrilvoyant/websemantic
|
|
9
|
+
Project-URL: Documentation, https://github.com/cyrilvoyant/websemantic#readme
|
|
10
|
+
Project-URL: Ontology, https://w3id.org/websemantic/ns
|
|
11
|
+
Project-URL: DOI, https://doi.org/10.5281/zenodo.23238902
|
|
12
|
+
Keywords: semantic web,scientific software,large language models,ontology,SHACL,provenance,FAIR
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: pydantic>=2.6
|
|
21
|
+
Requires-Dist: pyyaml>=6.0
|
|
22
|
+
Requires-Dist: rdflib>=7.0
|
|
23
|
+
Requires-Dist: pyshacl>=0.26
|
|
24
|
+
Requires-Dist: packaging>=24
|
|
25
|
+
Provides-Extra: tls
|
|
26
|
+
Requires-Dist: numpy>=1.24; extra == "tls"
|
|
27
|
+
Requires-Dist: pandas>=2.0; extra == "tls"
|
|
28
|
+
Provides-Extra: backends
|
|
29
|
+
Requires-Dist: numpy>=1.24; extra == "backends"
|
|
30
|
+
Requires-Dist: pandas>=2.0; extra == "backends"
|
|
31
|
+
Provides-Extra: atmosphere
|
|
32
|
+
Requires-Dist: numpy; extra == "atmosphere"
|
|
33
|
+
Requires-Dist: scipy; extra == "atmosphere"
|
|
34
|
+
Requires-Dist: pandas; extra == "atmosphere"
|
|
35
|
+
Requires-Dist: polars; extra == "atmosphere"
|
|
36
|
+
Requires-Dist: xarray; extra == "atmosphere"
|
|
37
|
+
Requires-Dist: netcdf4; extra == "atmosphere"
|
|
38
|
+
Requires-Dist: setuptools; extra == "atmosphere"
|
|
39
|
+
Requires-Dist: jax==0.11.2; extra == "atmosphere"
|
|
40
|
+
Requires-Dist: diffrax==0.7.2; extra == "atmosphere"
|
|
41
|
+
Requires-Dist: equinox==0.13.8; extra == "atmosphere"
|
|
42
|
+
Requires-Dist: optimistix==0.1.0; extra == "atmosphere"
|
|
43
|
+
Provides-Extra: llm
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
46
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
47
|
+
Requires-Dist: jsonschema>=4.18; extra == "dev"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# WebSemantic
|
|
51
|
+
|
|
52
|
+
Software-agnostic semantic layer for scientific codes (research prototype). One descriptor per code is compiled into
|
|
53
|
+
three documents: a readable contract for language models, an ontology with SHACL rules, and FAIR metadata. A
|
|
54
|
+
deterministic validator checks the scenario a model proposes (values, units, origins, acceptances) before the
|
|
55
|
+
unchanged code runs, and the outputs are stored with quantity, unit, origin, provenance and validity limits.
|
|
56
|
+
|
|
57
|
+
This package provides the Python layer: validation gate, adapters, semantic tools and command-line entry point. The
|
|
58
|
+
descriptors, ontology, contracts and the pinned scientific codes (TLS, LQL-Equiv, pyrcel) live in the repository;
|
|
59
|
+
full use requires a clone with its submodules.
|
|
60
|
+
|
|
61
|
+
- Repository and documentation: https://github.com/cyrilvoyant/websemantic
|
|
62
|
+
- Ontology (persistent namespace): https://w3id.org/websemantic/ns
|
|
63
|
+
- Concept DOI (all versions): https://doi.org/10.5281/zenodo.23238902
|
|
64
|
+
- Licence: PolyForm Noncommercial 1.0.0
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# WebSemantic
|
|
2
|
+
|
|
3
|
+
Préparer une simulation en phrases, examiner les hypothèses et obtenir des résultats avec leurs unités et leur provenance. Trois profils : TLS, LQL-Equiv (un cursus fictif) et pyrcel (un mode d’aérosol, ascendance constante).
|
|
4
|
+
|
|
5
|
+
[](https://doi.org/10.5281/zenodo.23238902) · ontologie : [w3id.org/websemantic/ns](https://w3id.org/websemantic/ns) · [Contribuer](CONTRIBUTING.md) · [Sécurité](SECURITY.md)
|
|
6
|
+
|
|
7
|
+
## Avec un agent Python
|
|
8
|
+
|
|
9
|
+
Lisez [les règles de réutilisation](llm.md), puis [le point d’entrée Python](agent/README.md). Il indique les fichiers, leurs empreintes et la commande commune aux trois modèles : `python agent/run.py tls|lql|pyrcel scenario.json` (choisir un modèle). Ce parcours utilise les fichiers sources, sans Git ni clé Gemini.
|
|
10
|
+
|
|
11
|
+
## Utiliser
|
|
12
|
+
|
|
13
|
+
Dans le package Windows, ouvrez `Installer.cmd` une fois, puis `WebSemantic.cmd` et choisissez 1 (TLS), 2 (LQL) ou 3 (pyrcel).
|
|
14
|
+
|
|
15
|
+
> Étudie un tunnel fictif de 2 km, deux tubes et deux voies par tube, avec éclairage LED fixe et ventilation longitudinale. Propose les paramètres manquants.
|
|
16
|
+
|
|
17
|
+
Demandez le tableau des paramètres ou les formules, précisez les choix, puis acceptez les hypothèses. Le calcul démarre lorsque la configuration est complète et validée. Une modification explicite recalcule l’étude. Le dossier des résultats est affiché à chaque calcul.
|
|
18
|
+
|
|
19
|
+
Le [guide](packaging/windows/Guide.txt) propose quatre essais, dont une comparaison de deux tunnels et une exécution par un agent Python.
|
|
20
|
+
|
|
21
|
+
## Installer depuis GitHub
|
|
22
|
+
|
|
23
|
+
Python 3.12+ et Git sont nécessaires pour installer les trois profils depuis GitHub. Configurez `GEMINI_API_KEY` pour le dialogue.
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
git clone --recurse-submodules https://github.com/cyrilvoyant/websemantic.git
|
|
27
|
+
cd websemantic
|
|
28
|
+
python -m venv .venv
|
|
29
|
+
& .\.venv\Scripts\python.exe -m pip install -e ".[tls,atmosphere]"
|
|
30
|
+
& .\.venv\Scripts\python.exe -m websemantic.cli chat
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
[Installation](docs/installation-windows.md) · [Dialogue et raccourcis](docs/terminal.md)
|
|
34
|
+
|
|
35
|
+
## Autres profils
|
|
36
|
+
|
|
37
|
+
LQL : « Étudie un schéma fictif rectum/prostate : 20 fractions de 3 Gy, référence 2 Gy. Propose les hypothèses manquantes. »
|
|
38
|
+
|
|
39
|
+
pyrcel : « Simule une parcelle à 1 m/s, 283 K, 850 hPa et S0 = -0,02 ; un mode lognormal de 1000 particules/cm³, rayon sec médian 0,05 µm, sigma 2, kappa 0,54. Propose les réglages manquants. »
|
|
40
|
+
|
|
41
|
+
Examinez les paramètres et leurs unités, puis acceptez les hypothèses. LQL sert à la recherche sur des schémas fictifs ; pyrcel simule une parcelle idéale.
|
|
42
|
+
|
|
43
|
+
## Résultats
|
|
44
|
+
|
|
45
|
+
TLS produit des données simulées. L’énergie totale couvre la période calculée ; l’énergie annualisée extrapole par `365/n_days`. Puissance : kW ; énergie : kWh ou MWh selon la table.
|
|
46
|
+
|
|
47
|
+
Les CSV contiennent les trajectoires, les séries journalières, les profils et les indicateurs par réalisation. `manifest.json` décrit les paramètres, les sources, les unités et les agrégations ; `semantics.ttl` conserve la provenance. Une comparaison ajoute `scenario` aux CSV réunis et conserve les deux calculs individuels. `comparison.json` donne les écarts entre médianes.
|
|
48
|
+
|
|
49
|
+
[Paramètres et sorties TLS](docs/tls-reference.md) · [Contrat JSON](ontology/tls-contract.json) · [Exécution depuis un agent](docs/agent-contract.md)
|
|
50
|
+
|
|
51
|
+
## Développement
|
|
52
|
+
|
|
53
|
+
Les définitions sont maintenues dans le [descripteur](descriptors/tls/descriptor.yaml). Le calcul utilise l’adaptateur et le backend TLS fixé, sans modifier ses sources.
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
& .\.venv\Scripts\python.exe -m pip install -e ".[dev,tls]"
|
|
57
|
+
& .\.venv\Scripts\python.exe -m pytest -q
|
|
58
|
+
& .\.venv\Scripts\python.exe -m ruff check src tests tools
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
[Architecture](docs/architecture.md) · [Intégration](docs/integrating-models.md) · [Protocole](docs/protocol.md)
|
|
62
|
+
|
|
63
|
+
## Licence
|
|
64
|
+
|
|
65
|
+
[PolyForm Noncommercial 1.0.0](LICENSE). Les composants conservent leurs licences ; les révisions déjà publiées sous MIT conservent leurs permissions. Le dépôt public ne contient aucune clé. Les archives privées de validation restent privées.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# WebSemantic
|
|
2
|
+
|
|
3
|
+
Software-agnostic semantic layer for scientific codes (research prototype). One descriptor per code is compiled into
|
|
4
|
+
three documents: a readable contract for language models, an ontology with SHACL rules, and FAIR metadata. A
|
|
5
|
+
deterministic validator checks the scenario a model proposes (values, units, origins, acceptances) before the
|
|
6
|
+
unchanged code runs, and the outputs are stored with quantity, unit, origin, provenance and validity limits.
|
|
7
|
+
|
|
8
|
+
This package provides the Python layer: validation gate, adapters, semantic tools and command-line entry point. The
|
|
9
|
+
descriptors, ontology, contracts and the pinned scientific codes (TLS, LQL-Equiv, pyrcel) live in the repository;
|
|
10
|
+
full use requires a clone with its submodules.
|
|
11
|
+
|
|
12
|
+
- Repository and documentation: https://github.com/cyrilvoyant/websemantic
|
|
13
|
+
- Ontology (persistent namespace): https://w3id.org/websemantic/ns
|
|
14
|
+
- Concept DOI (all versions): https://doi.org/10.5281/zenodo.23238902
|
|
15
|
+
- Licence: PolyForm Noncommercial 1.0.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "websemantic"
|
|
7
|
+
version = "0.2.5"
|
|
8
|
+
description = "Software-agnostic semantic layer from intent to qualified simulated data (research prototype)"
|
|
9
|
+
readme = "docs/pypi.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "PolyForm-Noncommercial-1.0.0"
|
|
12
|
+
authors = [{ name = "Cyril Voyant" }]
|
|
13
|
+
keywords = ["semantic web", "scientific software", "large language models", "ontology", "SHACL", "provenance", "FAIR"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Scientific/Engineering",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"pydantic>=2.6",
|
|
22
|
+
"pyyaml>=6.0",
|
|
23
|
+
"rdflib>=7.0",
|
|
24
|
+
"pyshacl>=0.26",
|
|
25
|
+
"packaging>=24",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://www.cyrilvoyant.com"
|
|
30
|
+
Repository = "https://github.com/cyrilvoyant/websemantic"
|
|
31
|
+
Documentation = "https://github.com/cyrilvoyant/websemantic#readme"
|
|
32
|
+
Ontology = "https://w3id.org/websemantic/ns"
|
|
33
|
+
DOI = "https://doi.org/10.5281/zenodo.23238902"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
tls = ["numpy>=1.24", "pandas>=2.0"]
|
|
37
|
+
# Target simulators. TLS and LQL-Equiv are loaded from the pinned submodules in external/.
|
|
38
|
+
backends = ["numpy>=1.24", "pandas>=2.0"]
|
|
39
|
+
atmosphere = ["numpy", "scipy", "pandas", "polars", "xarray", "netcdf4", "setuptools", "jax==0.11.2", "diffrax==0.7.2", "equinox==0.13.8", "optimistix==0.1.0"]
|
|
40
|
+
llm = [] # Gemini uses the Python standard library REST client.
|
|
41
|
+
dev = ["pytest>=8", "ruff>=0.6", "jsonschema>=4.18"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
websemantic = "websemantic.cli:main"
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.pytest.ini_options]
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.package-data]
|
|
53
|
+
websemantic = ["environments.json"]
|
|
54
|
+
|
|
55
|
+
[tool.ruff]
|
|
56
|
+
extend-exclude = ["external"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Thin per-software adapters. The only code allowed to call target simulator APIs."""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Read-only LQL-Equiv execution for one explicitly fictitious course."""
|
|
2
|
+
|
|
3
|
+
import csv
|
|
4
|
+
import hashlib
|
|
5
|
+
import importlib.util
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import sys
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import asdict
|
|
11
|
+
from datetime import datetime, timezone
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from websemantic.core.validation import validate
|
|
15
|
+
from websemantic.semantics import export_semantics
|
|
16
|
+
|
|
17
|
+
EXPECTED_COMMIT = "dfc9a338205b8864b8e3470c4ae245b019e88844"
|
|
18
|
+
# Filled from the pinned backend; line endings do not change numerical sources.
|
|
19
|
+
EXPECTED_FILES = {'__init__.py': '2685b5a34b0da98c8329928f1de533ec6619178c2e2e89e92d48e231ad7a9d82', 'data/__init__.py': 'c849ce7dc4c2cd921e57faf5ffa453f8772eeed07367af69f1f9d7288f1741dd', 'data/tissues.json': '63283344abdc8147da7e112e0a5d728d1dc0bc565ad9950e9e5adccd08027f61', 'model.py': '3e0638c077e2fd9a04fedf7520a8e0c008f88d61b6e28974abd9533d681e0800', 'schedule.py': '7cffecd6e82acf536521aab1c1f58ae997a6754d43185ae6d317752c057f7dfa', 'tissues.py': '9ef2ff4d9a93b040697e5199d6902ce0ca9888474591de56126accf826a05c15'}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _backend(workspace, descriptor):
|
|
23
|
+
if descriptor["software"]["commit"] != EXPECTED_COMMIT:
|
|
24
|
+
raise ValueError("Révision LQL-Equiv incompatible avec cet adaptateur.")
|
|
25
|
+
root = Path(workspace) / "external/LQL-Equiv-web/src/lqlequiv"
|
|
26
|
+
for relative, expected in EXPECTED_FILES.items():
|
|
27
|
+
digest = hashlib.sha256((root / relative).read_bytes().replace(b"\r\n", b"\n")).hexdigest()
|
|
28
|
+
if digest != expected:
|
|
29
|
+
raise ValueError(f"Empreinte LQL-Equiv incorrecte : {relative}.")
|
|
30
|
+
# The library uses importlib.resources on lqlequiv.data. Reject an unrelated
|
|
31
|
+
# installed package instead of silently executing it.
|
|
32
|
+
existing = sys.modules.get("lqlequiv")
|
|
33
|
+
if existing is not None:
|
|
34
|
+
if Path(existing.__file__).resolve() != (root / "__init__.py").resolve():
|
|
35
|
+
raise ValueError("Un autre module lqlequiv est déjà chargé ; redémarrez Python.")
|
|
36
|
+
return existing
|
|
37
|
+
spec = importlib.util.spec_from_file_location(
|
|
38
|
+
"lqlequiv", root / "__init__.py", submodule_search_locations=[str(root)]
|
|
39
|
+
)
|
|
40
|
+
module = importlib.util.module_from_spec(spec)
|
|
41
|
+
sys.modules[spec.name] = module
|
|
42
|
+
try:
|
|
43
|
+
spec.loader.exec_module(module)
|
|
44
|
+
except Exception:
|
|
45
|
+
for key in list(sys.modules):
|
|
46
|
+
if key == "lqlequiv" or key.startswith("lqlequiv."):
|
|
47
|
+
del sys.modules[key]
|
|
48
|
+
raise
|
|
49
|
+
return module
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(scenario, descriptor, workspace, output_root=None):
|
|
53
|
+
"""Return native qualified indicators; never recommend a clinical dose."""
|
|
54
|
+
gate = validate(scenario, descriptor)
|
|
55
|
+
if gate.decision != "execute":
|
|
56
|
+
raise ValueError("Scénario incomplet, non accepté ou hors du profil LQL fictif.")
|
|
57
|
+
config = {name: item.value for name, item in scenario.inputs.items()}
|
|
58
|
+
if scenario.experiment.get("scenario_scope").value != "fictitious":
|
|
59
|
+
raise ValueError("Le profil accepte uniquement des scénarios fictifs de recherche.")
|
|
60
|
+
module = _backend(workspace, descriptor)
|
|
61
|
+
library = module.load_library()
|
|
62
|
+
organ = library.organ(config["organ"])
|
|
63
|
+
tumour = library.tumour_site(config["tumour_site"])
|
|
64
|
+
course = module.Course(config["dose_per_fraction"], config["n_fractions"], config["gap_days"])
|
|
65
|
+
prescription = module.Prescription(
|
|
66
|
+
courses=(course,), reference_dose=config["reference_dose"],
|
|
67
|
+
bifractionated=config["bifractionated"] == "yes",
|
|
68
|
+
)
|
|
69
|
+
result = module.compute(organ, tumour, prescription, options=module.Options())
|
|
70
|
+
native = result.courses[0]
|
|
71
|
+
oar_ok = result.oar_total_valid and not native.oar_saturated
|
|
72
|
+
tumour_ok = result.tumour_total_valid and not native.tumour_saturated
|
|
73
|
+
values = {
|
|
74
|
+
"physical_dose_gy": course.total_dose,
|
|
75
|
+
"bed_oar": native.bed_oar if result.oar_total_valid else None,
|
|
76
|
+
"bed_tumour": native.bed_tumour if result.tumour_total_valid else None,
|
|
77
|
+
"eqd_oar_total": result.eqd_oar_total if oar_ok else None,
|
|
78
|
+
"eqd_tumour_total": result.eqd_tumour_total if tumour_ok else None,
|
|
79
|
+
"ntcp_percent": result.ntcp_percent if oar_ok else None,
|
|
80
|
+
"tcp_percent": result.tcp_percent if tumour_ok else None,
|
|
81
|
+
"cancer_risk": result.cancer_risk if oar_ok else None,
|
|
82
|
+
"overall_days_oar": native.overall_days_oar,
|
|
83
|
+
"overall_days_tumour": native.overall_days_tumour,
|
|
84
|
+
}
|
|
85
|
+
if any(value is not None and not math.isfinite(value) for value in values.values()):
|
|
86
|
+
raise ValueError("Sortie LQL-Equiv non finie ; résultat refusé.")
|
|
87
|
+
flags = {
|
|
88
|
+
"oar_total_valid": result.oar_total_valid, "tumour_total_valid": result.tumour_total_valid,
|
|
89
|
+
"oar_saturated": native.oar_saturated, "tumour_saturated": native.tumour_saturated,
|
|
90
|
+
}
|
|
91
|
+
target = (Path(output_root) if output_root else Path(workspace) / "runs") / (
|
|
92
|
+
datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + uuid.uuid4().hex[:8]
|
|
93
|
+
)
|
|
94
|
+
target.mkdir(parents=True)
|
|
95
|
+
row = {**values, **flags}
|
|
96
|
+
with (target / "indicators.csv").open("w", newline="", encoding="utf-8") as handle:
|
|
97
|
+
writer = csv.DictWriter(handle, fieldnames=list(row))
|
|
98
|
+
writer.writeheader()
|
|
99
|
+
writer.writerow(row)
|
|
100
|
+
qualification = {"tables": {"indicators": {
|
|
101
|
+
"file": "indicators.csv", "aggregation": "Un calcul déterministe, un seul cursus fictif.",
|
|
102
|
+
"columns": {name: {"unit": spec.get("display_unit"), "meaning": spec["definition"]}
|
|
103
|
+
for name, spec in descriptor["outputs"].items()},
|
|
104
|
+
}}}
|
|
105
|
+
manifest = {
|
|
106
|
+
"software": descriptor["software"], "scenario": asdict(scenario),
|
|
107
|
+
"nature": descriptor["nature"], "validity_notes": descriptor["validity_notes"],
|
|
108
|
+
"source_verification": {"method": "sha256_lf", "commit": EXPECTED_COMMIT,
|
|
109
|
+
"files": EXPECTED_FILES},
|
|
110
|
+
"backend_options": asdict(result.options), "tissues": {"organ": asdict(organ), "tumour": asdict(tumour)},
|
|
111
|
+
"flags": flags, "output_qualification": qualification,
|
|
112
|
+
"null_policy": "EQD et probabilités dépendantes supprimées si invalides ou à la borne ; coefficient absent = null.",
|
|
113
|
+
}
|
|
114
|
+
(target / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2,
|
|
115
|
+
allow_nan=False), encoding="utf-8")
|
|
116
|
+
(target / "scenario.json").write_text(json.dumps(asdict(scenario), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
117
|
+
export_semantics(scenario, qualification, target, descriptor)
|
|
118
|
+
return target, values
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Read-only pyrcel 2.0.0 adapter: constant updraft, one lognormal mode."""
|
|
2
|
+
import hashlib
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from websemantic.core.validation import validate
|
|
13
|
+
from websemantic.semantics import export_semantics
|
|
14
|
+
|
|
15
|
+
COMMIT = "977e9094644ec4a17d658d657094651928591c89"
|
|
16
|
+
SOURCE_HASHES = {'pyrcel/aerosol.py': 'd7fc5c1d055a5d6868aae53430a7d1c0797ff4c8f73caff387aef5900fcfc9c4', 'pyrcel/console_report.py': '2f64b98e0bda566582e46a6938c142d1aed361f9f3503f17864b5f153a98bf6e', 'pyrcel/constants.py': '7d75523d46874bb1faeaff3191f7effe27dc8e6e7866976dd72dc7b6d5df52d8', 'pyrcel/distributions.py': '712f41b5e72f54c3cb243d6e7708f8cb014c9f897f97a322ffac1ba832f1eb2e', 'pyrcel/ensemble.py': '856d97c73873061dfeae77c69db5da23d3b15019864808a9180ec6dd66e2641c', 'pyrcel/equilibrate.py': 'c85e21765e25589279d438f405b92bffabeac9115b290bfb76a3f3037cc0ffe7', 'pyrcel/integrator.py': 'd08876ea45151dd0fc2119aec2d4964dc0028087a904da4d7d006723b3e85992', 'pyrcel/model.py': '6c4cf795c682e4acb4b6531b4cd1fc3bf6b0689df9d046625862f0b8ef1ad5f0', 'pyrcel/model_output.py': '7214480542a6248ee914cd0e5709fd5223faf65f022bee3d6eea147ec0f64a3d', 'pyrcel/output.py': 'd7e543ed4d73fd853421f7106b0503dd3991c93a90082adfb180d2935514c472', 'pyrcel/parcel_aux.py': '07d56e3854d16acf309666d1444136fb1c52d5652d29442482a2047eadc91735', 'pyrcel/thermo.py': '779d453484e25b708afda3517f9d8f1303dd40c5ee1ead77c9ac01119cdccd80', 'pyrcel/updraft.py': 'f299171a7da17bbf206fe82b8a15a137e84265c2268b5489d32570e0f5a499ba', 'pyrcel/util.py': '7a1e64350e830870a5582822d2c93518cf18638e723fdc3333299855760db352', 'pyrcel/_types.py': '8e72187aa2c824f7adc4c784dcad676fc082b0487a422809a4619236eeb4561a', 'pyrcel/__init__.py': 'e4a30cbc78b63ef61c5a32f852a27362096c60a97f82c075b290a85acf050639', 'pyrcel/activation/_arg2000.py': 'e107a9c8ac418b09a9e6717bf14d108280ed02c2fc0ac159403cfb45d15f2ead', 'pyrcel/activation/_common.py': '383585cd6c00f2a9a7b940e3b7fa539132aba0981ac67db5f8ad061ec374514c', 'pyrcel/activation/_mbn2014.py': 'dd96adaa9c0282324b072dcf9f51dcac72507c75c0bb85d3cb7ee3f7a9f1a79d', 'pyrcel/activation/_scheme.py': 'f3976646eba72b0b81c4538be92c92fed60c39e4ec7a05bab6e204b9174254c7', 'pyrcel/activation/__init__.py': '9dd0d8a8e2246f6cc8e7aba01bc493c2cad47c9144752908f9a1e251a207cfd2', 'pyrcel/legacy/activation.py': 'f1e283f36d95ae373f705245f46d9beffebaed071a2e4e5ea8af1b81c8b4145d', 'pyrcel/legacy/postprocess.py': '4b763e13f904cd5c0662fbd43dc52e77acbedee61a6aaf9e7d19962216653dbc', 'pyrcel/legacy/thermo.py': '94e21fd06e573ebbadcf7e0ff5b2831f4e06b08f8e02ae185aeaae89e2c61a32', 'pyrcel/legacy/vis.py': '932c8ded03dcf9daeea05a6f9a5c8021841204d92afa32e1dcf85042e9b58a0d', 'pyrcel/legacy/__init__.py': '21ec0430ebb1bfdeef5e73bdf1f4227c23d83bc4d460b94a4f658f23bd45d34a', 'pyrcel/scripts/run_parcel.py': 'd8a45a5f535f469cd8ec6d9bfbc645bd3d28bc1dfc0cdc1f4496a1ccd5d40f4d', 'pyrcel/scripts/__init__.py': 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'pyrcel/test/generate_data.py': '5fcb1e9f4840c56e4834bbdfa8b3bf25dcd1fe09990f5e24998f3fa3a86165a7', 'pyrcel/test/pm_test.py': 'f0733f17aadccb1620e7c453c46647f9588c39bd7d2f5e79c165ad6671934ad8', 'pyrcel/test/test_thermo.py': '3f6f0bcb7ccec593246c5578fde57c3bcacc63729b3b1bc859da453c2f3b54d3', 'pyrcel/test/__init__.py': 'aedf1b7eccb3cfb02226205f82309a1290079ce31ed5204325781ee2bf368fc7'}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify_source(backend):
|
|
20
|
+
"""Verify every Python module in the reviewed package, without requiring Git."""
|
|
21
|
+
actual = {str(p.relative_to(backend)).replace("\\", "/") for p in (backend / "pyrcel").rglob("*.py")}
|
|
22
|
+
if actual != set(SOURCE_HASHES):
|
|
23
|
+
raise ValueError("Modules pyrcel inattendus ou manquants; relancez Installer.")
|
|
24
|
+
for name, expected in SOURCE_HASHES.items():
|
|
25
|
+
digest = hashlib.sha256((backend / name).read_bytes().replace(b"\r\n", b"\n")).hexdigest()
|
|
26
|
+
if digest != expected:
|
|
27
|
+
raise ValueError("Code pyrcel modifié : " + name)
|
|
28
|
+
return {"method": "sha256_lf", "commit": COMMIT, "files": SOURCE_HASHES}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def run(scenario, descriptor, workspace, output_root=None):
|
|
32
|
+
gate = validate(scenario, descriptor)
|
|
33
|
+
if gate.decision != "execute":
|
|
34
|
+
raise ValueError("Paramètres incomplets ou hypothèses non acceptées : " + "; ".join(i.field + ": " + i.code for i in gate.issues))
|
|
35
|
+
if descriptor["software"]["commit"] != COMMIT:
|
|
36
|
+
raise ValueError("Révision pyrcel incorrecte.")
|
|
37
|
+
params = {k: v.value for k, v in scenario.inputs.items()}
|
|
38
|
+
experiment = {k: v.value for k, v in scenario.experiment.items()}
|
|
39
|
+
if experiment["output_dt"] > experiment["t_end"] or experiment["t_end"] / experiment["output_dt"] > 100000:
|
|
40
|
+
raise ValueError("Pas de sortie incompatible avec la durée ou expérience trop volumineuse.")
|
|
41
|
+
workspace = Path(workspace)
|
|
42
|
+
backend = workspace / "external/pyrcel"
|
|
43
|
+
if not backend.is_dir():
|
|
44
|
+
backend = workspace / "work/candidates/pyrcel"
|
|
45
|
+
verification = verify_source(backend)
|
|
46
|
+
# Must configure before importing JAX; refuse a previously initialised incompatible runtime.
|
|
47
|
+
os.environ["JAX_ENABLE_X64"] = "true"
|
|
48
|
+
os.environ["JAX_PLATFORMS"] = "cpu"
|
|
49
|
+
try:
|
|
50
|
+
import jax
|
|
51
|
+
importlib.import_module("diffrax") # required solver; checked before source import
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
raise ValueError("Composants pyrcel manquants; lancez Installer.cmd pour installer le profil atmosphérique.") from exc
|
|
54
|
+
jax.config.update("jax_enable_x64", True)
|
|
55
|
+
if any(device.platform != "cpu" for device in jax.devices()):
|
|
56
|
+
raise ValueError("Ce profil nécessite JAX CPU.")
|
|
57
|
+
sys.path.insert(0, str(backend))
|
|
58
|
+
try:
|
|
59
|
+
import pyrcel as pm
|
|
60
|
+
if Path(pm.__file__).resolve() != (backend / "pyrcel/__init__.py").resolve():
|
|
61
|
+
raise ValueError("Un autre module pyrcel est déjà chargé; ouvrez une nouvelle session.")
|
|
62
|
+
import numpy as np
|
|
63
|
+
aerosol = pm.AerosolSpecies("mode1", pm.Lognorm(mu=params["mu"], sigma=params["sigma"], N=params["N"]), kappa=params["kappa"], bins=params["bins"])
|
|
64
|
+
model = pm.ParcelModel([aerosol], V=params["V"], T0=params["T0"], P0=params["P0"], S0=params["S0"], accom=params["accom"], console=False)
|
|
65
|
+
output = model.run(experiment["t_end"], output_dt=experiment["output_dt"], terminate=experiment["terminate"] == "yes", terminate_depth=experiment["terminate_depth"])
|
|
66
|
+
except ImportError as exc:
|
|
67
|
+
raise ValueError("Composants pyrcel manquants; lancez Installer.cmd pour installer le profil atmosphérique.") from exc
|
|
68
|
+
finally:
|
|
69
|
+
sys.path.remove(str(backend))
|
|
70
|
+
if not np.isfinite(output.state).all() or not np.isfinite(output.time).all():
|
|
71
|
+
raise ValueError("Sortie pyrcel non finie; résultat refusé.")
|
|
72
|
+
if len(output.time) < 2 or np.any(np.diff(output.time) <= 0):
|
|
73
|
+
raise ValueError("Support temporel invalide.")
|
|
74
|
+
frame, aerosol_frames = output.to_pandas()
|
|
75
|
+
frame = frame.reset_index()
|
|
76
|
+
frame.rename(columns={frame.columns[0]: "time_s"}, inplace=True)
|
|
77
|
+
if not np.array_equal(frame["time_s"].to_numpy(), output.time):
|
|
78
|
+
raise ValueError("Le support temporel natif n’a pas été conservé.")
|
|
79
|
+
metrics = {"maximum_supersaturation_percent": float(output.summary["S_max"]) * 100, "activated_number_cm3": float(output.Nd) / 1e6, "activated_fraction": float(output.nd_frac), "actual_end_time_s": float(output.time[-1])}
|
|
80
|
+
if not all(np.isfinite(v) for v in metrics.values()):
|
|
81
|
+
raise ValueError("Indicateurs pyrcel non finis.")
|
|
82
|
+
target = (Path(output_root) if output_root else workspace / "runs") / (datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + "-" + uuid.uuid4().hex[:8])
|
|
83
|
+
target.mkdir(parents=True)
|
|
84
|
+
frame.to_csv(target / "trajectory.csv", index=False)
|
|
85
|
+
wet = aerosol_frames["mode1"].reset_index()
|
|
86
|
+
wet.rename(columns={wet.columns[0]: "time_s"}, inplace=True)
|
|
87
|
+
wet.to_csv(target / "wet_radii.csv", index=False)
|
|
88
|
+
import pandas as pd
|
|
89
|
+
pd.DataFrame([metrics]).to_csv(target / "kpis.csv", index=False)
|
|
90
|
+
qualification = {"trajectory": {"units": {"time_s": "s", "z": "m", "P": "Pa", "T": "K", "wv": "kg/kg", "wc": "kg/kg", "wi": "kg/kg", "S": "1"}, "temporal_support": "Exact native timestamps; nominal output_dt; final interval may be irregular"}, "wet_radii": {"time_unit": "s", "all_radius_columns_unit": "m", "definition": "Wet particle radius per size bin; not dry diameter"}, "kpis": {"maximum_supersaturation_percent": {"unit": "%", "definition": "Native solver peak locator; may differ from sampled CSV maximum"}, "activated_number_cm3": {"unit": "cm^-3", "definition": "Native hard-activation count at actual final output time"}, "activated_fraction": {"unit": "1", "definition": "Native activation fraction at actual final time"}, "actual_end_time_s": {"unit": "s", "definition": "Actual end; t_end is a maximum"}}}
|
|
91
|
+
qualification['tables'] = {
|
|
92
|
+
'trajectory': {'file': 'trajectory.csv', 'aggregation': 'Pas natif exact, dernier intervalle potentiellement irrégulier.',
|
|
93
|
+
'columns': {name: {'unit': unit, 'meaning': name + ' : sortie native de la parcelle idéale.'} for name, unit in qualification['trajectory']['units'].items()}},
|
|
94
|
+
'wet_radii': {'file': 'wet_radii.csv', 'aggregation': 'Rayons humides par classe aux instants natifs.',
|
|
95
|
+
'columns': {name: {'unit': 's' if name == 'time_s' else 'm', 'meaning': 'Temps natif' if name == 'time_s' else 'Rayon humide, pas diamètre sec'} for name in wet.columns}},
|
|
96
|
+
'kpis': {'file': 'kpis.csv', 'aggregation': 'Indicateurs natifs ; activation à la fin effective.',
|
|
97
|
+
'columns': {name: {'unit': spec['unit'], 'meaning': spec['definition']} for name, spec in qualification['kpis'].items()}}
|
|
98
|
+
}
|
|
99
|
+
export_semantics(scenario, qualification, target, descriptor)
|
|
100
|
+
versions = {name: importlib.metadata.version(name) for name in ("jax", "jaxlib", "diffrax", "equinox", "optimistix", "numpy", "pandas", "scipy")}
|
|
101
|
+
manifest = {"software": descriptor["software"], "source_verification": verification, "scenario": asdict(scenario), "nature": descriptor["nature"], "validity_notes": descriptor["validity_notes"], "output_qualification": qualification, "execution": {"python": sys.version, "versions": versions, "jax_backend": "cpu", "jax_enable_x64": bool(jax.config.jax_enable_x64), "requested_maximum_duration_s": experiment["t_end"], "actual_duration_s": metrics["actual_end_time_s"]}}
|
|
102
|
+
(target / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2, allow_nan=False), encoding="utf8")
|
|
103
|
+
(target / "scenario.json").write_text(json.dumps(asdict(scenario), ensure_ascii=False, indent=2, allow_nan=False), encoding="utf8")
|
|
104
|
+
return target, metrics
|