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.
Files changed (73) hide show
  1. websemantic-0.2.5/LICENSE +135 -0
  2. websemantic-0.2.5/PKG-INFO +64 -0
  3. websemantic-0.2.5/README.md +65 -0
  4. websemantic-0.2.5/docs/pypi.md +15 -0
  5. websemantic-0.2.5/pyproject.toml +56 -0
  6. websemantic-0.2.5/setup.cfg +4 -0
  7. websemantic-0.2.5/src/websemantic/__init__.py +3 -0
  8. websemantic-0.2.5/src/websemantic/adapters/__init__.py +1 -0
  9. websemantic-0.2.5/src/websemantic/adapters/lql.py +118 -0
  10. websemantic-0.2.5/src/websemantic/adapters/pyrcel.py +104 -0
  11. websemantic-0.2.5/src/websemantic/adapters/tls.py +130 -0
  12. websemantic-0.2.5/src/websemantic/adapters/tls_outputs.py +70 -0
  13. websemantic-0.2.5/src/websemantic/cli.py +592 -0
  14. websemantic-0.2.5/src/websemantic/comparison.py +223 -0
  15. websemantic-0.2.5/src/websemantic/conversation.py +44 -0
  16. websemantic-0.2.5/src/websemantic/core/__init__.py +1 -0
  17. websemantic-0.2.5/src/websemantic/core/validation.py +172 -0
  18. websemantic-0.2.5/src/websemantic/environments.json +23 -0
  19. websemantic-0.2.5/src/websemantic/gemini.py +222 -0
  20. websemantic-0.2.5/src/websemantic/geography.py +223 -0
  21. websemantic-0.2.5/src/websemantic/lql_comparison.py +138 -0
  22. websemantic-0.2.5/src/websemantic/qualitative.py +60 -0
  23. websemantic-0.2.5/src/websemantic/registry.py +45 -0
  24. websemantic-0.2.5/src/websemantic/replay.py +68 -0
  25. websemantic-0.2.5/src/websemantic/semantics.py +266 -0
  26. websemantic-0.2.5/src/websemantic/session.py +271 -0
  27. websemantic-0.2.5/src/websemantic/units.py +92 -0
  28. websemantic-0.2.5/src/websemantic/web_research.py +146 -0
  29. websemantic-0.2.5/src/websemantic.egg-info/PKG-INFO +64 -0
  30. websemantic-0.2.5/src/websemantic.egg-info/SOURCES.txt +71 -0
  31. websemantic-0.2.5/src/websemantic.egg-info/dependency_links.txt +1 -0
  32. websemantic-0.2.5/src/websemantic.egg-info/entry_points.txt +2 -0
  33. websemantic-0.2.5/src/websemantic.egg-info/requires.txt +33 -0
  34. websemantic-0.2.5/src/websemantic.egg-info/top_level.txt +1 -0
  35. websemantic-0.2.5/tests/test_affine_units.py +22 -0
  36. websemantic-0.2.5/tests/test_agent_common.py +55 -0
  37. websemantic-0.2.5/tests/test_agent_portable.py +149 -0
  38. websemantic-0.2.5/tests/test_automatic_results.py +62 -0
  39. websemantic-0.2.5/tests/test_backends_smoke.py +36 -0
  40. websemantic-0.2.5/tests/test_comparison.py +210 -0
  41. websemantic-0.2.5/tests/test_core_ontology.py +176 -0
  42. websemantic-0.2.5/tests/test_daily_outputs.py +32 -0
  43. websemantic-0.2.5/tests/test_descriptor_transfer.py +156 -0
  44. websemantic-0.2.5/tests/test_e1_candidate.py +49 -0
  45. websemantic-0.2.5/tests/test_e1_review_regressions.py +47 -0
  46. websemantic-0.2.5/tests/test_e1_scoring.py +43 -0
  47. websemantic-0.2.5/tests/test_e1_scoring_v2.py +124 -0
  48. websemantic-0.2.5/tests/test_e3_bench.py +155 -0
  49. websemantic-0.2.5/tests/test_environment_menu.py +26 -0
  50. websemantic-0.2.5/tests/test_geography.py +79 -0
  51. websemantic-0.2.5/tests/test_guided_study.py +98 -0
  52. websemantic-0.2.5/tests/test_installation.py +55 -0
  53. websemantic-0.2.5/tests/test_length_controls.py +63 -0
  54. websemantic-0.2.5/tests/test_lql_adapter.py +127 -0
  55. websemantic-0.2.5/tests/test_lql_comparison.py +87 -0
  56. websemantic-0.2.5/tests/test_natural_conversation.py +71 -0
  57. websemantic-0.2.5/tests/test_number_words.py +41 -0
  58. websemantic-0.2.5/tests/test_operational_seed.py +47 -0
  59. websemantic-0.2.5/tests/test_output_contract_links.py +22 -0
  60. websemantic-0.2.5/tests/test_private_key.py +26 -0
  61. websemantic-0.2.5/tests/test_profile_acceptance_safety.py +110 -0
  62. websemantic-0.2.5/tests/test_published_contract.py +89 -0
  63. websemantic-0.2.5/tests/test_pyrcel_adapter.py +108 -0
  64. websemantic-0.2.5/tests/test_qualitative.py +100 -0
  65. websemantic-0.2.5/tests/test_semantic_pack.py +56 -0
  66. websemantic-0.2.5/tests/test_semantic_structure.py +54 -0
  67. websemantic-0.2.5/tests/test_session.py +136 -0
  68. websemantic-0.2.5/tests/test_shapes.py +136 -0
  69. websemantic-0.2.5/tests/test_unit_labels.py +48 -0
  70. websemantic-0.2.5/tests/test_validate_answers.py +94 -0
  71. websemantic-0.2.5/tests/test_validation.py +159 -0
  72. websemantic-0.2.5/tests/test_web_research.py +90 -0
  73. 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
+ [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.23238902.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """Software-agnostic semantic layer from intent to qualified simulated data (research prototype)."""
2
+
3
+ __version__ = "0.0.1"
@@ -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