scientific-computing-system 1.6.2__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.
- cds/__init__.py +102 -0
- cds/__main__.py +8 -0
- cds/_version.py +10 -0
- cds/cli/__init__.py +104 -0
- cds/cli/__main__.py +8 -0
- cds/cli/_handlers.py +405 -0
- cds/cli/_parser.py +156 -0
- cds/cli/_style.py +117 -0
- cds/core/__init__.py +5 -0
- cds/core/_numeric.py +92 -0
- cds/core/models.py +166 -0
- cds/data_analysis/__init__.py +30 -0
- cds/data_analysis/dataset.py +122 -0
- cds/data_analysis/loader.py +75 -0
- cds/data_analysis/pandas_io.py +108 -0
- cds/data_analysis/transform.py +36 -0
- cds/data_analysis/viz.py +88 -0
- cds/diffeq/__init__.py +29 -0
- cds/diffeq/_implicit.py +349 -0
- cds/diffeq/solvers.py +312 -0
- cds/graph/__init__.py +31 -0
- cds/graph/algorithms.py +399 -0
- cds/hypothesis/__init__.py +48 -0
- cds/hypothesis/evaluator.py +369 -0
- cds/hypothesis/generator.py +275 -0
- cds/knowledge/__init__.py +41 -0
- cds/knowledge/graph.py +489 -0
- cds/knowledge/notes.py +231 -0
- cds/knowledge/retrieval.py +167 -0
- cds/math_utils/__init__.py +41 -0
- cds/math_utils/calculus.py +40 -0
- cds/math_utils/linalg.py +461 -0
- cds/math_utils/special.py +167 -0
- cds/ml/__init__.py +25 -0
- cds/ml/clustering.py +190 -0
- cds/ml/decomposition.py +172 -0
- cds/ml/linear_models.py +225 -0
- cds/ml/neighbors.py +162 -0
- cds/ml/neural.py +204 -0
- cds/ml/preprocessing.py +131 -0
- cds/ml/tree.py +202 -0
- cds/modeling/__init__.py +40 -0
- cds/modeling/_base.py +210 -0
- cds/modeling/_nodes.py +526 -0
- cds/modeling/expression.py +81 -0
- cds/modeling/model.py +134 -0
- cds/modeling/solver.py +176 -0
- cds/montecarlo/__init__.py +21 -0
- cds/montecarlo/methods.py +272 -0
- cds/nlp/__init__.py +170 -0
- cds/nlp/attention.py +285 -0
- cds/nlp/autograd/__init__.py +70 -0
- cds/nlp/autograd/_grad.py +120 -0
- cds/nlp/autograd/ops.py +133 -0
- cds/nlp/autograd/tensor.py +298 -0
- cds/nlp/bpe.py +449 -0
- cds/nlp/data.py +70 -0
- cds/nlp/embed.py +190 -0
- cds/nlp/layers.py +259 -0
- cds/nlp/model.py +456 -0
- cds/nlp/optim.py +163 -0
- cds/nlp/training.py +167 -0
- cds/nlp/viz.py +254 -0
- cds/numerical_integration/__init__.py +31 -0
- cds/numerical_integration/quadrature.py +480 -0
- cds/optimization/__init__.py +18 -0
- cds/optimization/_metaheuristics.py +287 -0
- cds/optimization/minimize.py +425 -0
- cds/plot/__init__.py +49 -0
- cds/plot/_backend.py +32 -0
- cds/plot/charts.py +462 -0
- cds/probability/__init__.py +55 -0
- cds/probability/_advanced.py +283 -0
- cds/probability/distributions.py +236 -0
- cds/py.typed +0 -0
- cds/quantum/__init__.py +51 -0
- cds/quantum/circuit.py +92 -0
- cds/quantum/multi_qubit.py +255 -0
- cds/quantum/simulator.py +38 -0
- cds/scientific/__init__.py +34 -0
- cds/scientific/constants.py +35 -0
- cds/scientific/formulas.py +116 -0
- cds/signals/__init__.py +43 -0
- cds/signals/filters.py +421 -0
- cds/signals/processing.py +224 -0
- cds/stats/__init__.py +83 -0
- cds/stats/_distributions.py +102 -0
- cds/stats/descriptive.py +172 -0
- cds/stats/hypothesis_tests.py +403 -0
- cds/stats/nonparametric.py +131 -0
- cds/stats/regression.py +51 -0
- cds/stats/time_series.py +408 -0
- scientific_computing_system-1.6.2.dist-info/METADATA +741 -0
- scientific_computing_system-1.6.2.dist-info/RECORD +97 -0
- scientific_computing_system-1.6.2.dist-info/WHEEL +4 -0
- scientific_computing_system-1.6.2.dist-info/entry_points.txt +2 -0
- scientific_computing_system-1.6.2.dist-info/licenses/LICENSE +21 -0
cds/knowledge/notes.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Research notes linked to concepts — :class:`Note` and :class:`Notebook`.
|
|
2
|
+
|
|
3
|
+
A :class:`Note` is a free-form record (title + body + tags) that can be
|
|
4
|
+
linked to one or more concept names in a :class:`KnowledgeGraph`. A
|
|
5
|
+
:class:`Notebook` is the collection that owns notes by id and answers
|
|
6
|
+
"which notes mention this concept?" and "which notes carry this tag?".
|
|
7
|
+
|
|
8
|
+
Like :mod:`cds.knowledge.graph`, persistence mirrors the
|
|
9
|
+
:class:`cds.nlp.bpe.BPEMerge` idiom: :meth:`to_dict`/``from_dict`` per
|
|
10
|
+
entity, :meth:`Notebook.save`/:meth:`Notebook.load` for file round-trips.
|
|
11
|
+
|
|
12
|
+
Notes are deliberately decoupled from the graph: a note may reference a
|
|
13
|
+
concept name that is not (yet) in any graph, and adding a concept never
|
|
14
|
+
requires touching notes. The link is a string, not a reference, mirroring
|
|
15
|
+
how a citation works in a lab notebook.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Note:
|
|
27
|
+
"""A single research note linked to zero or more concept names.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
id: unique note identifier within a :class:`Notebook`.
|
|
31
|
+
title: short human-readable heading.
|
|
32
|
+
body: the note's free-form text content.
|
|
33
|
+
tags: grouping labels (e.g. ``["experiment", "failed"]``).
|
|
34
|
+
linked_concepts: names of concepts this note references. These are
|
|
35
|
+
plain strings, not references — they need not exist in any
|
|
36
|
+
particular :class:`KnowledgeGraph`.
|
|
37
|
+
created: optional ISO-8601 timestamp (or any caller-defined marker).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
id: str
|
|
41
|
+
title: str
|
|
42
|
+
body: str
|
|
43
|
+
tags: list[str] = field(default_factory=list)
|
|
44
|
+
linked_concepts: list[str] = field(default_factory=list)
|
|
45
|
+
created: str | None = None
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict[str, object]:
|
|
48
|
+
"""Serialize this note to a JSON-friendly dict."""
|
|
49
|
+
return {
|
|
50
|
+
"id": self.id,
|
|
51
|
+
"title": self.title,
|
|
52
|
+
"body": self.body,
|
|
53
|
+
"tags": list(self.tags),
|
|
54
|
+
"linked_concepts": list(self.linked_concepts),
|
|
55
|
+
"created": self.created,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_dict(cls, data: dict[str, object]) -> Note:
|
|
60
|
+
"""Reconstruct a :class:`Note` from :meth:`to_dict` output.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: if ``data`` is missing keys or has the wrong types.
|
|
64
|
+
"""
|
|
65
|
+
note_id = data["id"]
|
|
66
|
+
title = data["title"]
|
|
67
|
+
body = data["body"]
|
|
68
|
+
if not isinstance(note_id, str):
|
|
69
|
+
raise ValueError(f"Invalid note id: {note_id!r}")
|
|
70
|
+
if not isinstance(title, str):
|
|
71
|
+
raise ValueError(f"Invalid note title: {title!r}")
|
|
72
|
+
if not isinstance(body, str):
|
|
73
|
+
raise ValueError(f"Invalid note body: {body!r}")
|
|
74
|
+
tags_raw = data["tags"]
|
|
75
|
+
if not isinstance(tags_raw, list) or not all(isinstance(t, str) for t in tags_raw):
|
|
76
|
+
raise ValueError(f"Invalid note tags: {tags_raw!r}")
|
|
77
|
+
linked_raw = data["linked_concepts"]
|
|
78
|
+
if not isinstance(linked_raw, list) or not all(isinstance(c, str) for c in linked_raw):
|
|
79
|
+
raise ValueError(f"Invalid note linked_concepts: {linked_raw!r}")
|
|
80
|
+
created = data["created"]
|
|
81
|
+
if created is not None and not isinstance(created, str):
|
|
82
|
+
raise ValueError(f"Invalid note created: {created!r}")
|
|
83
|
+
return cls(
|
|
84
|
+
id=note_id,
|
|
85
|
+
title=title,
|
|
86
|
+
body=body,
|
|
87
|
+
tags=list(tags_raw),
|
|
88
|
+
linked_concepts=list(linked_raw),
|
|
89
|
+
created=created,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def to_markdown(self) -> str:
|
|
93
|
+
"""Render this note as a self-contained Markdown document."""
|
|
94
|
+
lines: list[str] = [f"# {self.title}", ""]
|
|
95
|
+
if self.created:
|
|
96
|
+
lines += [f"_Created: {self.created}_", ""]
|
|
97
|
+
if self.tags:
|
|
98
|
+
lines += ["Tags: " + ", ".join(f"`{tag}`" for tag in self.tags), ""]
|
|
99
|
+
lines += [self.body, ""]
|
|
100
|
+
if self.linked_concepts:
|
|
101
|
+
lines += ["## Linked concepts", ""]
|
|
102
|
+
for concept in self.linked_concepts:
|
|
103
|
+
lines.append(f"- `{concept}`")
|
|
104
|
+
lines.append("")
|
|
105
|
+
return "\n".join(lines)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class Notebook:
|
|
110
|
+
"""An ordered collection of research notes keyed by id.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
name: human-readable notebook title (used in :meth:`to_markdown`).
|
|
114
|
+
notes: mapping of note id to :class:`Note`.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
name: str
|
|
118
|
+
notes: dict[str, Note] = field(default_factory=dict)
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------ #
|
|
121
|
+
# Construction & lookup
|
|
122
|
+
# ------------------------------------------------------------------ #
|
|
123
|
+
def add_note(
|
|
124
|
+
self,
|
|
125
|
+
note_id: str,
|
|
126
|
+
title: str,
|
|
127
|
+
body: str,
|
|
128
|
+
tags: list[str] | None = None,
|
|
129
|
+
linked_concepts: list[str] | None = None,
|
|
130
|
+
created: str | None = None,
|
|
131
|
+
) -> Note:
|
|
132
|
+
"""Add a note, returning the stored :class:`Note`.
|
|
133
|
+
|
|
134
|
+
If ``note_id`` already exists it is overwritten (last-write-wins),
|
|
135
|
+
matching how a researcher edits a numbered entry in place.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
the stored :class:`Note`.
|
|
139
|
+
"""
|
|
140
|
+
note = Note(
|
|
141
|
+
id=note_id,
|
|
142
|
+
title=title,
|
|
143
|
+
body=body,
|
|
144
|
+
tags=list(tags) if tags else [],
|
|
145
|
+
linked_concepts=list(linked_concepts) if linked_concepts else [],
|
|
146
|
+
created=created,
|
|
147
|
+
)
|
|
148
|
+
self.notes[note_id] = note
|
|
149
|
+
return note
|
|
150
|
+
|
|
151
|
+
def get_note(self, note_id: str) -> Note:
|
|
152
|
+
"""Return the note with ``note_id``.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
KeyError: if ``note_id`` is not in this notebook.
|
|
156
|
+
"""
|
|
157
|
+
if note_id not in self.notes:
|
|
158
|
+
raise KeyError(f"unknown note: {note_id!r}")
|
|
159
|
+
return self.notes[note_id]
|
|
160
|
+
|
|
161
|
+
def notes_for_concept(self, concept: str) -> list[Note]:
|
|
162
|
+
"""All notes that reference ``concept`` (by linked_concepts membership)."""
|
|
163
|
+
return [note for note in self.notes.values() if concept in note.linked_concepts]
|
|
164
|
+
|
|
165
|
+
def notes_by_tag(self, tag: str) -> list[Note]:
|
|
166
|
+
"""All notes carrying ``tag``."""
|
|
167
|
+
return [note for note in self.notes.values() if tag in note.tags]
|
|
168
|
+
|
|
169
|
+
# ------------------------------------------------------------------ #
|
|
170
|
+
# Rendering & serialization
|
|
171
|
+
# ------------------------------------------------------------------ #
|
|
172
|
+
def to_markdown(self) -> str:
|
|
173
|
+
"""Render a compact index of this notebook's notes as Markdown."""
|
|
174
|
+
lines: list[str] = [f"# Notebook: {self.name}", ""]
|
|
175
|
+
if not self.notes:
|
|
176
|
+
lines += ["_No notes._", ""]
|
|
177
|
+
else:
|
|
178
|
+
lines += ["## Notes", ""]
|
|
179
|
+
for note_id in sorted(self.notes):
|
|
180
|
+
note = self.notes[note_id]
|
|
181
|
+
tags = f" ({', '.join(note.tags)})" if note.tags else ""
|
|
182
|
+
lines.append(f"- **{note_id}**: {note.title}{tags}")
|
|
183
|
+
lines.append("")
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
def to_dict(self) -> dict[str, object]:
|
|
187
|
+
"""Serialize the notebook to a JSON-friendly dict."""
|
|
188
|
+
return {
|
|
189
|
+
"name": self.name,
|
|
190
|
+
"notes": [note.to_dict() for note in self.notes.values()],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@classmethod
|
|
194
|
+
def from_dict(cls, data: dict[str, object]) -> Notebook:
|
|
195
|
+
"""Reconstruct a :class:`Notebook` from :meth:`to_dict` output.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
ValueError: if ``data`` is missing keys or has the wrong types.
|
|
199
|
+
"""
|
|
200
|
+
name = data["name"]
|
|
201
|
+
if not isinstance(name, str):
|
|
202
|
+
raise ValueError(f"Invalid notebook name: {name!r}")
|
|
203
|
+
notes_raw = data["notes"]
|
|
204
|
+
if not isinstance(notes_raw, list):
|
|
205
|
+
raise ValueError(f"Invalid notes list: {notes_raw!r}")
|
|
206
|
+
notebook = cls(name=name)
|
|
207
|
+
for item in notes_raw:
|
|
208
|
+
if not isinstance(item, dict):
|
|
209
|
+
raise ValueError(f"Invalid note entry: {item!r}")
|
|
210
|
+
note = Note.from_dict(item)
|
|
211
|
+
notebook.notes[note.id] = note
|
|
212
|
+
return notebook
|
|
213
|
+
|
|
214
|
+
def save(self, path: str | Path) -> None:
|
|
215
|
+
"""Write this notebook to ``path`` as indented UTF-8 JSON."""
|
|
216
|
+
Path(path).write_text(
|
|
217
|
+
json.dumps(self.to_dict(), ensure_ascii=False, indent=2),
|
|
218
|
+
encoding="utf-8",
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def load(cls, path: str | Path) -> Notebook:
|
|
223
|
+
"""Read a notebook previously written by :meth:`save`.
|
|
224
|
+
|
|
225
|
+
Raises:
|
|
226
|
+
ValueError: if the file does not contain valid notebook JSON.
|
|
227
|
+
"""
|
|
228
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
229
|
+
if not isinstance(data, dict):
|
|
230
|
+
raise ValueError(f"Invalid notebook file (expected object): {data!r}")
|
|
231
|
+
return cls.from_dict(data)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Structured retrieval across a knowledge graph and its notes.
|
|
2
|
+
|
|
3
|
+
Given a free-text ``query`` and an optional ``tag`` filter, :func:`search`
|
|
4
|
+
ranks matching concepts and notes by relevance. The scoring is deliberately
|
|
5
|
+
simple and deterministic — no statistics, no tokenization beyond case-folding
|
|
6
|
+
— so results are easy to explain and test:
|
|
7
|
+
|
|
8
|
+
* An exact name/tag match scores ``1.0`` (highest confidence).
|
|
9
|
+
* A substring match in the description/body/title scores ``0.5``.
|
|
10
|
+
* Results are sorted by descending score, then by name/id alphabetically
|
|
11
|
+
so ties resolve deterministically.
|
|
12
|
+
|
|
13
|
+
This is the *retrieval* half of Issue #3's four goals; the graph, concept
|
|
14
|
+
mapping, and note-management halves live in :mod:`cds.knowledge.graph` and
|
|
15
|
+
:mod:`cds.knowledge.notes`.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
from cds.knowledge.graph import KnowledgeGraph
|
|
23
|
+
from cds.knowledge.notes import Notebook
|
|
24
|
+
|
|
25
|
+
# Score constants — exposed as module attributes for clarity in assertions.
|
|
26
|
+
NAME_TAG_SCORE = 1.0
|
|
27
|
+
SUBSTRING_SCORE = 0.5
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class SearchResult:
|
|
32
|
+
"""A single ranked retrieval hit.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
concept_name: the matched concept name, if the hit is a concept;
|
|
36
|
+
``None`` otherwise.
|
|
37
|
+
note_id: the matched note id, if the hit is a note; ``None`` otherwise.
|
|
38
|
+
score: relevance in ``[0, 1]`` — higher is better.
|
|
39
|
+
matched_on: short label of the field that matched
|
|
40
|
+
(e.g. ``"name"``, ``"description"``, ``"title"``).
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
concept_name: str | None
|
|
44
|
+
note_id: str | None
|
|
45
|
+
score: float
|
|
46
|
+
matched_on: str
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def search_concepts(
|
|
50
|
+
graph: KnowledgeGraph,
|
|
51
|
+
query: str,
|
|
52
|
+
tag: str | None = None,
|
|
53
|
+
) -> list[SearchResult]:
|
|
54
|
+
"""Find concepts in ``graph`` matching ``query``.
|
|
55
|
+
|
|
56
|
+
A concept matches if its name matches the query exactly (score 1.0) or
|
|
57
|
+
its name or description contains the query as a substring (score 0.5).
|
|
58
|
+
When ``tag`` is given, only concepts carrying that tag are considered.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
graph: the :class:`KnowledgeGraph` to search.
|
|
62
|
+
query: case-insensitive search text.
|
|
63
|
+
tag: optional tag filter; ``None`` disables filtering.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
ranked :class:`SearchResult` list (best first, ties alphabetical).
|
|
67
|
+
"""
|
|
68
|
+
needle = query.casefold()
|
|
69
|
+
results: list[SearchResult] = []
|
|
70
|
+
for name in sorted(graph.concepts):
|
|
71
|
+
concept = graph.concepts[name]
|
|
72
|
+
if tag is not None and tag not in concept.tags:
|
|
73
|
+
continue
|
|
74
|
+
name_folded = name.casefold()
|
|
75
|
+
if name_folded == needle:
|
|
76
|
+
results.append(
|
|
77
|
+
SearchResult(
|
|
78
|
+
concept_name=name, note_id=None, score=NAME_TAG_SCORE, matched_on="name"
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
elif needle in name_folded:
|
|
82
|
+
results.append(
|
|
83
|
+
SearchResult(
|
|
84
|
+
concept_name=name, note_id=None, score=SUBSTRING_SCORE, matched_on="name"
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
elif concept.description is not None and needle in concept.description.casefold():
|
|
88
|
+
results.append(
|
|
89
|
+
SearchResult(
|
|
90
|
+
concept_name=name, note_id=None, score=SUBSTRING_SCORE, matched_on="description"
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
results.sort(key=lambda r: (-r.score, r.concept_name or ""))
|
|
94
|
+
return results
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def search_notes(
|
|
98
|
+
notebook: Notebook,
|
|
99
|
+
query: str,
|
|
100
|
+
tag: str | None = None,
|
|
101
|
+
) -> list[SearchResult]:
|
|
102
|
+
"""Find notes in ``notebook`` matching ``query``.
|
|
103
|
+
|
|
104
|
+
A note matches if its title matches exactly (score 1.0) or its title or
|
|
105
|
+
body contains the query as a substring (score 0.5). When ``tag`` is
|
|
106
|
+
given, only notes carrying that tag are considered.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
notebook: the :class:`Notebook` to search.
|
|
110
|
+
query: case-insensitive search text.
|
|
111
|
+
tag: optional tag filter; ``None`` disables filtering.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
ranked :class:`SearchResult` list (best first, ties alphabetical).
|
|
115
|
+
"""
|
|
116
|
+
needle = query.casefold()
|
|
117
|
+
results: list[SearchResult] = []
|
|
118
|
+
for note_id in sorted(notebook.notes):
|
|
119
|
+
note = notebook.notes[note_id]
|
|
120
|
+
if tag is not None and tag not in note.tags:
|
|
121
|
+
continue
|
|
122
|
+
title_folded = note.title.casefold()
|
|
123
|
+
if title_folded == needle:
|
|
124
|
+
results.append(
|
|
125
|
+
SearchResult(
|
|
126
|
+
concept_name=None, note_id=note_id, score=NAME_TAG_SCORE, matched_on="title"
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
elif needle in title_folded:
|
|
130
|
+
results.append(
|
|
131
|
+
SearchResult(
|
|
132
|
+
concept_name=None, note_id=note_id, score=SUBSTRING_SCORE, matched_on="title"
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
elif needle in note.body.casefold():
|
|
136
|
+
results.append(
|
|
137
|
+
SearchResult(
|
|
138
|
+
concept_name=None, note_id=note_id, score=SUBSTRING_SCORE, matched_on="body"
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
results.sort(key=lambda r: (-r.score, r.note_id or ""))
|
|
142
|
+
return results
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def search(
|
|
146
|
+
graph: KnowledgeGraph,
|
|
147
|
+
notebook: Notebook,
|
|
148
|
+
query: str,
|
|
149
|
+
tag: str | None = None,
|
|
150
|
+
) -> list[SearchResult]:
|
|
151
|
+
"""Combined ranked search over both a graph's concepts and a notebook's notes.
|
|
152
|
+
|
|
153
|
+
Results from :func:`search_concepts` and :func:`search_notes` are merged
|
|
154
|
+
and re-ranked by score (desc) then by identifier (asc).
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
graph: the :class:`KnowledgeGraph` whose concepts to search.
|
|
158
|
+
notebook: the :class:`Notebook` whose notes to search.
|
|
159
|
+
query: case-insensitive search text.
|
|
160
|
+
tag: optional tag filter applied to both concepts and notes.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
ranked :class:`SearchResult` list (best first, ties alphabetical).
|
|
164
|
+
"""
|
|
165
|
+
combined = search_concepts(graph, query, tag) + search_notes(notebook, query, tag)
|
|
166
|
+
combined.sort(key=lambda r: (-r.score, r.concept_name or r.note_id or ""))
|
|
167
|
+
return combined
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Math helper functions."""
|
|
2
|
+
|
|
3
|
+
from cds.math_utils.calculus import derivative, gradient, integral
|
|
4
|
+
from cds.math_utils.linalg import (
|
|
5
|
+
cholesky,
|
|
6
|
+
determinant,
|
|
7
|
+
dot,
|
|
8
|
+
frobenius_norm,
|
|
9
|
+
gram_schmidt,
|
|
10
|
+
identity,
|
|
11
|
+
lu_decomposition,
|
|
12
|
+
mat_mul,
|
|
13
|
+
matrix_inverse,
|
|
14
|
+
matrix_trace,
|
|
15
|
+
power_iteration,
|
|
16
|
+
qr_decomposition,
|
|
17
|
+
solve_linear,
|
|
18
|
+
transpose,
|
|
19
|
+
vector_norm,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"derivative",
|
|
24
|
+
"integral",
|
|
25
|
+
"gradient",
|
|
26
|
+
"dot",
|
|
27
|
+
"mat_mul",
|
|
28
|
+
"transpose",
|
|
29
|
+
"determinant",
|
|
30
|
+
"identity",
|
|
31
|
+
"lu_decomposition",
|
|
32
|
+
"solve_linear",
|
|
33
|
+
"matrix_inverse",
|
|
34
|
+
"power_iteration",
|
|
35
|
+
"gram_schmidt",
|
|
36
|
+
"qr_decomposition",
|
|
37
|
+
"cholesky",
|
|
38
|
+
"vector_norm",
|
|
39
|
+
"frobenius_norm",
|
|
40
|
+
"matrix_trace",
|
|
41
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Numerical calculus — derivatives, integrals, gradients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def derivative(f: Callable[[float], float], x: float, h_base: float = 1e-7) -> float:
|
|
9
|
+
"""Central difference approximation with adaptive step size."""
|
|
10
|
+
h = h_base * max(1.0, abs(x))
|
|
11
|
+
return (f(x + h) - f(x - h)) / (2 * h)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def integral(f: Callable[[float], float], a: float, b: float, n: int = 1000) -> float:
|
|
15
|
+
"""Simpson's rule for numerical integration."""
|
|
16
|
+
if n % 2 != 0:
|
|
17
|
+
n += 1
|
|
18
|
+
h = (b - a) / n
|
|
19
|
+
s = f(a) + f(b)
|
|
20
|
+
for i in range(1, n):
|
|
21
|
+
coeff = 4 if i % 2 != 0 else 2
|
|
22
|
+
s += coeff * f(a + i * h)
|
|
23
|
+
return s * h / 3
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def gradient(f: Callable[..., float], point: list[float], h_base: float = 1e-7) -> list[float]:
|
|
27
|
+
"""Numerical gradient for multivariable functions with adaptive scaling."""
|
|
28
|
+
grad = []
|
|
29
|
+
for i in range(len(point)):
|
|
30
|
+
# Scale step size h relative to point magnitude to maintain precision
|
|
31
|
+
h = h_base * max(1.0, abs(point[i]))
|
|
32
|
+
|
|
33
|
+
def partial(val: float, idx: int = i) -> float:
|
|
34
|
+
"""Return f evaluated with coordinate `idx` set to `val`."""
|
|
35
|
+
p = point.copy()
|
|
36
|
+
p[idx] = val
|
|
37
|
+
return f(*p)
|
|
38
|
+
|
|
39
|
+
grad.append((partial(point[i] + h) - partial(point[i] - h)) / (2 * h))
|
|
40
|
+
return grad
|