1111__copyright__ = "Copyright (c) 2025 PySATL project"
1212__license__ = "SPDX-License-Identifier: MIT"
1313
14- from typing import TYPE_CHECKING , Protocol , cast
14+ from typing import TYPE_CHECKING , Any , Protocol , cast
1515
1616import numpy as np
1717
2020
2121if TYPE_CHECKING :
2222 from collections .abc import Mapping
23- from typing import Any
2423
2524 from pysatl_core .distributions .computation import (
2625 AnalyticalComputation ,
2726 FittedComputationMethod ,
28- Method ,
2927 )
3028 from pysatl_core .distributions .distribution import Distribution
29+ from pysatl_core .distributions .registry .graph import RegistryView
3130 from pysatl_core .types import GenericCharacteristicName , LabelName
3231
3332
@@ -126,16 +125,30 @@ def _pick_analytical_method(
126125 f"Characteristic '{ state } ' provides no labeled analytical computations."
127126 ) from exc
128127
128+ @staticmethod
129+ def _pick_loop_method (
130+ state : GenericCharacteristicName ,
131+ view : RegistryView ,
132+ ) -> Method [Any , Any ] | None :
133+ """
134+ Pick the first available self-loop method for a characteristic in a view.
135+ """
136+ loops = view .variants (state , state )
137+ if not loops :
138+ return None
139+ return cast (Method [Any , Any ], next (iter (loops .values ())).method )
140+
129141 def query_method (
130142 self , state : GenericCharacteristicName , distr : Distribution , ** options : Any
131143 ) -> Method [Any , Any ]:
132144 """
133145 Resolve a computation method for the target characteristic.
134146
135147 Resolution order:
136- 1. Analytical implementation from the distribution
137- 2. Cached fitted method (if caching enabled)
138- 3. Conversion path from an analytical characteristic via the graph
148+ 1. Cached fitted method (if caching enabled)
149+ 2. Analytical implementation for non-registry characteristics
150+ 3. First self-loop from the registry view
151+ 4. Conversion path from loop characteristics via the graph
139152
140153 Parameters
141154 ----------
@@ -157,34 +170,46 @@ def query_method(
157170 If no analytical base exists, no conversion path is found,
158171 or a cycle is detected.
159172 """
160- # 1. Check for analytical implementation
161- if state in distr .analytical_computations :
162- return self ._pick_analytical_method (state , distr .analytical_computations [state ])
163-
164- # 2. Check cache if enabled
173+ # 1. Check cache if enabled
165174 if self ._enable_caching :
166175 cached = self ._cache .get (state )
167176 if cached is not None :
168177 return cached
169178
170- # 3 . Require at least one analytical characteristic
179+ # 2 . Require at least one analytical characteristic
171180 if not distr .analytical_computations :
172181 raise RuntimeError (
173182 "Distribution provides no analytical computations to ground conversions."
174183 )
175184
176- # 4. Get filtered graph view for this distribution
177- reg = characteristic_registry ().view (distr )
185+ # 3. Non-registry characteristics are resolved directly.
186+ # It covers the situation where user is providing their analytical computation which isn't
187+ # in the graph
188+ registry = characteristic_registry ()
189+ if state not in registry .declared_characteristics :
190+ if state in distr .analytical_computations :
191+ return self ._pick_analytical_method (state , distr .analytical_computations [state ])
192+ raise RuntimeError (
193+ f"Characteristic '{ state } ' is not declared in the registry and has no "
194+ "analytical implementation in the distribution."
195+ )
196+
197+ # 4. Get filtered graph view for this distribution.
198+ view = registry .view (distr )
178199
179200 self ._push_guard (distr , state )
180201 try :
181- # 5. Try each analytical characteristic as a source
202+ loop_method = self ._pick_loop_method (state , view )
203+ if loop_method is not None :
204+ return loop_method
205+
206+ # 5. Try each loop characteristic as a source
182207 for src in distr .analytical_computations :
183- if src == state :
184- return self . _pick_analytical_method ( src , distr . analytical_computations [ src ])
208+ if not view . variants ( src , src ) :
209+ continue
185210
186211 # Find conversion path in the graph
187- path = reg .find_path (src , state )
212+ path = view .find_path (src , state )
188213 if not path :
189214 continue
190215
@@ -201,7 +226,8 @@ def query_method(
201226 return last_fitted
202227
203228 raise RuntimeError (
204- f"No conversion path from any analytical characteristic to '{ state } '."
229+ "No conversion path from any characteristic in "
230+ f"analytical_computations to '{ state } '."
205231 )
206232 finally :
207233 self ._pop_guard (distr , state )
0 commit comments