Extending Frames and Operations / Frame・Operation 拡張ガイド¶
Use this guide when adding or materially changing a signal-processing operation, a public Frame method, or a Frame family. It defines the implementation path, ownership boundaries, and minimum tests expected in one change. 信号処理Operation、公開Frameメソッド、またはFrame familyを追加・実質変更する際は、 このガイドを使用してください。1つの変更で必要となる実装経路、責務境界、最低限のtestを定義します。
Repository agents start from
AGENTS.md, which routes
Frame, Operation, and Recipe extensions directly to this detailed workflow.
リポジトリ上のAgentは、まず
AGENTS.mdを読み、Frame、Operation、
Recipeの拡張についてこの詳細workflowへ直接進みます。
Choose the smallest extension / 最小の拡張単位を選ぶ¶
Do not start by creating a class. First decide which public contract is missing. 最初から class を作らず、欠けている公開契約を先に決めます。
| Need / 必要なもの | Add / 追加するもの | Do not add / 追加しないもの |
|---|---|---|
| Reusable numerical behavior only / 再利用可能な数値処理だけ | An AudioOperation in wandas/processing/ and focused processing tests / wandas/processing/ の AudioOperation と処理テスト |
Frame metadata or public API / Frame metadata や公開API |
| A chainable user operation / chain可能な利用者向け処理 | The AudioOperation, a thin public Frame method, and processing + Frame tests / AudioOperation、薄い公開Frameメソッド、processing + Frameテスト |
Numerical logic in the Frame method / Frameメソッド内の数値ロジック |
| A domain transition / 領域変換 | The operation plus construction of the existing destination Frame / Operationと既存の出力先Frame生成 | A new Frame when an existing domain model fits / 既存domain modelで表せる場合の新Frame |
| A genuinely new data domain / 本当に新しいデータ領域 | A new BaseFrame subclass with explicit axes and domain state / axisとdomain stateを明示したBaseFrame subclass |
A Frame that only renames an existing result / 既存結果を名前だけ変えるFrame |
| Portable replay / Recipeで再利用可能な処理 | @recipe_operation and an end-to-end Recipe probe / @recipe_operationとRecipe完全経路テスト |
Operation-specific branches in the Recipe model/compiler/serializer / Recipe中央層のoperation別分岐 |
A new Frame is justified only when data shape, axes, required constructor state, or domain-specific behavior cannot be represented clearly by an existing Frame. Different labels, units, or one extra convenience method are not enough. 新しい Frame は、data shape、axis、必須constructor state、またはdomain固有の振る舞いを 既存 Frame で明確に表現できない場合にだけ追加します。label、unit、便利メソッドが1つ違うだけでは 新しい Frame を作る理由になりません。
Ownership map / 責務の配置¶
Keep one owner for each kind of state or behavior. 状態や振る舞いの種類ごとに、正本を1か所に限定します。
| Concern / 関心事 | Owner / 正本 |
|---|---|
| Numerical algorithm and parameter validation / 数値アルゴリズムとparameter検証 | wandas/processing/ |
| Public method, input alignment, output Frame choice / 公開メソッド、入力整合、出力Frame選択 | wandas/frames/ |
| User/recording/domain metadata and axes / 利用者・収録・domain metadataとaxis | Frame constructor/helper |
| Runtime provenance / runtime provenance | Immutable lineage; operation_history is derived / immutableなlineage。operation_historyは派生view |
| Portable invocation intent / portableな呼出し意図 | @recipe_operation declaration |
| Numerical correctness / 数値的正しさ | tests/processing/ |
| Public Frame contract / 公開Frame契約 | tests/frames/ |
| Recipe extraction and replay / Recipe抽出と再実行 | tests/pipeline/ |
Frame methods must not duplicate processing parameters in metadata, reconstruct
lineage manually, or call .compute() while building a result. Processing code
must not construct Frames or mutate Frame metadata.
Frameメソッドは、処理parameterをmetadataへ重複保存したり、lineageを手作業で再構築したり、
結果構築中に.compute()を呼んではいけません。processingコードはFrameを生成したり、
Frame metadataを変更してはいけません。
Add an AudioOperation / AudioOperation を追加する¶
- Choose the nearest module such as
filters.py,spectral.py,temporal.py,stats.py, oreffects.py.filters.py、spectral.py、temporal.py、stats.py、effects.pyなど、 最も近いmoduleを選びます。 - Decide channel dependency before choosing the base class. Subclass
ChannelIndependentAudioOperationonly when every output channel depends on the corresponding input channel and the kernel satisfiesop(all_channels) == concatenate(op(channel) for channel in each_channel). Subclasses inherit and must preserve that semantic contract. UseAudioOperationfor cross-channel or parameter-dependent operations that cannot make the independence guarantee for every supported configuration. Give the operation a stable registrynameand pass all constructor configuration tosuper().__init__; the base snapshots caller-owned configuration. base classを選ぶ前にchannel依存性を判断します。各出力channelが対応入力channelだけに依存し、 上記の等価関係を全対応構成で保証できる場合だけChannelIndependentAudioOperationを継承します。 subclassもこの意味論を維持します。cross-channel処理や、全対応parameter構成では独立性を 保証できない処理はAudioOperationを使用します。安定したregistrynameを付け、 constructor設定をsuper().__init__へ渡します。 - Validate operation parameters in
validate_params()and implement the eager array kernel in_process().process()is the shared lazy Dask boundary; do not replace it merely to implement the algorithm.validate_params()でparameterを検証し、_process()にeager array kernelを実装します。process()は共通のlazy Dask境界なので、アルゴリズム実装のためだけに上書きしません。 - Override
calculate_output_shape()andcalculate_output_dtype()whenever shape or dtype differs from the input metadata. Dask needs both before compute. shapeまたはdtypeが入力metadataと異なる場合は、calculate_output_shape()とcalculate_output_dtype()を上書きします。Daskはcompute前に両方を必要とします。 - Use
get_metadata_updates()only for real domain state changes such as a new sampling rate. It is not a second parameter store.get_metadata_updates()はsampling rate変更など実際のdomain state変更だけに使い、 parameterの第2保存先にはしません。 - Register the class with
register_operation(). If the public class should be importable fromwandas.processing, update its eager import or lazy-operation mapping and__all__. classをregister_operation()へ登録します。wandas.processingから公開importする場合は、 eager importまたはlazy-operation mappingと__all__も更新します。
The following sketch shows the required boundaries. Use the exact validation and dtype appropriate for the real operation. 次の例は必要な責務境界を示します。実際のOperationに適した検証とdtypeを使用してください。
class Gain(ChannelIndependentAudioOperation[NDArrayReal, NDArrayReal]):
name = "gain"
_display = "gain"
def __init__(self, sampling_rate: float, factor: float) -> None:
super().__init__(sampling_rate, factor=factor)
def validate_params(self) -> None:
factor = self._config_value("factor")
if not isinstance(factor, int | float):
raise TypeError(
"Invalid gain factor\n"
f" Got: {type(factor).__name__}\n"
" Expected: int or float\n"
"Pass a numeric amplitude multiplier."
)
def _process(self, data: NDArrayReal) -> NDArrayReal:
return data * float(self._config_value("factor"))
def calculate_output_dtype(
self,
input_dtype: np.dtype[Any],
*input_dtypes: np.dtype[Any],
) -> np.dtype[Any]:
return np.result_type(input_dtype, np.float64)
register_operation(Gain)
Lazy Operations use an explicit provider declaration. The implementation module
must not call register_operation() while it is imported; the provider names the
module and class attribute that get_operation() will resolve after import:
register_lazy_operation(
"my_operation",
"my_package.operations",
attribute_name="MyOperation",
)
Keep the implementation class's name equal to the registered name. Public
export names, provider attribute names, and Operation registry names are separate
contracts. Eager and lazy providers cannot claim the same name, and only the
identical provider may be registered again. A lazy registration does not import
the module; an import, attribute lookup, or class-validation failure leaves the
provider and resolved-class cache unchanged. Import failures retain their original
exception. Hot reload and replacement of a class object are outside the registry
contract.
register_lazy_operation() requires the keyword-only attribute_name argument;
the former two-argument import-side-effect form is not supported. Recipe replay
implementations that are private still need an explicit provider, even though
they are not public wandas.processing exports.
A cross-channel kernel must use the conservative base explicitly:
class CommonModeRemoval(AudioOperation[NDArrayReal, NDArrayReal]):
name = "common_mode_removal"
def _process(self, data: NDArrayReal) -> NDArrayReal:
return data - data.mean(axis=0, keepdims=True)
ChannelIndependentAudioOperation expresses numerical meaning, not execution
topology. Do not assume or document a fixed task count, per-channel scheduling, or
public scheduler control. Its kernel must also accept multiple channels together
because zero or unknown channel counts, runtime inputs, and channel-axis-changing
outputs currently fall back to whole-frame execution.
Channel dependency and time dependency are separate decisions: channel-independent means that channels can be processed independently, while an operation may still require the complete time axis for each channel. チャネル依存性と時間依存性は別の 判断です。channel-independentでも、各チャネルの完全な時間軸を必要とする operationがあります。
A whole-Frame fallback must preserve the public numerical result, shape and dtype, metadata, lineage, Recipe behavior, and lazy boundary. Use focused numerical evidence at representative channel counts and parameter boundaries; task counts, timing, RSS, and worker topology are implementation evidence, not public promises.
Never retain a user-supplied mutable list, mapping, or NumPy array on a separate
public attribute that must stay synchronized with the base configuration. Read it
through _config_value() or _config_snapshot().
利用者が渡したmutableなlist、mapping、NumPy arrayを、base設定と同期が必要な別のpublic属性へ
保持しないでください。_config_value()または_config_snapshot()から読み出します。
Add a public Frame method / 公開 Frame メソッドを追加する¶
The method is an orchestration boundary. It validates Frame-level compatibility, creates or selects the operation, and returns a new Frame. For a same-domain unary operation, prefer the existing application helper: メソッドはorchestration境界です。Frame-levelの互換性を検証し、Operationを生成・選択して、 新しいFrameを返します。同一domainのunary operationでは既存の適用helperを優先します。
@recipe_operation("wandas.audio.gain")
def gain(self, factor: float) -> ChannelFrame:
return self._apply_operation_impl("gain", factor=factor)
Keep the public method name readable (low_pass_filter) even when the numerical
registry key differs (lowpass_filter). Treat these names as separate stable
contracts and test the mapping.
公開メソッド名は読みやすく保ち(low_pass_filter)、数値registry key
(lowpass_filter)と異なっていても構いません。両者を別々の安定契約として扱い、mappingをテストします。
For a domain transition, instantiate the destination Frame explicitly or use
_apply_operation_instance(..., output_frame_class=..., output_frame_kwargs=...)
when that helper expresses the contract without hidden state. Preserve channel IDs,
channel metadata, user metadata, sampling rate or its documented replacement,
source_time_offset, and the semantic lineage supplied by
_required_semantic_lineage().
domain transitionでは、出力先Frameを明示的に生成するか、隠れた状態を増やさず契約を表現できる場合に
_apply_operation_instance(..., output_frame_class=..., output_frame_kwargs=...)を使います。
channel ID、channel metadata、利用者metadata、sampling rateまたは文書化した変更値、
source_time_offset、_required_semantic_lineage()が供給するsemantic lineageを維持します。
For multiple Frame or external array inputs, define input order and alignment in the public method. Do not invent metadata for raw arrays. Preserve NumPy/Dask laziness and snapshot mutable inputs at the operation boundary where the contract requires value stability. 複数Frame入力やexternal array入力では、入力順序とalignmentを公開メソッドで定義します。 raw arrayにmetadataを捏造しません。NumPy/Daskのlazinessを維持し、値の安定性が契約上必要な場合は Operation境界でmutable inputをsnapshotします。
Add a new Frame family / 新しい Frame family を追加する¶
Before implementation, write down these invariants in the class docstring and tests: 実装前に、次のinvariantをclass docstringとtestへ記述します。
- array rank, channel axis, and domain axes; array rank、channel axis、domain axis。
- real or complex dtype expectations; real/complex dtypeの期待値。
- required constructor state and how it determines axes; 必須constructor stateと、それがaxisを決める方法。
- sampling-rate and
source_time_offsetmeaning; sampling rateとsource_time_offsetの意味。 - which operations preserve the family and which transition to another Frame. familyを維持するOperationと、別Frameへ遷移するOperation。
Then implement the smallest BaseFrame subclass that satisfies them:
その後、契約を満たす最小のBaseFrame subclassを実装します。
- Validate and normalize shape in
__init__, while keeping the internal array lazy.__init__でshapeを検証・正規化し、内部arrayはlazyなまま維持します。 - Set
_xarray_dim_suffixto the authoritative dimension names._xarray_dim_suffixへ正本となるdimension名を設定します。 - Implement required domain properties and
plot(). 必要なdomain propertyとplot()を実装します。 - Override
_get_additional_init_kwargs()for constructor state that must survive_create_new_instance()._create_new_instance()後も維持すべきconstructor stateは_get_additional_init_kwargs()で返します。 - Override
_get_dataframe_index()when DataFrame export has a domain axis. DataFrame exportにdomain axisがある場合は_get_dataframe_index()を上書きします。 - Export the class from
wandas.frames; add a top-levelwandasexport only when that is the intended public UX. Add it to the Frame API reference. classをwandas.framesからexportします。top-levelwandasexportは意図した公開UXの場合だけ 追加し、Frame API referenceにも追加します。
previous is the immediate receiver Frame for runtime data comparison in notebooks.
For binary or multi-input operations it follows only the left/base receiver. It is a
process-local strong reference and is not persisted in WDF. Never derive history or
Recipe structure from it: lineage remains the complete provenance authority for
Frame inputs, while RecipePlan owns reusable execution intent and external input
slots. Concrete external arrays are supplied again at replay.
previousはnotebookでruntime dataを比較するための、直前のreceiver Frameです。binaryまたは
multi-input operationではleft/base receiverだけを辿ります。process-localなstrong referenceであり、
WDFには永続化しません。historyやRecipe構造をpreviousから生成せず、Frame入力の完全なprovenanceは
lineage、再利用可能な実行意図とexternal input slotはRecipePlanを正本とします。具体的なexternal
arrayはreplay時に再度渡します。
Make the operation Recipe-capable / Operation を Recipe 対応にする¶
Use @recipe_operation when a public Frame call should be portable. The
declaration owns the stable operation ID, version, accepted ordered bindings,
parameter validation, semantic lineage, immutable registry entry, and
Frame-returning handler. The ID describes serialized behavior, not a Python
class path. Unary calls can use the default capture and handler; multi-Frame,
Frame-or-array, positional-only, and variadic calls need explicit bindings and
handlers.
呼出しをportableにする場合は公開Frameメソッドへ@recipe_operationを付けます。宣言が
stableなoperation ID、version、binding順序、parameter検証、Frameを返すhandlerを所有します。
IDはPython class pathではなくserializeされる振る舞いを表します。複数入力や特殊な引数形状では
明示的なbindingとhandlerが必要です。
Built-in declarations collected by wandas.pipeline.builtins enter the default
immutable registry. Add a new built-in owner to that collection. External
extensions derive a value with
default_recipe_registry().with_operation(recipe_definition(method)); they do
not mutate process-wide state. Parameter values use the shared canonical value
grammar rather than an operation-specific codec.
wandas.pipeline.builtinsが収集するbuilt-in宣言はdefault immutable registryへ入ります。
外部拡張は新しいregistryを派生し、process-wide stateを変更しません。parameter値は共有する
canonical value grammarを使い、operation固有codecを追加しません。
Prove portability through this end-to-end public path: portable性は次の公開完全経路で証明します。
public Frame operation
-> semantic lineage
-> RecipePlan.from_frame
-> RecipePlan.to_dict
-> RecipePlan.from_dict
-> RecipePlan.apply
Do not add operation-specific conditionals to the Recipe model, compiler, validator, executor, or serializer. If a runtime operation is not declared in the supplied registry, keep it runtime-only and let extraction fail at that node; never cut the graph or serialize a Python callable path. Recipeのmodel、compiler、validator、executor、serializerへoperation固有の条件分岐を追加しません。 registryに宣言されていないruntime operationはruntime-onlyとして残し、そのnodeで抽出を失敗させます。
Lineage order is part of the contract. For example, a rename_channels() node
must precede a later selector that uses the new channel name, so authored and
replayed selector meaning stays identical. Include typed Frame transitions and
true multi-Frame operations in the public probe; they are not special cases for
the Recipe model.
lineageの順序もcontractです。rename_channels()は新しいchannel名を使うselectorより先に
実行し、作成時とreplay時のselector意味を一致させます。typed Frame transitionと複数Frame
operationもRecipe modelの例外にせず、公開probeへ含めます。
The handler receives ordered runtime inputs and decoded immutable parameters.
It does not receive a compiler, executor, registry, import path, or mutable
context. Use frame bindings for Frame operands and array bindings for
external NumPy/Dask operands; do not embed arrays or compute lazy values.
Handler boundaryではordered inputとdecoded immutable parameterだけを受け取り、compilerや
registryなどの実行基盤を渡しません。Frameはframe、external arrayはarray bindingを使います。
Parameter validators are pure and run during complete-plan validation. Handlers check operation-specific runtime shape, sampling rate, class, and metadata at apply time; the common executor checks named inputs, graph kinds, and the authoritative semantic lineage. Keep omitted arguments omitted in serialized params so input-dependent defaults are resolved by the handler during replay. parameter validatorはpureで完全なplan検証時に実行します。handlerはapply時にruntimeのshape、 sampling rate、class、metadataを検証し、common executorはnamed input、graph kind、semantic lineageを検証します。省略引数はserialized paramsでも省略状態を保ち、input依存defaultはreplay時に handlerで解決します。
Add a focused test for the full probe. Cover at least the operation's metadata, source-time offsets, input order, mutation isolation, lazy behavior, deterministic schema roundtrip, and unknown operation/version rejection where applicable. 該当する場合はmetadata、offset、入力順序、mutation isolation、laziness、schema roundtrip、 未知operation/version拒否をfocused testで検証します。
Add tests with the feature / 機能と同時にテストを追加する¶
Tests should describe the public contract, not reproduce private implementation. Use deterministic known signals and independent expected values. testはprivate実装を再現するのではなく、公開契約を記述します。決定論的な既知信号と、 実装から独立した期待値を使います。
Processing tests / Processing テスト¶
Add focused tests under tests/processing/ for:
tests/processing/へ次のfocused testを追加します。
- constructor validation and WHAT/WHY/HOW errors; constructor検証とWHAT/WHY/HOW形式のerror。
- operation registration and the public registry key; Operation登録と公開registry key。
- output shape and dtype metadata before compute; compute前の出力shape/dtype metadata。
- no eager
.compute()whileprocess()builds the graph;process()のgraph構築中にeagerな.compute()がないこと。 - numerical equivalence to SciPy/librosa/MoSQITo for wrappers, or an analytical result for custom algorithms; wrapperではSciPy/librosa/MoSQIToとの数値一致、独自algorithmでは解析解との一致。
- input and caller-owned configuration remain unchanged. 入力と呼出し側所有の設定値が変更されないこと。
Frame tests / Frame テスト¶
Add focused tests under tests/frames/ for:
tests/frames/へ次のfocused testを追加します。
- correct return Frame type and chainability; 正しい戻りFrame型とchainability。
- original data, metadata, labels, offsets, and lineage remain unchanged; 元のdata、metadata、label、offset、lineageが変更されないこと。
- output metadata, axes, labels, and
source_time_offsetare correct; 出力metadata、axis、label、source_time_offsetが正しいこと。 - one public processing call creates exactly one lineage/history entry; 公開処理1回がlineage/historyを正確に1件生成すること。
- the result remains Dask-backed and lazy; 結果がDask-backedかつlazyであること。
- invalid shape, sampling rate, channel alignment, and boundary parameters fail explicitly; 不正なshape、sampling rate、channel alignment、境界parameterが明示的に失敗すること。
- domain transitions produce theoretically correct axes and dimensions. domain transition後のaxisとdimensionが理論値に一致すること。
For a new Frame family, also test constructor normalization, _create_new_instance()
state preservation, xarray dimension names, DataFrame index behavior, public exports,
and at least one inbound and outbound domain transition.
新しいFrame familyでは、constructor正規化、_create_new_instance()によるstate維持、
xarray dimension名、DataFrame index、public export、少なくとも1つの入出domain transitionもテストします。
Recipe and documentation tests / Recipe・文書テスト¶
If the operation is portable, add a complete extract/serialize/load/apply test under
tests/pipeline/. Cover mutation isolation, metadata and offset preservation, input
order for multiple inputs, lazy external Dask arrays, and unknown ID/version rejection
when relevant.
Operationがportableなら、tests/pipeline/へextract/serialize/load/apply完全経路テストを
追加します。該当する場合はmutation isolation、metadata/offset維持、複数入力の順序、
external Dask arrayのlaziness、未知ID/version拒否を含めます。
Update public docstrings, docs/src/api/, and a tutorial or how-to when users need a
new workflow explanation. Every public class and method must describe parameters,
return type, raised errors, laziness or compute behavior, metadata/axis effects, and a
minimal example.
新しいworkflow説明が必要なら、公開docstring、docs/src/api/、tutorialまたはhow-toも更新します。
すべての公開class/methodにparameter、戻り型、例外、lazinessまたはcompute動作、metadata/axisへの
影響、最小exampleを記載します。
Definition of done / 完了条件¶
Run focused tests first, then the repository gates appropriate to the change. 最初にfocused testを実行し、その後変更に応じたrepository gateを実行します。
uv run pytest tests/processing/<module-test>.py -q
uv run pytest tests/frames/<frame-test>.py -q
uv run pytest tests/pipeline -q # when Recipe behavior changes
uv run ruff check wandas tests scripts
uv run ty check wandas tests
uv run pytest -q
uv run mkdocs build --strict -f docs/mkdocs.yml
Before opening a PR, confirm: PR作成前に次を確認します。
- no input Frame or caller-owned configuration is mutated; 入力Frameや呼出し側所有の設定値が変更されていない。
- metadata, axes, labels, offsets, lineage, and output type change atomically; metadata、axis、label、offset、lineage、出力型がatomicに変更される。
- graph construction does not call Dask
.compute(); graph構築中にDask.compute()を呼ばない。 - the operation has one numerical registry key and, when portable, one Recipe ID; Operationが1つの数値registry keyと、portableな場合は1つのRecipe IDを持つ。
- every new built-in Frame has an explicit WDF codec plus round-trip and corruption coverage, and participates in the shared Frame-family contracts; 新しいbuilt-in Frameが明示的なWDF codec、round-trip/corruption testを持ち、共有Frame-family contractへ含まれている。
- every public Frame or mixin that owns
@recipe_operationmethods is covered by the built-in Recipe owner completeness contract;@recipe_operationmethodを所有する公開Frame/mixinがbuilt-in Recipe owner完全性contractに 含まれている。 - every lazy Operation has an explicit module/attribute provider, and its module import does not mutate the Operation provider mapping; すべてのlazy Operationがmodule/attributeを明示したproviderを持ち、module importがOperation provider mappingを変更しない。
- Operation names are collision-safe across eager and lazy providers, and the
public export, direct implementation import, and
get_operation()resolve the same class object; eager/lazyをまたぐOperation name衝突を拒否し、public export、実装moduleの直接import、get_operation()が同じclass objectを返す。 - tests cover Unit, Domain, and Integration layers where applicable; 該当するUnit、Domain、Integration層をtestがcoverする。
- no new compatibility shim, duplicate state, or operation-specific Recipe branch was introduced. 新しいcompatibility shim、状態重複、operation固有Recipe分岐を追加していない。
Agent route / Agent の参照順序¶
AGENTS.md routes Frame, Operation, and Recipe extensions directly to this
guide. Vendor adapters point to AGENTS.md; no custom agent or path-specific
instruction is required for this workflow.
AGENTS.mdはFrame、Operation、Recipeの拡張をこのガイドへ直接routeします。vendor
adapterはAGENTS.mdを参照し、このworkflowにcustom agentやpath-specific instructionは
必要ありません。
When Recipe portability is in scope, continue with the “Make the operation Recipe-capable” section above. Recipe portabilityがscopeに含まれる場合は、上の「Operation を Recipe 対応にする」節へ進みます。