Skip to content

Commit eb6aa48

Browse files
committed
Enhance transformers to preserve structured metadata and alarms in outputs; update documentation and examples accordingly.
1 parent 9dc7c35 commit eb6aa48

File tree

8 files changed

+324
-10
lines changed

8 files changed

+324
-10
lines changed

docs/source/interfaces.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ Alarm Behavior
9191
``NO_ALARM=0``, ``HIHI=3``, ``HIGH=4``, ``LOLO=5``, ``LOW=6``.
9292
- Explicit ``alarm`` payload overrides computed alarm.
9393
- Non-scalars do not compute alarms, but explicit ``alarm`` payloads are accepted.
94+
- ``p4p`` client attempts a structured put first; if the target rejects it, it
95+
retries with a value-only put.
9496

9597
Model Alarm Override
9698
~~~~~~~~~~~~~~~~~~~~
@@ -99,6 +101,11 @@ Models can publish structured output with explicit alarm fields (for example
99101
``{"PV": {"value": 1.0, "alarm": {...}}}``), and ``ModelObserver`` preserves
100102
that structure when publishing downstream.
101103

104+
In ``examples/base/local/deployment_config_p4p_alarm.yaml`` this passes through
105+
an ``output_transformer`` direct-symbol mapping
106+
(``ML:LOCAL:TEST_S -> ML:LOCAL:TEST_S``), which preserves ``alarm`` and other
107+
non-``value`` fields.
108+
102109
See example:
103110

104111
- ``examples/base/local/deployment_config_p4p_alarm.yaml``

docs/source/transformers.rst

Lines changed: 157 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,168 @@ This page explains how to use transformers in your project.
55

66
Overview
77
--------
8-
Transformers are used to transform data from one format to another...
8+
Transformers convert message payloads from one shape to another before model
9+
evaluation or interface publication.
10+
11+
All transformer modules follow the
12+
:class:`~poly_lithic.src.transformers.BaseTransformer.BaseTransformer` contract
13+
and expose:
14+
15+
- ``handler(pv_name, value)`` to receive incoming struct data
16+
- ``transform()`` to produce output values
917

1018
Available Transformers
1119
----------------------
20+
1221
- :class:`~poly_lithic.src.transformers.BaseTransformers.SimpleTransformer`
1322
- :class:`~poly_lithic.src.transformers.BaseTransformers.CAImageTransfomer`
1423
- :class:`~poly_lithic.src.transformers.BaseTransformers.PassThroughTransformer`
1524
- :class:`~poly_lithic.src.transformers.CompoundTransformer.CompoundTransformer`
1625

17-
Examples
18-
--------
19-
Here are some examples of how to use transformers...
26+
Transformer Configs
27+
-------------------
28+
29+
.. list-table::
30+
:header-rows: 1
31+
:widths: 24 46 30
32+
33+
* - Module
34+
- Description
35+
- YAML reference
36+
* - ``SimpleTransformer``
37+
- Scalar/array formula transform from input symbols to output variables
38+
- See sample below
39+
* - ``CAImageTransformer``
40+
- Reconstruct image arrays from flattened channel + X + Y inputs
41+
- See sample below
42+
* - ``PassThroughTransformer``
43+
- Relabel and forward values without numeric transformation
44+
- See sample below
45+
* - ``CompoundTransformer``
46+
- Run multiple transformers in one module
47+
- See sample below
48+
49+
Metadata Propagation
50+
--------------------
51+
52+
Transformer outputs may be either plain values or structured payloads with
53+
``value`` and additional fields:
54+
55+
- ``PassThroughTransformer`` preserves non-``value`` fields (for example
56+
``alarm``, ``timestamp``, ``metadata``) when they are present in input
57+
structs.
58+
- ``SimpleTransformer`` preserves non-``value`` fields only for direct-symbol
59+
formulas (for example ``OUT = IN``). Computed formulas emit value-only output.
60+
- ``TransformerObserver`` forwards structured outputs unchanged and wraps plain
61+
outputs as ``{"value": ...}``.
62+
63+
This makes direct pass-through paths suitable for carrying alarm payloads across
64+
transformer stages.
65+
66+
``SimpleTransformer`` Sample Configuration
67+
------------------------------------------
68+
69+
.. code-block:: yaml
70+
71+
modules:
72+
input_transformer:
73+
name: "input_transformer"
74+
type: "transformer.SimpleTransformer"
75+
pub: "model_input"
76+
sub:
77+
- "system_input"
78+
module_args: None
79+
config:
80+
symbols:
81+
- "LUME:MLFLOW:TEST_B"
82+
- "LUME:MLFLOW:TEST_A"
83+
variables:
84+
x2:
85+
formula: "LUME:MLFLOW:TEST_B"
86+
x1:
87+
formula: "LUME:MLFLOW:TEST_A"
88+
89+
``CAImageTransformer`` Sample Configuration
90+
-------------------------------------------
91+
92+
.. code-block:: yaml
93+
94+
modules:
95+
image_transformer:
96+
name: "image_transformer"
97+
type: "transformer.CAImageTransformer"
98+
pub: "model_input"
99+
sub:
100+
- "update"
101+
module_args: None
102+
config:
103+
variables:
104+
img_1:
105+
img_ch: "MY_TEST_CA"
106+
img_x_ch: "MY_TEST_CA_X"
107+
img_y_ch: "MY_TEST_CA_Y"
108+
img_2:
109+
img_ch: "MY_TEST_C2"
110+
img_x_ch: "MY_TEST_CA_X2"
111+
img_y_ch: "MY_TEST_CA_Y2"
112+
113+
``PassThroughTransformer`` Sample Configuration
114+
-----------------------------------------------
115+
116+
.. code-block:: yaml
117+
118+
modules:
119+
output_transformer:
120+
name: "output_transformer"
121+
type: "transformer.PassThroughTransformer"
122+
pub: "system_output"
123+
sub:
124+
- "model_output"
125+
module_args: None
126+
config:
127+
variables:
128+
LUME:MLFLOW:TEST_IMAGE: "y_img"
129+
130+
``CompoundTransformer`` Sample Configuration
131+
--------------------------------------------
132+
133+
.. caution::
134+
135+
This module may be deprecated in the future because the pub-sub model can
136+
replace most compound-transformer use cases.
137+
138+
.. code-block:: yaml
139+
140+
modules:
141+
compound_transformer:
142+
name: "compound_transformer"
143+
type: "transformer.CompoundTransformer"
144+
pub: "model_input"
145+
sub:
146+
- "update"
147+
module_args: None
148+
config:
149+
transformers:
150+
transformer_1:
151+
type: "SimpleTransformer"
152+
config:
153+
symbols:
154+
- "MY_TEST_A"
155+
- "MY_TEST_B"
156+
variables:
157+
x2:
158+
formula: "MY_TEST_A*2"
159+
x1:
160+
formula: "MY_TEST_B+MY_TEST_A"
161+
transformer_2:
162+
type: "CAImageTransformer"
163+
config:
164+
variables:
165+
img_1:
166+
img_ch: "MY_TEST_CA"
167+
img_x_ch: "MY_TEST_CA_X"
168+
img_y_ch: "MY_TEST_CA_Y"
169+
img_2:
170+
img_ch: "MY_TEST_C2"
171+
img_x_ch: "MY_TEST_CA_X2"
172+
img_y_ch: "MY_TEST_CA_Y2"

examples/base/local/deployment_config_p4p_alarm.yaml

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
deployment:
22
type: "continuous"
3-
rate: .1 #seconds
3+
rate: .001 #seconds
44

55
modules:
66
p4p_server:
@@ -10,7 +10,7 @@ modules:
1010
- "in_interface"
1111
sub:
1212
- "get_all"
13-
- "model_out"
13+
- "out_transformer"
1414
module_args: None
1515
config:
1616
EPICS_PVA_NAME_SERVERS: "localhost:5075"
@@ -125,3 +125,18 @@ modules:
125125
variables:
126126
max:
127127
type: "scalar"
128+
129+
output_transformer:
130+
name: "output_transformer"
131+
type: "transformer.SimpleTransformer"
132+
pub: "out_transformer"
133+
sub: "model_out"
134+
module_args: None
135+
config:
136+
symbols:
137+
- "ML:LOCAL:TEST_S"
138+
variables:
139+
# Direct-symbol mapping intentionally preserves non-value fields
140+
# (e.g. alarm/timestamp/metadata) from model_out.
141+
ML:LOCAL:TEST_S:
142+
formula: "ML:LOCAL:TEST_S"

poly_lithic/src/transformers/BaseTransformers.py

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@ def __init__(self, config):
3131
for key, value in self.pv_mapping.items():
3232
self.__validate_formulas(value['formula'])
3333
self.latest_input = {symbol: None for symbol in self.input_list}
34+
self.latest_input_struct = {symbol: None for symbol in self.input_list}
3435
self.latest_transformed = {key: 0 for key in self.pv_mapping.keys()}
3536
self.updated = False
3637
self.handler_time = None
3738
self.formulas = {}
3839
self.lambdified_formulas = {}
40+
self.direct_formula_inputs = {}
41+
self.renamed_symbol_lookup = {
42+
symbol.replace(':', '_'): symbol for symbol in self.input_list
43+
}
3944
for key, value in self.pv_mapping.items():
4045
self.formulas[key] = sp.sympify(value['formula'].replace(':', '_'))
46+
self.direct_formula_inputs[key] = self.__get_direct_input_symbol(
47+
self.formulas[key]
48+
)
4149
input_list_renamed = [
4250
symbol.replace(':', '_') for symbol in self.input_list
4351
]
@@ -53,11 +61,17 @@ def __validate_formulas(self, formula: str):
5361
except Exception as e:
5462
raise Exception(f'Invalid formula: {formula}: {e}')
5563

64+
def __get_direct_input_symbol(self, formula_expr):
65+
if isinstance(formula_expr, sp.Symbol):
66+
return self.renamed_symbol_lookup.get(str(formula_expr))
67+
return None
68+
5669
def handler(self, pv_name, value):
5770
# logger.debug(f"SimpleTransformer handler for {pv_name} with value {value}")
5871

5972
# chek if pv_name is in sel.input_list
6073
if pv_name in self.input_list:
74+
self.latest_input_struct[pv_name] = dict(value)
6175
# assert value is float
6276
try:
6377
if isinstance(value['value'], (float, int, np.float32)):
@@ -161,6 +175,24 @@ def transform(self):
161175
raise e
162176

163177
for key, value in transformed.items():
178+
direct_input = self.direct_formula_inputs.get(key)
179+
input_struct = (
180+
self.latest_input_struct.get(direct_input)
181+
if direct_input is not None
182+
else None
183+
)
184+
if isinstance(input_struct, dict):
185+
passthrough_fields = {
186+
field_name: field_value
187+
for field_name, field_value in input_struct.items()
188+
if field_name != 'value'
189+
}
190+
if passthrough_fields:
191+
self.latest_transformed[key] = {
192+
'value': value,
193+
**passthrough_fields,
194+
}
195+
continue
164196
self.latest_transformed[key] = value
165197
self.updated = True
166198

@@ -239,12 +271,14 @@ def __init__(self, config):
239271
# config is a dictionary of output:intput pairs
240272
pv_mapping = config['variables']
241273
self.latest_input = {}
274+
self.latest_input_struct = {}
242275
self.latest_transformed = {}
243276
self.updated = False
244277
self.input_list = list(pv_mapping.values())
245278

246279
for key, value in pv_mapping.items():
247280
self.latest_input[value] = None
281+
self.latest_input_struct[value] = None
248282
self.latest_transformed[key] = None
249283
self.pv_mapping = pv_mapping
250284

@@ -253,6 +287,7 @@ def __init__(self, config):
253287
def handler(self, pv_name, value):
254288
time_start = time.time()
255289
logger.debug(f'PassThroughTransformer handler for {pv_name}')
290+
self.latest_input_struct[pv_name] = dict(value)
256291
self.latest_input[pv_name] = value['value']
257292
if all([value is not None for value in self.latest_input.values()]):
258293
self.transform()
@@ -263,10 +298,31 @@ def handler(self, pv_name, value):
263298
def transform(self):
264299
logger.debug('Transforming')
265300
for key, value in self.pv_mapping.items():
266-
self.latest_transformed[key] = self.latest_input[value]
301+
input_value = self.latest_input[value]
302+
input_struct = self.latest_input_struct.get(value)
303+
if isinstance(input_struct, dict):
304+
passthrough_fields = {
305+
field_name: field_value
306+
for field_name, field_value in input_struct.items()
307+
if field_name != 'value'
308+
}
309+
if passthrough_fields:
310+
self.latest_transformed[key] = {
311+
'value': input_value,
312+
**passthrough_fields,
313+
}
314+
else:
315+
self.latest_transformed[key] = input_value
316+
else:
317+
self.latest_transformed[key] = input_value
267318

268-
if isinstance(self.latest_input[value], np.ndarray):
269-
if self.latest_input[value].shape != self.latest_transformed[key].shape:
319+
transformed_value = (
320+
self.latest_transformed[key]['value']
321+
if isinstance(self.latest_transformed[key], dict)
322+
else self.latest_transformed[key]
323+
)
324+
if isinstance(input_value, np.ndarray):
325+
if input_value.shape != transformed_value.shape:
270326
logger.error(f'Shape mismatch between input and output for {key}')
271327
self.updated = True
272328

poly_lithic/src/utils/messaging.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,10 @@ def update(self, message: Message) -> Message | list[Message]:
261261
values = self.transformer.latest_transformed
262262
message_dict = {}
263263
for key, value in values.items():
264-
message_dict[key] = {'value': value}
264+
if isinstance(value, dict) and 'value' in value:
265+
message_dict[key] = value
266+
else:
267+
message_dict[key] = {'value': value}
265268

266269
self.transformer.updated = False
267270
return Message(topic=self.topic, source=str(self), value=message_dict)

readme.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,9 @@ return {
327327
```
328328

329329
This is supported by ``ModelObserver`` and passed through to interfaces.
330+
In ``examples/base/local/deployment_config_p4p_alarm.yaml`` this now goes through an
331+
``output_transformer`` direct-symbol mapping (``ML:LOCAL:TEST_S -> ML:LOCAL:TEST_S``),
332+
which preserves ``alarm`` and other non-``value`` fields.
330333
See runnable example:
331334

332335
- config: ``examples/base/local/deployment_config_p4p_alarm.yaml``

0 commit comments

Comments
 (0)