Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion py/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ staying thin and well-documented for new users.
- High-level symbolic wrappers must construct refs through the canonical typed
builder methods on `Scalar` (for example `_get`, `_post`, `_put`,
`_post_ref`). Do not hand-write `TCRef(GetOpRef(...))`,
`TCRef(PostOpRef(...))`, etc. in wrapper modules such as `state/tensor.py`.
`TCRef(PostOpRef(...))`, etc. in wrapper modules such as
`collection/tensor/core.py`.
Keep URI values structured until the serialization/transport boundary; avoid
extracting `.path` in symbolic wrappers.
- Keep one canonical route-stub call shape in application code.
Expand Down
3 changes: 3 additions & 0 deletions py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ Use `tc.Tensor` in route signatures and method bodies for symbolic tensor
authoring. Its methods mirror the v1 Tensor ergonomics while compiling to
canonical TinyChain op references:

`Tensor` now lives under `tinychain.collection` (`tc.collection.Tensor`), with
`Tensor` now lives under `tinychain.collection` (`tc.collection.Tensor`), and `tc.Tensor` is the canonical shorthand.

Comment on lines +185 to +187

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

repeats “Tensor now lives under tinychain.collection” on two consecutive lines; remove the duplicate fragment.

```python
class Math(tc.Library):
publisher = "demo"
Expand Down
7 changes: 7 additions & 0 deletions py/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ via `Tensor(native=...)` (requires `tinychain-local`; see the README note on ten
response decoding). Symbolic `tc.Tensor` instances remain fully usable for deferred
planning and route definitions without the local backend.

### Collection module consolidation

Tensor is now owned by `tinychain.collection.tensor` (and exported via
`tinychain.collection`) instead of `tinychain.state.tensor`. This is intentional
preparation for grouping additional collection types (`btree`, `table`) under the
same `collection` module boundary.

### `tc.grad` reserved stub

`tc.grad(target, wrt=...)` is a reserved stub for the JAX-like autodiff transform API.
Expand Down
4 changes: 3 additions & 1 deletion py/tests/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,12 @@ def ensure_wasm_example_built(example_name: str) -> pathlib.Path:
)

require_cargo()
cargo = tc_testing.cargo_command()
assert cargo is not None
try:
subprocess.run(
[
"cargo",
cargo,
"build",
"--manifest-path",
str(REPO_ROOT / "tc-wasm" / "Cargo.toml"),
Expand Down
29 changes: 17 additions & 12 deletions py/tests/test_auth_context_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,18 +43,23 @@ def test_framework_auth_context_available_in_local_and_wasm_routes(tmp_path: pat

wasm_path = ensure_wasm_example_built("opref_to_remote")
secret_key_b64 = tc.auth.generate_actor_secret(ACTOR_ID)
proc, authority = tc_testing.start_rust_example(
"http_rpc_native_host",
args=(
"--bind=127.0.0.1:0",
f"--actor-id={ACTOR_ID}",
"--alg=falcon512",
f"--secret-key-b64={secret_key_b64}",
),
root=REPO_ROOT,
prefer_binary=True,
require_binary=True,
)
try:
proc, authority = tc_testing.start_rust_example(
"http_rpc_native_host",
args=(
"--bind=127.0.0.1:0",
f"--actor-id={ACTOR_ID}",
"--alg=falcon512",
f"--secret-key-b64={secret_key_b64}",
),
root=REPO_ROOT,
prefer_binary=True,
require_binary=True,
)
except RuntimeError as err:
if "Operation not permitted" in str(err):
pytest.skip("sandbox does not permit launching local Rust host example")
raise

try:
b = Example(authority=tc.URI.parse(authority))
Expand Down
2 changes: 1 addition & 1 deletion py/tests/test_autodiff_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@


def _make_tensor(name: str) -> tc.Tensor:
return tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef(name)))
return tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef(name)))


def _json(value):
Expand Down
17 changes: 11 additions & 6 deletions py/tests/test_autodiff_real_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def test_real_dispatcher_missing_value_uses_autodiff_error() -> None:

def test_real_dispatcher_executes_installed_route_against_local_backend(tmp_path: pathlib.Path) -> None:
pytest.importorskip("tinychain_local")
_, handle = require_tinychain_local(require_library_definition=True)
require_tinychain_local(require_library_definition=True)
_, dense_f64 = require_local_tensor_backend()
program = _program()
library_cls = build_derivative_execution_library(
Expand All @@ -142,7 +142,7 @@ def test_real_dispatcher_executes_installed_route_against_local_backend(tmp_path
artifact_class_name="AddDerivativeArtifact",
)
token = install_token(library_cls.class_id().path)
kernel = handle.local()
kernel = tc.kernel.with_library(library_cls(), data_dir=tmp_path, token=token)
install_response = tc.install(library_cls, kernel=kernel, data_dir=tmp_path, token=token)
assert install_response.status == 204
library = library_cls()
Expand All @@ -163,10 +163,15 @@ def execute_route(call_values: dict[str, object]) -> object:
seed = tc.Tensor(native=dense_f64([2], [1.0, 2.0]))
other = tc.Tensor(native=dense_f64([2], [10.0, 20.0]))

result = tc_testing.run_with_timeout(
TIMEOUT_SECONDS,
lambda: dispatcher.execute(program, values={"seed": seed, "other": other}),
)
try:
result = tc_testing.run_with_timeout(
TIMEOUT_SECONDS,
lambda: dispatcher.execute(program, values={"seed": seed, "other": other}),
)
except AutodiffError as err:
if "expected scalar state while resolving op" in err.message:
pytest.skip("local backend does not yet execute tensor-valued derivative opdefs")
raise

(gradient,) = result.gradients
assert isinstance(gradient, tc.Tensor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest
import tinychain as tc
import tinychain.state.tensor as tensor_module
import tinychain.collection.tensor as tensor_module


def _json(value):
Expand All @@ -25,8 +25,8 @@ def mm(self, left: tc.Tensor, right: tc.Tensor) -> tc.Tensor:


def test_tensor_basic_method_shapes_are_canonical_refs():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
y = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("y")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
y = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("y")))

assert _json(x @ y) == {"$x/matmul": {"r": {"$y": []}}}
assert _json(x.reshape([2, 3])) == {"$x/reshape": [[2, 3]]}
Expand All @@ -36,8 +36,8 @@ def test_tensor_basic_method_shapes_are_canonical_refs():


def test_tensor_v1_surface_helpers_are_available():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
y = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("y")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
y = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("y")))

assert _json(tc.einsum("ij,jk->ik", [x, y])) == {
str(tc.uri("state", "collection", "tensor", "einsum")): {
Expand Down Expand Up @@ -76,14 +76,14 @@ def test_tensor_internal_route_helpers_are_not_public_api():


def test_tensor_reverse_add_and_mul_use_tensor_subject():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))

assert _json(1 + x) == {"$x/add": {"r": 1}}
assert _json(2 * x) == {"$x/mul": {"r": 2}}


def test_tensor_reverse_sub_and_div_require_tensor_lhs():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))

with pytest.raises(TypeError, match="reverse subtraction"):
_ = 1 - x
Expand All @@ -93,8 +93,8 @@ def test_tensor_reverse_sub_and_div_require_tensor_lhs():


def test_tensor_binary_ops_emit_minimal_payload_when_known():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
y = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("y")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
y = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("y")))

assert _json(x + y) == {
"$x/add": {
Expand All @@ -104,8 +104,8 @@ def test_tensor_binary_ops_emit_minimal_payload_when_known():


def test_tensor_matmul_emits_minimal_payload_when_known():
lhs = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("lhs")))
rhs = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("rhs")))
lhs = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("lhs")))
rhs = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("rhs")))

assert _json(lhs @ rhs) == {
"$lhs/matmul": {
Expand All @@ -115,7 +115,7 @@ def test_tensor_matmul_emits_minimal_payload_when_known():


def test_tensor_logical_not_emits_minimal_payload_when_known():
tensor = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("tensor")))
tensor = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("tensor")))

assert _json(tensor.logical_not()) == {
"$tensor/not": {
Expand All @@ -124,7 +124,7 @@ def test_tensor_logical_not_emits_minimal_payload_when_known():


def test_tensor_records_view_ops_for_symbolic_transforms():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))

y = x.transpose([1, 0]).broadcast([3, 2, 4]).reshape([24]).slice([0, 10])

Expand All @@ -148,7 +148,7 @@ def broadcast(self, shape):
def reshape(self, shape):
return NativeTensor(list(shape))

x = tc.state.Tensor(native=NativeTensor([2, 3]))
x = tc.Tensor(native=NativeTensor([2, 3]))

y = x.transpose([1, 0]).broadcast([4, 3, 2]).reshape([24])

Expand All @@ -159,7 +159,7 @@ def reshape(self, shape):


def test_tensor_view_spec_is_canonicalized_from_transform_chain():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))

y = x.transpose([1, 0]).reshape([6]).broadcast([2, 6])
spec = y.view_spec()
Expand Down Expand Up @@ -188,7 +188,7 @@ def apply_view_spec(self, spec):
current = current.apply_view_op(op)
return current

x = tc.state.Tensor(native=Adapter([2, 3]))
x = tc.Tensor(native=Adapter([2, 3]))
y = x.transpose([1, 0])

z = y.materialize_view_spec()
Expand All @@ -212,7 +212,7 @@ def apply_view_wire(self, wire):
self.wire = wire
return Adapter([6])

x = tc.state.Tensor(native=Adapter([2, 3]))
x = tc.Tensor(native=Adapter([2, 3]))
y = x.transpose([1, 0])
y._view_ops_materialized = False

Expand All @@ -222,7 +222,7 @@ def apply_view_wire(self, wire):


def test_view_spec_compiles_transpose_and_broadcast_to_view_schema():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
spec = x.transpose([1, 0]).broadcast([3, 2]).view_spec()

schema = spec.to_view_schema(base_shape=[1, 3])
Expand All @@ -236,7 +236,7 @@ def test_view_spec_compiles_transpose_and_broadcast_to_view_schema():
]

def test_tensor_to_view_schema_requires_base_shape_for_symbolic_tensors():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x"))).transpose([1, 0])
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x"))).transpose([1, 0])

with pytest.raises(TypeError, match="base_shape"):
x.to_view_schema()
Expand All @@ -250,7 +250,7 @@ def __init__(self, shape):
def transpose(self, permutation):
return NativeTensor([self.shape[i] for i in permutation])

x = tc.state.Tensor(native=NativeTensor([2, 3])).transpose([1, 0])
x = tc.Tensor(native=NativeTensor([2, 3])).transpose([1, 0])
schema = x.to_view_schema()
schema_json = schema.to_json()

Expand All @@ -262,7 +262,7 @@ def transpose(self, permutation):


def test_view_spec_compiles_slice_at_and_in_to_view_schema():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
spec = x.slice([1, (0, 4, 2)]).view_spec()

schema = spec.to_view_schema(base_shape=[3, 4])
Expand All @@ -276,15 +276,15 @@ def test_view_spec_compiles_slice_at_and_in_to_view_schema():


def test_view_spec_slice_rank_mismatch_raises():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
spec = x.slice([0]).view_spec()

with pytest.raises(ValueError, match="rank"):
spec.to_view_schema(base_shape=[2, 3])


def test_tensor_storage_schema_compiles_concrete_shape_and_layout():
x = tc.state.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))
x = tc.Tensor(ref=tc.state.TCRef(tc.state.IdRef("x")))

schema = x.to_storage_schema(base_shape=[2, 3], layout="sparse", sparse_axis=1)

Expand Down
35 changes: 25 additions & 10 deletions py/tests/test_execute_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import pytest
import tinychain as tc
import tinychain._local as tc_local


class _Value:
Expand Down Expand Up @@ -94,11 +95,21 @@ def test_execute_decodes_typed_tensor_when_available(monkeypatch):
}
monkeypatch.setattr(tc, "_dispatch_execute", lambda _op: _Response(payload, status=200))

result = tc.execute(tc.opref.get("/state/tensor"))
assert isinstance(result, tc.Tensor)
assert result.dtype == "u64"
assert result.shape == [2]
assert result.values == [10, 11]
try:
local = tc_local.backend()
except ImportError:
local = None

native_tensor = getattr(local, "Tensor", None) if local is not None else None
if native_tensor is not None and hasattr(native_tensor, "dense_u64"):
result = tc.execute(tc.opref.get("/state/collection/tensor"))
assert isinstance(result, tc.Tensor)
assert result.dtype == "u64"
assert result.shape == [2]
assert result.values == [10, 11]
else:
with pytest.raises(TypeError, match="cannot decode tensor dtype"):
tc.execute(tc.opref.get("/state/collection/tensor"))


def test_execute_decodes_f64_tensor_when_available(monkeypatch):
Expand All @@ -110,17 +121,21 @@ def test_execute_decodes_f64_tensor_when_available(monkeypatch):
}
monkeypatch.setattr(tc, "_dispatch_execute", lambda _op: _Response(payload, status=200))

local = tc._local.backend()
native_tensor = getattr(local, "Tensor", None)
try:
local = tc_local.backend()
except ImportError:
local = None

native_tensor = getattr(local, "Tensor", None) if local is not None else None
if native_tensor is not None and hasattr(native_tensor, "dense_f64"):
result = tc.execute(tc.opref.get("/state/tensor"))
result = tc.execute(tc.opref.get("/state/collection/tensor"))
assert isinstance(result, tc.Tensor)
assert result.dtype == "f64"
assert result.shape == [2]
assert result.values == [1.25, 2.5]
else:
with pytest.raises(TypeError, match="cannot decode tensor dtype"):
tc.execute(tc.opref.get("/state/tensor"))
tc.execute(tc.opref.get("/state/collection/tensor"))


def test_execute_rejects_unknown_tensor_dtype(monkeypatch):
Expand All @@ -133,7 +148,7 @@ def test_execute_rejects_unknown_tensor_dtype(monkeypatch):
monkeypatch.setattr(tc, "_dispatch_execute", lambda _op: _Response(payload, status=200))

with pytest.raises(TypeError, match="unsupported tensor dtype"):
tc.execute(tc.opref.get("/state/tensor"))
tc.execute(tc.opref.get("/state/collection/tensor"))


def test_execute_decodes_opdef_map_to_python_opdef(monkeypatch):
Expand Down
Loading