Affected Component: Study loading / model resolution (study/) — specifically src/gems_craft/model/ and src/gems_craft/libs/
Type of Change: Minor — backward-compatible refactor (deprecation shim retained); no change to GEMS syntax, no change to solver results
Current version: 0.1.3
Description
Two independent layout problems in gems_craft make the package harder to read than it needs to be. They are worth fixing together because they touch the same files.
Problem 1 — src/gems_craft/libs/ is dead code
The package contains exactly one file, __init__.py, whose entire payload is:
lib_path = Path(__file__).parent
Its stated purpose (AGENTS.md:97) is to "resolve the path to bundled YAML model libraries shipped with the package". Evidence that it no longer does anything:
- No bundled libraries exist.
find src/gems_craft/libs -type f returns only __init__.py.
- Zero importers.
grep -rn "lib_path\|gems_craft\.libs" src/ tests/ docs/ matches only the definition itself.
- The libraries were left behind in the package split. Git history shows the pre-split
src/gems/libs/ did ship antares_historic.yml, pypsa_models.yml and andromede_v1_models.yml. Commit 0dcfc40 ("Split gems into gems_craft/gems_runner") carried only __init__.py across to gems_craft/. Those YAMLs now live under tests/e2e/models/.
- They could not ship even if restored.
pyproject.toml declares no package-data and no include-package-data, and there is no MANIFEST.in, so non-.py files under src/ are not packaged into the wheel.
Consequences: a misleading empty package at the top level of gems_craft, a stale line in AGENTS.md, and a stale libs/ entry in the "Affected Component" dropdown of .github/ISSUE_TEMPLATE/gp-02.yml.
Problem 2 — src/gems_craft/model/ is named after one element of the file it actually owns
The folder is not organized around the Model class. It is a one-to-one mirror of a model library YAML file:
library:
id: simple_models # → library.py
dependencies: [...] # → resolve_library.py
taxonomy: ... # → taxonomy.py
port-types: # → port.py (library-scoped, shared across models)
- id: flow
models: # → model.py + parameter/variable/constraint
- id: bus
This causes two recurring points of confusion:
(a) Library, PortType and Taxonomy look like strays. They are not model parts — they are library-level declarations:
PortType is declared at library level and shared by many models. It is also the only thing that crosses library boundaries: the dependency-resolution machinery in resolve_library.py (_add_preloaded_port_types_to_current_lib, _add_resolved_dependent_port_types_to_current_lib, lines 136–150) propagates port_types and nothing else. Models are never merged across libraries — they are namespaced library_id.model_id (resolve_library.py:378) and stay put.
taxonomy.py imports only LibrarySchema/ModelSchema from parsing.py. It never touches Model, Parameter, Variable or any resolved object.
Every other package in gems_craft is named after the input artifact it owns — expression/ for Expr.g4 expressions, optim_config/ for optim-config.yml, study/ for the study directory. model/ is the odd one out.
(b) "model" collides with pydantic's meaning. model/parsing.py contains ModelSchema (a pydantic model describing a GEMS model) next to model_config (pure pydantic API, parsing.py:37). The disambiguating convention already exists and is applied consistently — *Schema = pydantic, everything else = GEMS — but it is nowhere written down, and the directory layout does not reflect it.
Note that (a) and (b) are separate: renaming the folder does not fix (b), because parsing.py would still hold ModelSchema alongside model_config.
Proposed change
Step 1 — Remove the dead libs/ package
- Delete
src/gems_craft/libs/.
- Remove the
gems_craft/libs/ entry from AGENTS.md:97.
- Update the
libs/ option in .github/ISSUE_TEMPLATE/gp-02.yml to point at the new location.
Decision required before implementing. If bundled model libraries are intended to ship with GemsPy again, the correct fix is the opposite: repopulate libs/, add [tool.setuptools.package-data] so the YAMLs actually land in the wheel, and keep the package. This issue assumes they are not coming back. Please confirm.
Step 2 — Rename model/ → library/ and isolate the pydantic schemas
gems_craft/library/
├── schema/ ← 100% pydantic; every public name ends in Schema
│ ├── parsing.py
│ └── taxonomy.py
├── resolve_library.py ← the bridge: schema → domain (only file importing both)
├── library.py model.py port.py ← library-level + model-level domain objects
└── parameter.py variable.py constraint.py common.py
The schema/ boundary follows a seam that already exists in the import graph: parsing.py imports nothing from the folder, and taxonomy.py imports only parsing.py. Neither touches a domain object.
Steps 1 and 2 are deliberately bundled: Step 1 frees the name library/ (currently shadowed by libs/), and the import churn — 146 lines across 55 files (15 in src/, 36 in tests/, 4 in docs/) — is paid once instead of twice.
Step 3 — Backward compatibility
gems_craft is explicitly documented as standalone-importable for an API layer, so gems_craft/model/__init__.py is retained for one release as a deprecation shim re-exporting from gems_craft.library, emitting DeprecationWarning. Removing the shim in a later release is a Major change and needs its own issue.
Step 4 — Small correctness fixes in the same area
resolve_library.py:59 and :62 import model twice (via gems_craft.model and gems_craft.model.model); the second silently overwrites the first with the same object. Drop one.
model.py:124 — the parameter model: "Model" shadows the module-level factory model() defined 88 lines below. Rename the parameter.
- Add a module docstring to the package
__init__.py stating the convention explicitly: *Schema = pydantic mirror of the YAML (strings, no semantics); everything else = resolved GEMS domain object; model_config / model_validate / model_dump are pydantic API and unrelated to GEMS models.
Out of scope
check_library_against_taxonomy is currently unreachable from the loading pipeline — load_study never calls it, only tests/unittests/gems_craft/lib_parsing/test_taxonomy_check.py does. Deciding whether to wire it into load_study or to document it as a deliberately opt-in validation API is a separate question and should get its own issue.
Results Impact
No change to solver output values is expected or intended.
This is a pure module-layout change: file moves, import-path updates, one duplicate import removed, one local parameter renamed, and documentation. No expression parsing, constraint construction, indexing, or optimization logic is touched. No behavior is added, removed, or reordered.
This must be confirmed via tests, not assumed — see Validation Strategy.
Validation Strategy
Because no behavior should change, validation is about proving equivalence rather than checking new expected values.
- Solver equivalence (primary): the full E2E suite under
tests/e2e/ must pass unchanged, with no reference study expectations edited. Any diff to an expected-results file in this PR is a red flag and must be justified or reverted.
- Unit tests: the
tests/unittests/gems_craft/ suite covers library parsing, resolution and taxonomy checking (lib_parsing/test_taxonomy_check.py among them) and must pass with import paths updated only.
- Deprecation shim: add a test asserting that
from gems_craft.model import Model, model, PortType still resolves to the same objects as gems_craft.library, and emits DeprecationWarning.
- Static checks:
mypy is configured with disallow_untyped_defs over packages = ["gems_craft", "gems_craft_hybrid", "gems_runner"] and will catch any import path missed by the rename. black and isort --profile black must pass.
- Dead-code claim: re-run
grep -rn "lib_path\|gems_craft\.libs" src/ tests/ docs/ at review time to confirm libs/ still has no importers before deleting it.
- Packaging: build the wheel and confirm
gems_craft.library and gems_craft.library.schema are both present ([tool.setuptools.packages.find] uses where = ["src"] auto-discovery, so new subpackages are picked up automatically — worth verifying rather than assuming).
Process Checklist
Step 1 — Issue Creation
Step 2 — Triage
Step 3 — Impact Analysis ⚠️ extended results analysis required
Step 4 — Implementation
Step 5 — Testing & Validation
Step 6 — CI Validation
Step 7 — Review & Merge
Step 8 — Versioning
Step 9 — Supporting Files
Step 10 — Release
Affected Component: Study loading / model resolution (
study/) — specificallysrc/gems_craft/model/andsrc/gems_craft/libs/Type of Change: Minor — backward-compatible refactor (deprecation shim retained); no change to GEMS syntax, no change to solver results
Current version: 0.1.3
Description
Two independent layout problems in
gems_craftmake the package harder to read than it needs to be. They are worth fixing together because they touch the same files.Problem 1 —
src/gems_craft/libs/is dead codeThe package contains exactly one file,
__init__.py, whose entire payload is:Its stated purpose (
AGENTS.md:97) is to "resolve the path to bundled YAML model libraries shipped with the package". Evidence that it no longer does anything:find src/gems_craft/libs -type freturns only__init__.py.grep -rn "lib_path\|gems_craft\.libs" src/ tests/ docs/matches only the definition itself.src/gems/libs/did shipantares_historic.yml,pypsa_models.ymlandandromede_v1_models.yml. Commit0dcfc40("Split gems into gems_craft/gems_runner") carried only__init__.pyacross togems_craft/. Those YAMLs now live undertests/e2e/models/.pyproject.tomldeclares nopackage-dataand noinclude-package-data, and there is noMANIFEST.in, so non-.pyfiles undersrc/are not packaged into the wheel.Consequences: a misleading empty package at the top level of
gems_craft, a stale line inAGENTS.md, and a stalelibs/entry in the "Affected Component" dropdown of.github/ISSUE_TEMPLATE/gp-02.yml.Problem 2 —
src/gems_craft/model/is named after one element of the file it actually ownsThe folder is not organized around the
Modelclass. It is a one-to-one mirror of a model library YAML file:This causes two recurring points of confusion:
(a)
Library,PortTypeandTaxonomylook like strays. They are not model parts — they are library-level declarations:PortTypeis declared at library level and shared by many models. It is also the only thing that crosses library boundaries: the dependency-resolution machinery inresolve_library.py(_add_preloaded_port_types_to_current_lib,_add_resolved_dependent_port_types_to_current_lib, lines 136–150) propagatesport_typesand nothing else. Models are never merged across libraries — they are namespacedlibrary_id.model_id(resolve_library.py:378) and stay put.taxonomy.pyimports onlyLibrarySchema/ModelSchemafromparsing.py. It never touchesModel,Parameter,Variableor any resolved object.Every other package in
gems_craftis named after the input artifact it owns —expression/forExpr.g4expressions,optim_config/foroptim-config.yml,study/for the study directory.model/is the odd one out.(b) "model" collides with pydantic's meaning.
model/parsing.pycontainsModelSchema(a pydantic model describing a GEMS model) next tomodel_config(pure pydantic API,parsing.py:37). The disambiguating convention already exists and is applied consistently —*Schema= pydantic, everything else = GEMS — but it is nowhere written down, and the directory layout does not reflect it.Note that (a) and (b) are separate: renaming the folder does not fix (b), because
parsing.pywould still holdModelSchemaalongsidemodel_config.Proposed change
Step 1 — Remove the dead
libs/packagesrc/gems_craft/libs/.gems_craft/libs/entry fromAGENTS.md:97.libs/option in.github/ISSUE_TEMPLATE/gp-02.ymlto point at the new location.Step 2 — Rename
model/→library/and isolate the pydantic schemasThe
schema/boundary follows a seam that already exists in the import graph:parsing.pyimports nothing from the folder, andtaxonomy.pyimports onlyparsing.py. Neither touches a domain object.Steps 1 and 2 are deliberately bundled: Step 1 frees the name
library/(currently shadowed bylibs/), and the import churn — 146 lines across 55 files (15 insrc/, 36 intests/, 4 indocs/) — is paid once instead of twice.Step 3 — Backward compatibility
gems_craftis explicitly documented as standalone-importable for an API layer, sogems_craft/model/__init__.pyis retained for one release as a deprecation shim re-exporting fromgems_craft.library, emittingDeprecationWarning. Removing the shim in a later release is a Major change and needs its own issue.Step 4 — Small correctness fixes in the same area
resolve_library.py:59and:62importmodeltwice (viagems_craft.modelandgems_craft.model.model); the second silently overwrites the first with the same object. Drop one.model.py:124— the parametermodel: "Model"shadows the module-level factorymodel()defined 88 lines below. Rename the parameter.__init__.pystating the convention explicitly:*Schema= pydantic mirror of the YAML (strings, no semantics); everything else = resolved GEMS domain object;model_config/model_validate/model_dumpare pydantic API and unrelated to GEMS models.Out of scope
check_library_against_taxonomyis currently unreachable from the loading pipeline —load_studynever calls it, onlytests/unittests/gems_craft/lib_parsing/test_taxonomy_check.pydoes. Deciding whether to wire it intoload_studyor to document it as a deliberately opt-in validation API is a separate question and should get its own issue.Results Impact
No change to solver output values is expected or intended.
This is a pure module-layout change: file moves, import-path updates, one duplicate import removed, one local parameter renamed, and documentation. No expression parsing, constraint construction, indexing, or optimization logic is touched. No behavior is added, removed, or reordered.
This must be confirmed via tests, not assumed — see Validation Strategy.
Validation Strategy
Because no behavior should change, validation is about proving equivalence rather than checking new expected values.
tests/e2e/must pass unchanged, with no reference study expectations edited. Any diff to an expected-results file in this PR is a red flag and must be justified or reverted.tests/unittests/gems_craft/suite covers library parsing, resolution and taxonomy checking (lib_parsing/test_taxonomy_check.pyamong them) and must pass with import paths updated only.from gems_craft.model import Model, model, PortTypestill resolves to the same objects asgems_craft.library, and emitsDeprecationWarning.mypyis configured withdisallow_untyped_defsoverpackages = ["gems_craft", "gems_craft_hybrid", "gems_runner"]and will catch any import path missed by the rename.blackandisort --profile blackmust pass.grep -rn "lib_path\|gems_craft\.libs" src/ tests/ docs/at review time to confirmlibs/still has no importers before deleting it.gems_craft.libraryandgems_craft.library.schemaare both present ([tool.setuptools.packages.find]useswhere = ["src"]auto-discovery, so new subpackages are picked up automatically — worth verifying rather than assuming).Process Checklist
Step 1 — Issue Creation
Step 2 — Triage
Step 3 — Impact Analysis⚠️ extended results analysis required
gems_craft/model/,gems_craft/libs/, plus 55 importing files)gems_craft.modeloutside this repo identified and notifiedStep 4 — Implementation
src/gems_craft/libs/deleted (or repopulated +package-dataadded, per the Step 2 decision)model/renamed tolibrary/;parsing.pyandtaxonomy.pymoved tolibrary/schema/src/,tests/,docs/gems_craft/model/__init__.pymodelimport inresolve_library.pyremoved; shadowing parameter inmodel.py:124renamed*Schemaconvention addedStep 5 — Testing & Validation
Step 6 — CI Validation
uv run mypy)uv run black src tests,uv run isort --profile black src tests)uv run pytest)Step 7 — Review & Merge
Step 8 — Versioning
pyproject.tomlversion bumped (Minor)Step 9 — Supporting Files
AGENTS.mdupdated:gems_craft/libs/entry removed,gems_craft/model/section renamed and theschema/split described.github/ISSUE_TEMPLATE/gp-02.yml"Affected Component" dropdown updateddocs/references togems_craft.modelupdated (4 occurrences)Step 10 — Release