design-research-problems 0.1.0__py3-none-any.whl

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 (30) hide show
  1. design_research_problems/__init__.py +73 -0
  2. design_research_problems/_assets/catalog/peanut_sheller_fu2010/citation.bib +5 -0
  3. design_research_problems/_assets/catalog/peanut_sheller_fu2010/problem.toml +26 -0
  4. design_research_problems/_assets/catalog/peanut_sheller_fu2010/statement.md +18 -0
  5. design_research_problems/_assets/catalog/pill_capsule_min_area/problem.toml +28 -0
  6. design_research_problems/_assets/catalog/pill_capsule_min_area/statement.md +8 -0
  7. design_research_problems/_assets/catalog/planar_truss_span/problem.toml +30 -0
  8. design_research_problems/_assets/catalog/planar_truss_span/statement.md +10 -0
  9. design_research_problems/_catalog/__init__.py +5 -0
  10. design_research_problems/_catalog/_loader.py +220 -0
  11. design_research_problems/_catalog/_manifest.py +22 -0
  12. design_research_problems/_catalog/_registry.py +199 -0
  13. design_research_problems/_exceptions.py +11 -0
  14. design_research_problems/_lazy_exports.py +53 -0
  15. design_research_problems/problems/__init__.py +17 -0
  16. design_research_problems/problems/_assets.py +47 -0
  17. design_research_problems/problems/_grammar.py +63 -0
  18. design_research_problems/problems/_metadata.py +104 -0
  19. design_research_problems/problems/_optimization.py +161 -0
  20. design_research_problems/problems/_text.py +55 -0
  21. design_research_problems/problems/grammar/__init__.py +23 -0
  22. design_research_problems/problems/grammar/_planar_truss.py +418 -0
  23. design_research_problems/problems/optimization/__init__.py +5 -0
  24. design_research_problems/problems/optimization/_pill.py +118 -0
  25. design_research_problems/py.typed +1 -0
  26. design_research_problems-0.1.0.dist-info/METADATA +118 -0
  27. design_research_problems-0.1.0.dist-info/RECORD +30 -0
  28. design_research_problems-0.1.0.dist-info/WHEEL +5 -0
  29. design_research_problems-0.1.0.dist-info/licenses/LICENSE +21 -0
  30. design_research_problems-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,73 @@
1
+ """Curated public package interface with lazily resolved exports."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from typing import TYPE_CHECKING, Final
7
+
8
+ from design_research_problems._lazy_exports import module_dir, resolve_lazy_export
9
+
10
+ _EXPORTS: Final[dict[str, str]] = {
11
+ "ProblemKind": "design_research_problems.problems:ProblemKind",
12
+ "ProblemMetadata": "design_research_problems.problems:ProblemMetadata",
13
+ "ProblemTaxonomy": "design_research_problems.problems:ProblemTaxonomy",
14
+ "Citation": "design_research_problems.problems:Citation",
15
+ "ProblemAsset": "design_research_problems.problems:ProblemAsset",
16
+ "TextProblem": "design_research_problems.problems:TextProblem",
17
+ "OptimizationProblem": "design_research_problems.problems:OptimizationProblem",
18
+ "GrammarProblem": "design_research_problems.problems:GrammarProblem",
19
+ "ProblemRegistry": "design_research_problems._catalog:ProblemRegistry",
20
+ "MissingOptionalDependencyError": "design_research_problems._exceptions:MissingOptionalDependencyError",
21
+ "ProblemEvaluationError": "design_research_problems._exceptions:ProblemEvaluationError",
22
+ "get_problem": "design_research_problems._catalog:get_problem",
23
+ "list_problems": "design_research_problems._catalog:list_problems",
24
+ }
25
+
26
+ __all__ = ["__version__", *_EXPORTS.keys()]
27
+
28
+ try:
29
+ __version__ = version("design-research-problems")
30
+ except PackageNotFoundError:
31
+ __version__ = "0+unknown"
32
+
33
+
34
+ def __getattr__(name: str) -> object:
35
+ """Resolve and cache one deferred public export.
36
+
37
+ Args:
38
+ name: Public symbol name requested from the package module.
39
+
40
+ Returns:
41
+ Resolved export object.
42
+ """
43
+ return resolve_lazy_export(
44
+ module_name=__name__,
45
+ exports=_EXPORTS,
46
+ export_name=name,
47
+ namespace=globals(),
48
+ )
49
+
50
+
51
+ def __dir__() -> list[str]:
52
+ """Return package attributes, including deferred exports.
53
+
54
+ Returns:
55
+ Sorted attribute list for interactive discovery.
56
+ """
57
+ return module_dir(globals(), __all__)
58
+
59
+
60
+ if TYPE_CHECKING:
61
+ from ._catalog import ProblemRegistry as ProblemRegistry
62
+ from ._catalog import get_problem as get_problem
63
+ from ._catalog import list_problems as list_problems
64
+ from ._exceptions import MissingOptionalDependencyError as MissingOptionalDependencyError
65
+ from ._exceptions import ProblemEvaluationError as ProblemEvaluationError
66
+ from .problems import Citation as Citation
67
+ from .problems import GrammarProblem as GrammarProblem
68
+ from .problems import OptimizationProblem as OptimizationProblem
69
+ from .problems import ProblemAsset as ProblemAsset
70
+ from .problems import ProblemKind as ProblemKind
71
+ from .problems import ProblemMetadata as ProblemMetadata
72
+ from .problems import ProblemTaxonomy as ProblemTaxonomy
73
+ from .problems import TextProblem as TextProblem
@@ -0,0 +1,5 @@
1
+ @article{fu2010design,
2
+ title={Design team convergence: the influence of example solution quality},
3
+ author={Fu, Katherine and Cagan, Jonathan and Kotovsky, Kenneth},
4
+ year={2010}
5
+ }
@@ -0,0 +1,26 @@
1
+ problem_id = "peanut_sheller_fu2010"
2
+ title = "Design Problem - Device to shell peanuts"
3
+ summary = "A text-based design prompt about a low-cost peanut sheller for small-scale farmers."
4
+ kind = "text"
5
+ statement_file = "statement.md"
6
+ citation_file = "citation.bib"
7
+ citation_key = "fu2010design"
8
+ citation_kind = "bibtex"
9
+ feature_flags = [
10
+ "statement-markdown",
11
+ "citation-backed",
12
+ "prompt-packet",
13
+ "human-subjects-ready",
14
+ ]
15
+
16
+ [taxonomy]
17
+ formulation = "textual_prompt"
18
+ convexity = "not_applicable"
19
+ design_variable_type = "not_applicable"
20
+ is_dynamic = false
21
+ orientation = "engineering_practical"
22
+ feasibility_ratio_hint = 1.0
23
+ objective_mode = "qualitative"
24
+ constraint_nature = "informal"
25
+ bounds_summary = "not_applicable"
26
+ tags = ["text", "human-subjects", "agriculture", "mechanical-design"]
@@ -0,0 +1,18 @@
1
+ # Design Problem - Device to shell peanuts
2
+
3
+ In places like Haiti and certain West African countries, peanuts are a
4
+ significant crop. Most peanut farmers shell their peanuts by hand, an
5
+ inefficient and labor-intensive process. Our goal is to build a low-cost,
6
+ easy to manufacture peanut sheller targeted at individuals and small
7
+ cooperatives that will increase the productivity of the peanut farmers.
8
+ Further, this peanut sheller should be manufacturable with materials that are
9
+ readily available in the target communities. Our target throughput is
10
+ approximately 50 kg (110 lbs) per hour.
11
+
12
+ # Customer Needs
13
+
14
+ - Must remove the shell with minimal damage to the peanuts.
15
+ - Electrical outlets are not available as a power source.
16
+ - A large quantity of peanuts must be quickly shelled.
17
+ - Low cost.
18
+ - Easy to manufacture.
@@ -0,0 +1,28 @@
1
+ problem_id = "pill_capsule_min_area"
2
+ title = "Pill capsule minimum-area problem"
3
+ summary = "A two-variable constrained optimization problem that minimizes capsule surface area at fixed volume."
4
+ kind = "optimization"
5
+ statement_file = "statement.md"
6
+ implementation = "design_research_problems.problems.optimization._pill:PillCapsuleMinArea"
7
+ feature_flags = [
8
+ "statement-markdown",
9
+ "bounded-variables",
10
+ "equality-constraint",
11
+ "seeded-data-generation",
12
+ "optional-solver",
13
+ ]
14
+
15
+ [parameters]
16
+ required_volume = 1e-6
17
+
18
+ [taxonomy]
19
+ formulation = "nonlinear_program"
20
+ convexity = "not_guaranteed"
21
+ design_variable_type = "continuous"
22
+ is_dynamic = false
23
+ orientation = "mathematical"
24
+ feasibility_ratio_hint = 0.5
25
+ objective_mode = "single"
26
+ constraint_nature = "hard"
27
+ bounds_summary = "radius and length bounded on [0, 1]"
28
+ tags = ["optimization", "continuous", "nonlinear", "single-objective"]
@@ -0,0 +1,8 @@
1
+ # Pill capsule minimum-area problem
2
+
3
+ Minimize the surface area of a capsule-shaped pill while satisfying a fixed
4
+ volume requirement. The design variables are the capsule radius and cylindrical
5
+ section length. Both variables are bounded on [0, 1].
6
+
7
+ This seed problem is a compact constrained optimization benchmark for design
8
+ research workflows and solver experiments.
@@ -0,0 +1,30 @@
1
+ problem_id = "planar_truss_span"
2
+ title = "Planar truss span grammar problem"
3
+ summary = "A discrete topology grammar for adding and removing joints and members in a planar truss."
4
+ kind = "grammar"
5
+ statement_file = "statement.md"
6
+ implementation = "design_research_problems.problems.grammar._planar_truss:PlanarTrussSpanProblem"
7
+ feature_flags = [
8
+ "statement-markdown",
9
+ "serializable-state",
10
+ "discrete-actions",
11
+ "optional-evaluator",
12
+ "external-adapter",
13
+ ]
14
+
15
+ [parameters]
16
+ span = 10.0
17
+ max_height = 5.0
18
+ load_magnitude = 1000.0
19
+
20
+ [taxonomy]
21
+ formulation = "discrete_grammar"
22
+ convexity = "not_applicable"
23
+ design_variable_type = "discrete"
24
+ is_dynamic = false
25
+ orientation = "engineering_practical"
26
+ feasibility_ratio_hint = 0.25
27
+ objective_mode = "single"
28
+ constraint_nature = "hard"
29
+ bounds_summary = "planar topology over a fixed span and height envelope"
30
+ tags = ["grammar", "truss", "discrete", "topology"]
@@ -0,0 +1,10 @@
1
+ # Planar truss span grammar problem
2
+
3
+ Create a stable, lightweight planar truss spanning a fixed distance between a
4
+ left pinned support and a right roller support. The grammar begins with the
5
+ supports and one loaded free joint. Designers can add interior joints, add
6
+ members, and remove members.
7
+
8
+ States are evaluated by translating the library-owned grammar state into a
9
+ fresh `trussme.Truss`, applying the load, and attempting a structural
10
+ analysis. Singular or unstable states are treated as infeasible results.
@@ -0,0 +1,5 @@
1
+ """Catalog loading and registry exports."""
2
+
3
+ from ._registry import ProblemRegistry, get_problem, list_problems
4
+
5
+ __all__ = ["ProblemRegistry", "get_problem", "list_problems"]
@@ -0,0 +1,220 @@
1
+ """Load packaged problem manifests and resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tomllib
6
+ from importlib.resources import files
7
+ from importlib.resources.abc import Traversable
8
+ from typing import Any, cast
9
+
10
+ from design_research_problems._catalog._manifest import ProblemManifest
11
+ from design_research_problems.problems._metadata import (
12
+ Citation,
13
+ ProblemAsset,
14
+ ProblemKind,
15
+ ProblemMetadata,
16
+ ProblemTaxonomy,
17
+ )
18
+
19
+ _PACKAGE = "design_research_problems"
20
+ _CATALOG_DIR = "_assets/catalog"
21
+
22
+
23
+ def _catalog_root() -> Traversable:
24
+ """Return the traversable root directory for packaged catalog resources.
25
+
26
+ Returns:
27
+ Package resource directory containing all problem entries.
28
+ """
29
+ return files(_PACKAGE).joinpath("_assets", "catalog")
30
+
31
+
32
+ def _resource_root(resource_dir: str) -> Traversable:
33
+ """Return the traversable root directory for one packaged resource path.
34
+
35
+ Args:
36
+ resource_dir: Package-relative resource directory.
37
+
38
+ Returns:
39
+ Traversable directory for the requested resource path.
40
+ """
41
+ return files(_PACKAGE).joinpath(*resource_dir.split("/"))
42
+
43
+
44
+ def _parse_problem_kind(raw_kind: str) -> ProblemKind:
45
+ """Parse one manifest problem-kind string into the enum.
46
+
47
+ Args:
48
+ raw_kind: Raw string from the manifest file.
49
+
50
+ Returns:
51
+ Parsed problem kind.
52
+
53
+ Raises:
54
+ ValueError: If the manifest contains an unsupported problem kind.
55
+ """
56
+ normalized = raw_kind.strip().lower()
57
+ if normalized == "text":
58
+ return ProblemKind.TEXT
59
+ if normalized == "optimization":
60
+ return ProblemKind.OPTIMIZATION
61
+ if normalized == "grammar":
62
+ return ProblemKind.GRAMMAR
63
+ raise ValueError(f"Unsupported problem kind: {raw_kind!r}")
64
+
65
+
66
+ def _parse_citations(raw_data: dict[str, Any], resource_dir: str) -> tuple[Citation, ...]:
67
+ """Build citation objects from one manifest payload.
68
+
69
+ Args:
70
+ raw_data: Parsed TOML manifest mapping.
71
+ resource_dir: Package-relative resource directory for the entry.
72
+
73
+ Returns:
74
+ Parsed citations in manifest order.
75
+ """
76
+ citations: list[Citation] = []
77
+ citation_file = raw_data.get("citation_file")
78
+ if isinstance(citation_file, str):
79
+ resource = _resource_root(resource_dir).joinpath(citation_file)
80
+ raw_text = resource.read_text(encoding="utf-8")
81
+ citations.append(
82
+ Citation(
83
+ key=str(raw_data.get("citation_key", "")),
84
+ kind=str(raw_data.get("citation_kind", "bibtex")),
85
+ raw_text=raw_text,
86
+ url=cast(str | None, raw_data.get("citation_url")),
87
+ )
88
+ )
89
+
90
+ for entry in cast(list[dict[str, Any]], raw_data.get("citations", [])):
91
+ citations.append(
92
+ Citation(
93
+ key=str(entry["key"]),
94
+ kind=str(entry.get("kind", "inline")),
95
+ raw_text=str(entry["raw_text"]),
96
+ url=cast(str | None, entry.get("url")),
97
+ )
98
+ )
99
+
100
+ return tuple(citations)
101
+
102
+
103
+ def _parse_assets(raw_assets: list[dict[str, Any]]) -> tuple[ProblemAsset, ...]:
104
+ """Build asset metadata objects from raw manifest entries.
105
+
106
+ Args:
107
+ raw_assets: Raw asset dictionaries from the manifest.
108
+
109
+ Returns:
110
+ Parsed asset metadata tuple.
111
+ """
112
+ return tuple(
113
+ ProblemAsset(
114
+ name=str(entry["name"]),
115
+ media_type=str(entry["media_type"]),
116
+ description=str(entry.get("description", "")),
117
+ resource_path=str(entry["path"]),
118
+ )
119
+ for entry in raw_assets
120
+ )
121
+
122
+
123
+ def _parse_feature_flags(raw_flags: list[object]) -> tuple[str, ...]:
124
+ """Build a normalized, deterministic feature-flag tuple.
125
+
126
+ Args:
127
+ raw_flags: Raw feature-flag values from the manifest.
128
+
129
+ Returns:
130
+ Sorted unique normalized feature flags.
131
+ """
132
+ normalized: set[str] = set()
133
+ for raw_flag in raw_flags:
134
+ flag = str(raw_flag).strip().lower().replace(" ", "-")
135
+ if flag:
136
+ normalized.add(flag)
137
+ return tuple(sorted(normalized))
138
+
139
+
140
+ def _parse_taxonomy(raw_taxonomy: dict[str, Any]) -> ProblemTaxonomy:
141
+ """Build the shared taxonomy object from raw manifest data.
142
+
143
+ Args:
144
+ raw_taxonomy: Raw taxonomy mapping from the manifest.
145
+
146
+ Returns:
147
+ Parsed taxonomy object.
148
+ """
149
+ return ProblemTaxonomy(
150
+ formulation=str(raw_taxonomy.get("formulation", "unspecified")),
151
+ convexity=str(raw_taxonomy.get("convexity", "unknown")),
152
+ design_variable_type=str(raw_taxonomy.get("design_variable_type", "unknown")),
153
+ is_dynamic=bool(raw_taxonomy.get("is_dynamic", False)),
154
+ orientation=str(raw_taxonomy.get("orientation", "unknown")),
155
+ feasibility_ratio_hint=raw_taxonomy.get("feasibility_ratio_hint"),
156
+ objective_mode=str(raw_taxonomy.get("objective_mode", "single")),
157
+ constraint_nature=str(raw_taxonomy.get("constraint_nature", "unspecified")),
158
+ bounds_summary=str(raw_taxonomy.get("bounds_summary", "unspecified")),
159
+ tags=tuple(str(tag) for tag in raw_taxonomy.get("tags", [])),
160
+ )
161
+
162
+
163
+ def _load_single_manifest(entry: Any) -> ProblemManifest:
164
+ """Load one problem manifest directory.
165
+
166
+ Args:
167
+ entry: Traversable directory representing one problem entry.
168
+
169
+ Returns:
170
+ Parsed problem manifest object.
171
+ """
172
+ raw_text = entry.joinpath("problem.toml").read_text(encoding="utf-8")
173
+ raw_data = tomllib.loads(raw_text)
174
+ resource_dir = f"{_CATALOG_DIR}/{entry.name}"
175
+ metadata = ProblemMetadata(
176
+ problem_id=str(raw_data["problem_id"]),
177
+ title=str(raw_data["title"]),
178
+ summary=str(raw_data["summary"]),
179
+ kind=_parse_problem_kind(str(raw_data["kind"])),
180
+ taxonomy=_parse_taxonomy(cast(dict[str, Any], raw_data.get("taxonomy", {}))),
181
+ citations=_parse_citations(raw_data, resource_dir),
182
+ assets=_parse_assets(cast(list[dict[str, Any]], raw_data.get("assets", []))),
183
+ feature_flags=_parse_feature_flags(cast(list[object], raw_data.get("feature_flags", []))),
184
+ implementation=cast(str | None, raw_data.get("implementation")),
185
+ )
186
+ statement_resource = str(raw_data.get("statement_file", "statement.md"))
187
+ parameters = cast(dict[str, object], raw_data.get("parameters", {}))
188
+ return ProblemManifest(
189
+ metadata=metadata,
190
+ resource_dir=resource_dir,
191
+ statement_resource=statement_resource,
192
+ parameters=parameters,
193
+ )
194
+
195
+
196
+ def load_problem_manifests() -> dict[str, ProblemManifest]:
197
+ """Load all packaged problem manifests.
198
+
199
+ Returns:
200
+ Mapping of problem IDs to parsed manifests.
201
+ """
202
+ manifests: dict[str, ProblemManifest] = {}
203
+ for entry in sorted(_catalog_root().iterdir(), key=lambda item: item.name):
204
+ if not entry.is_dir():
205
+ continue
206
+ manifest = _load_single_manifest(entry)
207
+ manifests[manifest.metadata.problem_id] = manifest
208
+ return manifests
209
+
210
+
211
+ def load_statement_text(manifest: ProblemManifest) -> str:
212
+ """Load the statement markdown for one problem manifest.
213
+
214
+ Args:
215
+ manifest: Parsed manifest describing the packaged resource path.
216
+
217
+ Returns:
218
+ UTF-8 Markdown statement text.
219
+ """
220
+ return _resource_root(manifest.resource_dir).joinpath(manifest.statement_resource).read_text(encoding="utf-8")
@@ -0,0 +1,22 @@
1
+ """Manifest types for packaged problem resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+
8
+ from design_research_problems.problems._metadata import ProblemMetadata
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ProblemManifest:
13
+ """Container for one packaged catalog entry."""
14
+
15
+ metadata: ProblemMetadata
16
+ """Loaded problem metadata used for registry lookups."""
17
+ resource_dir: str
18
+ """Package-relative directory containing the problem resources."""
19
+ statement_resource: str
20
+ """File name of the packaged Markdown statement."""
21
+ parameters: Mapping[str, object]
22
+ """Implementation-specific manifest parameters."""
@@ -0,0 +1,199 @@
1
+ """Problem registry facade."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from importlib import import_module
7
+ from typing import cast
8
+
9
+ from design_research_problems._catalog._loader import load_problem_manifests, load_statement_text
10
+ from design_research_problems._catalog._manifest import ProblemManifest
11
+ from design_research_problems._exceptions import ProblemEvaluationError
12
+ from design_research_problems.problems import GrammarProblem, OptimizationProblem, ProblemKind, TextProblem
13
+ from design_research_problems.problems._assets import PackageResourceBundle
14
+ from design_research_problems.problems._metadata import ProblemMetadata
15
+
16
+
17
+ def _resolve_object(import_path: str) -> object:
18
+ """Import one ``module:attribute`` object reference.
19
+
20
+ Args:
21
+ import_path: Import target in ``module:attribute`` form.
22
+
23
+ Returns:
24
+ Imported Python object.
25
+
26
+ Raises:
27
+ design_research_problems.ProblemEvaluationError: If the import path is malformed.
28
+ """
29
+ module_path, _, attr_name = import_path.partition(":")
30
+ if not module_path or not attr_name:
31
+ raise ProblemEvaluationError(f"Invalid implementation path: {import_path!r}")
32
+ return getattr(import_module(module_path), attr_name)
33
+
34
+
35
+ ProblemInstance = TextProblem | OptimizationProblem | GrammarProblem
36
+
37
+
38
+ class ProblemRegistry:
39
+ """Lazy-loading registry for the packaged problem catalog."""
40
+
41
+ def __init__(self) -> None:
42
+ """Initialize an empty lazy registry cache."""
43
+ self._manifests: dict[str, ProblemManifest] | None = None
44
+
45
+ def _catalog(self) -> dict[str, ProblemManifest]:
46
+ """Return the cached manifest catalog.
47
+
48
+ Returns:
49
+ Mapping of problem IDs to parsed manifests.
50
+ """
51
+ if self._manifests is None:
52
+ self._manifests = load_problem_manifests()
53
+ return self._manifests
54
+
55
+ def list(self) -> tuple[ProblemMetadata, ...]:
56
+ """Return all problem metadata in ID-sorted order.
57
+
58
+ Returns:
59
+ Metadata entries sorted by problem ID.
60
+ """
61
+ return tuple(self._catalog()[problem_id].metadata for problem_id in sorted(self._catalog()))
62
+
63
+ def by_kind(self, kind: ProblemKind) -> tuple[ProblemMetadata, ...]:
64
+ """Return metadata entries with the requested kind.
65
+
66
+ Args:
67
+ kind: Problem family to filter for.
68
+
69
+ Returns:
70
+ Matching metadata entries.
71
+ """
72
+ return tuple(metadata for metadata in self.list() if metadata.kind is kind)
73
+
74
+ def feature_flags(self, problem_id: str) -> tuple[str, ...]:
75
+ """Return the feature flags for one problem ID.
76
+
77
+ Args:
78
+ problem_id: Stable catalog identifier.
79
+
80
+ Returns:
81
+ Feature flags in deterministic sorted order.
82
+
83
+ Raises:
84
+ KeyError: If the ID is unknown.
85
+ """
86
+ manifest = self._catalog().get(problem_id)
87
+ if manifest is None:
88
+ raise KeyError(f"Unknown problem id: {problem_id}")
89
+ return manifest.metadata.feature_flags
90
+
91
+ def kind_feature_flags(self) -> dict[ProblemKind, tuple[str, ...]]:
92
+ """Return aggregated feature flags for each problem family.
93
+
94
+ Returns:
95
+ Mapping of problem kinds to the union of feature flags across that family.
96
+ """
97
+ grouped: dict[ProblemKind, set[str]] = {kind: set() for kind in ProblemKind}
98
+ for metadata in self.list():
99
+ grouped[metadata.kind].update(metadata.feature_flags)
100
+ return {kind: tuple(sorted(grouped[kind])) for kind in ProblemKind}
101
+
102
+ def search(
103
+ self,
104
+ tags: tuple[str, ...] = (),
105
+ text: str = "",
106
+ feature_flags: tuple[str, ...] = (),
107
+ ) -> tuple[ProblemMetadata, ...]:
108
+ """Search metadata by tags and free text.
109
+
110
+ Args:
111
+ tags: Tags that must all be present on a matching entry.
112
+ text: Case-insensitive free-text search term.
113
+ feature_flags: Feature flags that must all be present on a matching entry.
114
+
115
+ Returns:
116
+ Matching metadata entries.
117
+ """
118
+ tag_set = {tag.lower() for tag in tags}
119
+ feature_flag_set = {flag.strip().lower().replace(" ", "-") for flag in feature_flags}
120
+ text_query = text.strip().lower()
121
+ matches: list[ProblemMetadata] = []
122
+ for metadata in self.list():
123
+ if tag_set and not tag_set.issubset({tag.lower() for tag in metadata.taxonomy.tags}):
124
+ continue
125
+ if feature_flag_set and not feature_flag_set.issubset(set(metadata.feature_flags)):
126
+ continue
127
+ haystack = " ".join(
128
+ (metadata.problem_id, metadata.title, metadata.summary, *metadata.taxonomy.tags)
129
+ ).lower()
130
+ if text_query and text_query not in haystack:
131
+ continue
132
+ matches.append(metadata)
133
+ return tuple(matches)
134
+
135
+ def get(self, problem_id: str) -> ProblemInstance:
136
+ """Instantiate one problem by ID.
137
+
138
+ Args:
139
+ problem_id: Stable catalog identifier.
140
+
141
+ Returns:
142
+ Loaded problem instance.
143
+
144
+ Raises:
145
+ KeyError: If the ID is unknown.
146
+ design_research_problems.ProblemEvaluationError: If the packaged implementation metadata is invalid.
147
+ """
148
+ manifest = self._catalog().get(problem_id)
149
+ if manifest is None:
150
+ raise KeyError(f"Unknown problem id: {problem_id}")
151
+
152
+ statement_markdown = load_statement_text(manifest)
153
+ if manifest.metadata.kind is ProblemKind.TEXT:
154
+ return TextProblem(
155
+ metadata=manifest.metadata,
156
+ statement_markdown=statement_markdown,
157
+ assets=manifest.metadata.assets,
158
+ resource_bundle=PackageResourceBundle("design_research_problems", manifest.resource_dir),
159
+ )
160
+
161
+ implementation = manifest.metadata.implementation
162
+ if implementation is None:
163
+ raise ProblemEvaluationError(f"Problem {problem_id!r} is missing an implementation path.")
164
+
165
+ target = _resolve_object(implementation)
166
+ factory = getattr(target, "from_manifest", None)
167
+ if callable(factory):
168
+ manifest_factory = cast(Callable[[ProblemManifest, str], ProblemInstance], factory)
169
+ return manifest_factory(manifest, statement_markdown)
170
+
171
+ if callable(target):
172
+ direct_factory = cast(Callable[[object, str], ProblemInstance], target)
173
+ return direct_factory(manifest.metadata, statement_markdown)
174
+
175
+ raise ProblemEvaluationError(f"Problem implementation for {problem_id!r} is not callable.")
176
+
177
+
178
+ _DEFAULT_REGISTRY = ProblemRegistry()
179
+
180
+
181
+ def list_problems() -> tuple[str, ...]:
182
+ """Return all packaged problem IDs.
183
+
184
+ Returns:
185
+ Stable problem IDs in sorted order.
186
+ """
187
+ return tuple(metadata.problem_id for metadata in _DEFAULT_REGISTRY.list())
188
+
189
+
190
+ def get_problem(problem_id: str) -> TextProblem | OptimizationProblem | GrammarProblem:
191
+ """Return one problem instance by ID.
192
+
193
+ Args:
194
+ problem_id: Stable catalog identifier.
195
+
196
+ Returns:
197
+ Loaded problem instance.
198
+ """
199
+ return _DEFAULT_REGISTRY.get(problem_id)
@@ -0,0 +1,11 @@
1
+ """Package-specific exceptions."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class MissingOptionalDependencyError(ImportError):
7
+ """Raised when a caller uses an optional integration that is not installed."""
8
+
9
+
10
+ class ProblemEvaluationError(RuntimeError):
11
+ """Raised when a problem definition is internally inconsistent."""