Replies: 3 comments 5 replies
|
Potential solutions to avoid this complexity:
The wrapper could hand the external calculator a subclass whose value reads directly from the core's Parameter class ProxyParameter(refnx.analysis.Parameter):
def __init__(self, es_param, name):
super().__init__(es_param.value, name=name)
self._es = es_param
@property
def value(self):
return self._es.value_no_call_back
@value.setter
def value(self, v): # backend writes (constraints, refnx-side fits)
self._es.value = v # land directly in the core parameterThis assumes that external calculator is Python and we can subclass its state. |
We could also drop the persistent storage dict and build the refnx structure from easyscience model at evaluation time (as it was done at some point with the CrysFML calculator in diffraction) def calculate(self, q, model): # model = easyscience Model, not a name
structure = reflect.Structure([
reflect.Slab(l.thickness.value, l.material.sld.value, l.roughness.value)
for l in model.layers
])
return reflect.ReflectModel(structure, scale=model.scale.value, ...)(q)We could then get rid of the entire binding thing - ItemContainer, generate_bindings, _callback, switch etc. This loses the bidirectionality, though. |
|
I still would really like to know more about how This is a one-way communication, I don't really understand why and how |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
How a refnx object is created and kept in sync with a Parameter
This writeup is a description of why and how we use the interface bindings and what functionality needs to remain during the potential rewrite, as proposed in #7 and #160.
The easyscience
Parameteris the persistent, user-facing "source of truth"The refnx object is transient backend state,
The binding layer - a
propertystored inParameter._callbackis the only bridge between them. It is rebuilt on calculator switch.1. The static picture
Example: a
Material('Si', sld=6.335). Its two parameters are mirrored into onereflect.SLDobject, keyed by the material's unique_namein the wrapper's storage dict. Note the name translation: easyscience sayssld/isld, refnx saysreal/imag`.flowchart LR subgraph ES["easyscience model"] M["Material 'Si'"] P1["Parameter sld<br/>_scalar = 6.335 (value, unit, variance)<br/>min / max / fixed<br/>_callback ●"] P2["Parameter isld<br/>_scalar = 0.0<br/>_callback ●"] M --> P1 M --> P2 end subgraph BIND["binding layer"] IC["ItemContainer<br/>link_name = material.unique_name<br/>name_conversion: sld→real, isld→imag<br/>getter = wrapper.get_material_value<br/>setter = wrapper.update_material"] end subgraph RX["refnx backend"] ST["RefnxWrapper.storage['material'][unique_name]"] SLD["reflect.SLD<br/>real = 6.335<br/>imag = 0.0"] ST --> SLD end P1 -- "_callback = property(fget, fset)" --> IC P2 -- "_callback" --> IC IC --> STParameter._callbackis set inInterfaceFactoryTemplate.generate_bindings(core:src/easyscience/fitting/calculators/interface_factory.py:170) from theItemContainerreturned byCalculatorBase.create(easyreflectometry:calculators/calculator_base.py:57).The name map
{'sld': 'real', 'isld': 'imag'}isRefnx._material_link(calculators/refnx/calculator.py:14).2. Creation -
generate_bindingsThe refnx object is born empty (placeholder zeros) and only receives real values when each parameter's freshly attached callback pushes its current value across. That final
fsetis what populates the backend.sequenceDiagram autonumber actor U as User participant P as Parameter sld participant F as InterfaceFactory participant C as Refnx calculator participant W as RefnxWrapper participant R as refnx.reflect U->>P: Material('Si', sld=6.335) Note over P: _scalar = 6.335<br/>_callback = property() - empty, no backend yet U->>F: generate_bindings(material) F->>C: create(material) C->>W: create_material(unique_name) W->>R: reflect.SLD(0, name=unique_name) Note over W,R: storage['material'][unique_name] = SLD<br/>placeholder zeros! C-->>F: [ItemContainer(unique_name, sld→real, getter, setter)] loop for each linkable Parameter (sld, isld) F->>P: v = value_no_call_back Note over P: read cached _scalar directly -<br/>callback not wired yet F->>P: _callback = item.make_prop('sld') F->>P: _callback.fset(v) P->>W: update_material(unique_name, real=6.335) W->>R: SLD.real.value = 6.335 Note over R: placeholder replaced with real value endSteps 3-8:
interface_factory.py:188-208. Steps 5-6:calculator_base.py:67-78andrefnx/wrapper.py:32-40.make_propbuilds the property with the sld→real key translation built in its closures (interface_factory.py:244-268).generate_bindingsin detailThe full method, from
src/easyscience/fitting/calculators/interface_factory.py:170-208:Step by step:
self.__interface_obj.create(model)- asks the current calculator (e.g. theRefnxclass) to create its backend representation ofmodel. For aMaterialthis callswrapper.create_material(unique_name), which storesreflect.SLD(0, name=...)instorage['material']with placeholder zeros. The return value is a list ofItemContainertuples; each one bundles everything needed to talk to that backend object:link_name: the storage key (the model object'sunique_name),name_conversion: dict mapping easyscience parameter names to backend attribute names(e.g.
{'sld': 'real', 'isld': 'imag'}fromRefnx._material_link),getter_fn/setter_fn: bound wrapper methods (get_material_value/update_material).model._get_linkable_attributes()- collects the model'sParameter/Descriptorobjects that are eligible for binding, so they can be matched by name against thename_conversionkeys.Outer loop over
class_links- a singlecreatecall can return severalItemContainers (aModelreturns containers for the model itself plus what it recursed into), so each container is processed in turn.Inner loop over
name_conversionkeys - each easyscience-side name (sld,isld,thickness, ...) is looked up among the model's linkable attributes. Names not present on this model are skipped.value_no_call_back- reads the parameter's cached scipp scalar directly, bypassing_callback.fget. This matters in two cases: the parameter may still hold a callback into the previous backend (during a calculator switch), or the new callback is not attached yet. Either way, reading throughvaluehere would return stale or wrong data. PlainDescriptorobjects have no callback machinery, hence thehasattrfallback to.value.item.make_prop(item_key)- builds the actual bridge: a Pythonpropertywhose closures capture the container'slink_name,name_conversion, and the wrapper's getter/setter (seeItemContainer.make_prop,interface_factory.py:244):fget()callsgetter_fn(link_name, name_conversion[item_key]), e.g.get_material_value(unique_name, 'real'),fset(value)callssetter_fn(link_name, **{name_conversion[item_key]: value}), e.g.update_material(unique_name, real=value).This property is assigned to
prop._callback, replacing the emptyproperty()the parameter was constructed with.prop._callback.fset(prop_value)- the initial push: the parameter's current value is written into the backend object, replacing the placeholder zero from step 1. From this point on, the two sides stay in sync through the set/get paths below.generate_bindingsis idempotent in practice:CalculatorBase.createchecksif key not in storagebefore creating backend objects, so re-running it refreshes callbacks without duplicating backend state. It is also whatswitch()relies on: after swappingcalculators,
update_bindings/generate_bindingsreplays this sequence against the new wrapper, and step 7 carries the parameter values across.3. Update and read - the callback at work
After binding, every
valueaccess goes through the callback. A set writes both homes (local scalar first, then push to refnx). A get trusts the backend: it pulls from refnx and re-syncs the local scalar if the backend drifted - which happens when refnx mutates its own parameters during a fit.sequenceDiagram autonumber actor U as User / minimizer participant P as Parameter sld participant W as RefnxWrapper participant R as reflect.SLD rect rgba(120,120,120,0.08) Note over U,R: SET - material.sld.value = 4.2 U->>P: value = 4.2 Note over P: clamp to [min, max]<br/>_scalar.value = 4.2 P->>W: _callback.fset(4.2) → update_material(unique_name, real=4.2) W->>R: SLD.real.value = 4.2 Note over P: notify observers end rect rgba(120,120,120,0.08) Note over U,R: GET - material.sld.value U->>P: value P->>W: _callback.fget() → get_material_value(unique_name, 'real') W->>R: read SLD.real.value R-->>P: 4.2 Note over P: if backend ≠ _scalar: _scalar.value = backend value<br/>(backend wins - re-sync on drift) P-->>U: 4.2 endSet path:
src/easyscience/variable/parameter.py:599-615(clamp, write scalar,fset, notify). Get path:parameter.py:573-577(fget, drift re-sync). During a fit,wrapper.calculate(refnx/wrapper.py:169) rebuilds aReflectModelfrom storage and evaluates reflectivity - the values it sees are whatever the callbacks last pushed.Two locations
switch()Because calculators are swappable (refnx ↔ BornAgain), the model cannot live inside any one
backend.
switch()throws the old wrapper away andupdate_bindings/generate_bindingsreplays diagram 2 against the new one - the Parameters carry the values across.
All reactions