ANYfeSolver-results 2.2.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ """NumPy-only runtime for ANYfeSolver recovery capsules (contracts v1 and v2).
2
+
3
+ This package reconstructs ANYfeSolver response fields offline. It is plain
4
+ data in, arrays out: no executable or pickle payloads, and no imports from
5
+ ``anyfesolver``, scipy, ANYfem or ANYpost. ANYpost resolves dependencies and
6
+ calls this runtime; the closed derived-operation registry and the physics
7
+ live here.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from .capsule import (SCHEMA, VERSION, RUNTIME_ID, SUPPORTED_RUNTIME_IDS,
12
+ SUPPORTED_COMBINATIONS, PreparedResponse, prepare_response,
13
+ reconstruct_prepared_response, reconstruct_response,
14
+ validate_capsule)
15
+ from .errors import (CapsuleError, CapsuleValidationError, DerivationError,
16
+ ReconstructionError)
17
+ from .registry import OPERATIONS, DERIVATION_EXECUTION_API, derive_field
18
+ from .storage_validation import STORAGE_PREDICTION_API, validate_storage_prediction
19
+ from .formulas import shell_surface_component
20
+
21
+ __all__ = [
22
+ 'shell_surface_component',
23
+ 'STORAGE_PREDICTION_API',
24
+ 'validate_storage_prediction',
25
+ 'RUNTIME_ID',
26
+ 'SUPPORTED_RUNTIME_IDS',
27
+ 'SUPPORTED_COMBINATIONS',
28
+ 'SCHEMA',
29
+ 'VERSION',
30
+ 'OPERATIONS',
31
+ 'DERIVATION_EXECUTION_API',
32
+ 'PreparedResponse',
33
+ 'prepare_response',
34
+ 'reconstruct_prepared_response',
35
+ 'reconstruct_response',
36
+ 'derive_field',
37
+ 'validate_capsule',
38
+ 'CapsuleError',
39
+ 'CapsuleValidationError',
40
+ 'ReconstructionError',
41
+ 'DerivationError',
42
+ ]
@@ -0,0 +1,517 @@
1
+ """Capsule validation and response reconstruction (NumPy only)."""
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ from typing import Any, Mapping
6
+
7
+ import numpy as np
8
+
9
+ from .errors import CapsuleValidationError, ReconstructionError
10
+
11
+ SCHEMA = 'anysolver.recovery_capsule'
12
+ VERSION = 2
13
+ RUNTIME_ID = 'anysolver-results/2'
14
+ SUPPORTED_RUNTIME_IDS = ('anysolver-results/1','anysolver-results/2')
15
+ SUPPORTED_COMBINATIONS = ('legacy_blas/1','ordered_float64/1')
16
+ AVAILABILITIES = ('stored', 'reconstructable', 'unavailable')
17
+ # The completed-status set ANYpost admits for certified cases; a certified
18
+ # failure status is never part of it.
19
+ COMPLETED_STATUSES = ('completed', 'complete', 'success', 'succeeded', 'ok', 'converged')
20
+ GEOMETRY_NAMES = ('positions', 'triangles', 'lines', 'triangle_to_element',
21
+ 'line_to_element', 'node_ids', 'element_ids')
22
+
23
+
24
+ def _require(condition: bool, message: str) -> None:
25
+ if not condition:
26
+ raise CapsuleValidationError(message)
27
+
28
+
29
+ def _is_plain_json(value: Any) -> bool:
30
+ if value is None or type(value) in (bool, str):
31
+ return True
32
+ if type(value) is int:
33
+ return True
34
+ if type(value) is float:
35
+ # JSON has no NaN or infinity; a non-finite float is not plain data.
36
+ return math.isfinite(value)
37
+ # Solver snapshots can use JSON-compatible read-only dict/list
38
+ # subclasses. Their contents still obey the same finite plain-data
39
+ # admission; no arbitrary objects or executable archive values enter.
40
+ if isinstance(value, list):
41
+ return all(_is_plain_json(item) for item in value)
42
+ if isinstance(value, dict):
43
+ return all(type(key) is str and _is_plain_json(item) for key, item in value.items())
44
+ return False
45
+
46
+
47
+ def validate_capsule(capsule: Mapping[str, Any]) -> None:
48
+ """Lightweight shape/identity/status checks for a recovery capsule.
49
+
50
+ This is a structural admission check, not a certification: it refuses
51
+ malformed capsules so consumers never guess at layout, and it admits
52
+ failed cases (they stay discoverable) while reconstruction itself only
53
+ ever admits explicitly completed certified cases.
54
+ """
55
+ if not isinstance(capsule, Mapping):
56
+ raise CapsuleValidationError('capsule must be a mapping')
57
+ _require(capsule.get('schema') == SCHEMA, 'capsule schema is not ' + SCHEMA)
58
+ version = capsule.get('version')
59
+ _require(type(version) is int and version in (1,2), 'unsupported capsule version')
60
+ _require(capsule.get('runtime') == f'anysolver-results/{version}', 'capsule runtime identity mismatch')
61
+ expected = 'legacy_blas/1' if version == 1 else 'ordered_float64/1'
62
+ _require(capsule.get('combination',expected if version == 1 else None) == expected,
63
+ 'capsule combination identity mismatch')
64
+ for name in ('model_identity', 'mesh_identity'):
65
+ identity = capsule.get(name)
66
+ _require(type(identity) is str and identity, f'{name} must be a nonempty string')
67
+ _require(isinstance(capsule.get('solver_identity'), Mapping) and _is_plain_json(dict(capsule['solver_identity'])),
68
+ 'solver_identity must be a JSON mapping')
69
+ provenance = capsule.get('model_provenance')
70
+ if provenance is not None:
71
+ _require(isinstance(provenance, Mapping) and _is_plain_json(dict(provenance)),
72
+ 'model_provenance must be a JSON mapping when present')
73
+ digest = provenance.get('snapshot_content_digest')
74
+ _require(type(digest) is str and digest,
75
+ 'model_provenance needs a nonempty snapshot_content_digest')
76
+ recovery_metadata = capsule.get('model_recovery_metadata')
77
+ if recovery_metadata is not None:
78
+ # Shared once-per-model frames/sampling tables: plain JSON only.
79
+ _require(isinstance(recovery_metadata, Mapping)
80
+ and _is_plain_json(dict(recovery_metadata)),
81
+ 'model_recovery_metadata must be a JSON mapping when present')
82
+ constants = capsule.get('element_constants')
83
+ if constants is not None:
84
+ _require(isinstance(constants, Mapping), 'element_constants must be a mapping when present')
85
+ for element_id, values in constants.items():
86
+ _require(type(element_id) is str, 'element_constants keys must be strings')
87
+ _require(isinstance(values, Mapping) and _is_plain_json(dict(values)),
88
+ f'element_constants[{element_id!r}] must be a JSON mapping')
89
+ geometry = capsule.get('geometry')
90
+ _require(isinstance(geometry, Mapping), 'geometry must be a mapping')
91
+ for name, value in geometry.items():
92
+ _require(type(name) is str, 'geometry names must be strings')
93
+ _require(isinstance(value, np.ndarray) and value.dtype.kind in 'biuf',
94
+ f'geometry array {name!r} must be a numeric array')
95
+ if version == 2:
96
+ positions=geometry.get('positions')
97
+ if positions is not None:
98
+ _require(positions.ndim == 2 and positions.shape[1] == 3
99
+ and np.all(np.isfinite(positions)),'positions must be finite (node_count,3)')
100
+ for name in ('element_ids','node_ids'):
101
+ if name in geometry:
102
+ value=geometry[name]
103
+ _require(value.dtype.kind in 'iu' and value.ndim == 1,
104
+ f'{name} must be a one-dimensional integer identity table')
105
+ _require(len(np.unique(value)) == len(value),f'{name} must be unique')
106
+ if name == 'node_ids':
107
+ _require(positions is not None and len(value) == len(positions),'node_ids must match positions')
108
+ for topology,width in (('triangles',3),('lines',2)):
109
+ if topology in geometry:
110
+ indices=geometry[topology]
111
+ _require(positions is not None and indices.dtype.kind in 'iu'
112
+ and indices.ndim == 2 and indices.shape[1] == width,
113
+ f'{topology} must be dense node indices with width {width}')
114
+ _require(np.all(indices >= 0) and np.all(indices < len(positions)),
115
+ f'{topology} node indices are out of bounds')
116
+ for name, topology in (('triangle_to_element','triangles'),('line_to_element','lines')):
117
+ if name in geometry:
118
+ value=geometry[name]
119
+ _require(value.dtype.kind in 'iu' and value.ndim == 1,
120
+ f'{name} must be a one-dimensional integer mapping')
121
+ _require(topology in geometry and len(value) == len(geometry[topology]),
122
+ f'{name} must match {topology} rows')
123
+ _require('element_ids' in geometry,'dense element mappings require element_ids')
124
+ _require(np.all(value >= 0) and np.all(value < len(geometry['element_ids'])),
125
+ f'{name} must index element_ids')
126
+ patterns = capsule.get('patterns')
127
+ _require(type(patterns) is tuple, 'patterns must be a tuple')
128
+ _require(all(type(name) is str for name in patterns), 'pattern names must be strings')
129
+ _require(len(set(patterns)) == len(patterns), 'pattern names must be unique')
130
+ pattern_count = len(patterns)
131
+ coefficients = capsule.get('coefficients')
132
+ _require(isinstance(coefficients, np.ndarray) and coefficients.dtype == np.float64
133
+ and coefficients.ndim == 2 and coefficients.shape[1] == pattern_count,
134
+ 'coefficients must be float64 (case_count, pattern_count)')
135
+ _require(np.all(np.isfinite(coefficients)), 'coefficients must be finite')
136
+ case_count = coefficients.shape[0]
137
+ cases = capsule.get('cases')
138
+ _require(type(cases) is tuple and len(cases) == case_count,
139
+ 'cases must be a tuple matching the coefficient rows')
140
+ seen_cases = set()
141
+ for case in cases:
142
+ _require(isinstance(case, Mapping) and _is_plain_json(dict(case)),
143
+ 'each case must be a JSON mapping')
144
+ case_id = case.get('case_id')
145
+ _require(type(case_id) is str and case_id, 'each case needs a nonempty case_id')
146
+ _require(case_id not in seen_cases, 'case_id values must be unique')
147
+ seen_cases.add(case_id)
148
+ _require(type(case.get('status')) is str, f'case {case_id!r} needs a status string')
149
+ _require(type(case.get('certified')) is bool, f'case {case_id!r} needs a certified flag')
150
+ _require(type(case.get('route')) is str and case['route'],
151
+ f'case {case_id!r} needs a route string')
152
+ if case['certified']:
153
+ _require(case['status'] in COMPLETED_STATUSES,
154
+ f'case {case_id!r} is certified but its status is not a completed status')
155
+ active_indices = case.get('active_indices')
156
+ if active_indices is not None:
157
+ # One-based original-order pattern positions, unique and in range,
158
+ # validated exactly like a reconstruction request.
159
+ try:
160
+ _integer_active_indices(active_indices, pattern_count + 1)
161
+ except ReconstructionError as error:
162
+ raise CapsuleValidationError(str(error)) from error
163
+ portable = case.get('portable')
164
+ if portable is not None:
165
+ _require(type(portable) is bool, f'case {case_id!r} needs a boolean portable flag')
166
+ if portable:
167
+ _require(case['certified'],
168
+ f'case {case_id!r} is portable but not certified')
169
+ if 'displacement_portable' in case:
170
+ _require(case['displacement_portable'] is None or type(case['displacement_portable']) is bool,
171
+ 'displacement_portable must be boolean or null')
172
+ if 'unavailable_components' in case:
173
+ value=case['unavailable_components']
174
+ _require(isinstance(value,list) and all(type(key) is str for key in value)
175
+ and len(value)==len(set(value)), 'unavailable_components must contain unique strings')
176
+ if 'correction_reasons' in case:
177
+ reasons=case['correction_reasons']
178
+ _require(isinstance(reasons,Mapping) and all(type(key) is str and type(reason) is str
179
+ for key,reason in reasons.items()),'correction_reasons must map strings to strings')
180
+ descriptors = capsule.get('descriptors')
181
+ _require(type(descriptors) is tuple, 'descriptors must be a tuple')
182
+ keys = set()
183
+ pairs = set()
184
+ for descriptor in descriptors:
185
+ _require(isinstance(descriptor, Mapping) and _is_plain_json(dict(descriptor)),
186
+ 'each descriptor must be a JSON mapping')
187
+ key = descriptor.get('key')
188
+ _require(type(key) is str and key, 'each descriptor needs a nonempty key')
189
+ _require(key not in keys, 'descriptor keys must be unique')
190
+ keys.add(key)
191
+ for name in ('quantity_id', 'component', 'location', 'unit', 'basis',
192
+ 'sampling'):
193
+ _require(type(descriptor.get(name)) is str,
194
+ f'descriptor {key!r} needs a string {name}')
195
+ # `reconstruction` is an optional v1 extension: absent is admitted,
196
+ # but a present value must be a string (None is not a string).
197
+ if 'reconstruction' in descriptor:
198
+ _require(type(descriptor['reconstruction']) is str,
199
+ f'descriptor {key!r} needs a string reconstruction')
200
+ pair = (descriptor['quantity_id'], descriptor['component'])
201
+ _require(pair not in pairs,
202
+ 'descriptor (quantity_id, component) pairs must be unique')
203
+ pairs.add(pair)
204
+ _require(descriptor.get('availability') in AVAILABILITIES,
205
+ f'descriptor {key!r} has an unknown availability')
206
+ if version == 2:
207
+ for case in cases:
208
+ _require(type(case.get('portable')) is bool,'runtime2 cases require a portable flag')
209
+ _require(set(case.get('unavailable_components',())) <= keys,
210
+ 'unavailable_components reference unknown descriptors')
211
+ _require(set(case.get('correction_reasons',{})) <= keys,
212
+ 'correction_reasons reference unknown descriptors')
213
+ associations = capsule.get('associations')
214
+ _require(isinstance(associations, Mapping), 'associations must be a mapping')
215
+ for key, value in associations.items():
216
+ _require(type(key) is str, 'association keys must be strings')
217
+ _require(isinstance(value, np.ndarray) and value.ndim == 1 and value.dtype.kind in 'iu',
218
+ f'association {key!r} must be a 1-D integer array')
219
+ responses = capsule.get('responses')
220
+ _require(isinstance(responses, Mapping), 'responses must be a mapping')
221
+ spatial = {}
222
+ for key, value in responses.items():
223
+ _require(type(key) is str, 'response keys must be strings')
224
+ _require(isinstance(value, np.ndarray) and value.dtype == np.float64 and value.ndim == 2
225
+ and value.shape[1] == pattern_count + 1,
226
+ f'response {key!r} must be float64 (spatial_count, pattern_count+1)')
227
+ _require(np.all(np.isfinite(value)), f'response {key!r} must be finite')
228
+ _require(key in associations, f'response {key!r} has no association array')
229
+ _require(key in keys, f'response {key!r} has no descriptor')
230
+ association = associations[key]
231
+ _require(association.shape[0] == value.shape[0],
232
+ f'response {key!r} disagrees with its association length')
233
+ spatial[key] = value.shape[0]
234
+ stored = capsule.get('stored')
235
+ _require(isinstance(stored, Mapping), 'stored must be a mapping')
236
+ for key, value in stored.items():
237
+ _require(type(key) is str, 'stored keys must be strings')
238
+ _require(isinstance(value, Mapping), f'stored {key!r} must be a mapping')
239
+ indices = value.get('case_indices')
240
+ values = value.get('values')
241
+ _require(isinstance(indices, np.ndarray) and indices.ndim == 1 and indices.dtype.kind in 'iu',
242
+ f'stored {key!r} needs integer case_indices')
243
+ _require(isinstance(values, np.ndarray) and values.dtype == np.float64 and values.ndim == 2,
244
+ f'stored {key!r} needs float64 values (stored_case_count, spatial_count)')
245
+ _require(np.all(np.isfinite(values)), f'stored {key!r} values must be finite')
246
+ _require(indices.shape[0] == values.shape[0],
247
+ f'stored {key!r} case_indices and values disagree')
248
+ _require(np.all((indices >= 0) & (indices < case_count)),
249
+ f'stored {key!r} case_indices are out of range')
250
+ _require(len(set(indices.tolist())) == indices.shape[0],
251
+ f'stored {key!r} case_indices must be unique')
252
+ _require(key in associations, f'stored {key!r} has no association array')
253
+ _require(key in keys, f'stored {key!r} has no descriptor')
254
+ _require(associations[key].shape[0] == values.shape[1],
255
+ f'stored {key!r} disagrees with its association length')
256
+ if key in spatial:
257
+ _require(spatial[key] == values.shape[1],
258
+ f'stored {key!r} disagrees with the response spatial count')
259
+ derived = capsule.get('derived')
260
+ _require(isinstance(derived, Mapping), 'derived must be a mapping')
261
+ for key, specification in derived.items():
262
+ _require(type(key) is str, 'derived keys must be strings')
263
+ _require(isinstance(specification, Mapping) and _is_plain_json(dict(specification)),
264
+ f'derived {key!r} must be a JSON mapping')
265
+ _require(type(specification.get('operation')) is str,
266
+ f'derived {key!r} needs an operation')
267
+ inputs = specification.get('inputs')
268
+ _require(isinstance(inputs, Mapping), f'derived {key!r} needs an inputs mapping')
269
+ _require(key in keys, f'derived {key!r} has no descriptor')
270
+ for input_name, target in inputs.items():
271
+ _require(type(input_name) is str and type(target) is str,
272
+ f'derived {key!r} inputs must map names to component keys')
273
+ _require(target in keys,
274
+ f'derived {key!r} input {input_name!r} references '
275
+ f'unknown component {target!r}')
276
+ _require_acyclical_derived(derived)
277
+ if 'storage_prediction' in capsule:
278
+ _require(version == 2, 'storage prediction requires capsule2')
279
+ from .storage_validation import validate_storage_prediction
280
+ validate_storage_prediction(capsule['storage_prediction'], patterns=patterns,
281
+ descriptors=descriptors, associations=associations)
282
+ for key in capsule['storage_prediction']['targets']:
283
+ _require(key not in responses and key not in derived,
284
+ 'storage-only target cannot enter engineering replay')
285
+
286
+
287
+ def _require_acyclical_derived(derived: Mapping[str, Any]) -> None:
288
+ """Refuse derived specifications whose dependencies form a cycle."""
289
+ targets = {key: {target for target in specification.get('inputs', {}).values()
290
+ if target in derived}
291
+ for key, specification in derived.items()
292
+ if isinstance(specification, Mapping)}
293
+ state: dict = {}
294
+
295
+ def visit(key: str) -> None:
296
+ if state.get(key) == 1:
297
+ raise CapsuleValidationError(f'derived components form a cycle at {key!r}')
298
+ if state.get(key) == 2:
299
+ return
300
+ state[key] = 1
301
+ for target in targets.get(key, ()):
302
+ visit(target)
303
+ state[key] = 2
304
+
305
+ for key in targets:
306
+ visit(key)
307
+
308
+
309
+ def _as_float_array(values: Any, message: str) -> np.ndarray:
310
+ """Float64 conversion whose failure is the documented typed refusal."""
311
+ try:
312
+ return np.asarray(values, dtype=np.float64)
313
+ except (TypeError, ValueError, OverflowError) as error:
314
+ raise ReconstructionError(message) from error
315
+
316
+
317
+ def _integer_active_indices(active_indices: Any, column_count: int) -> np.ndarray:
318
+ """Strict 1-D integer pattern-column positions, unique and in range.
319
+
320
+ Fractional, boolean and duplicate indices are refused instead of being
321
+ silently coerced; the original sparse order of valid indices is preserved.
322
+ Range and uniqueness are validated on the original values before any
323
+ fixed-width conversion, so oversized plain integers (e.g. ``2**100``)
324
+ surface as the documented typed refusal instead of a raw OverflowError.
325
+ """
326
+ if isinstance(active_indices, (list, tuple)):
327
+ if any(type(index) is not int for index in active_indices):
328
+ raise ReconstructionError('active_indices must be integer indices')
329
+ if any(not 1 <= index < column_count for index in active_indices):
330
+ raise ReconstructionError('active_indices must reference pattern columns')
331
+ if len(set(active_indices)) != len(active_indices):
332
+ raise ReconstructionError('active_indices must be unique')
333
+ return np.asarray(active_indices, dtype=np.intp)
334
+ try:
335
+ active = np.asarray(active_indices)
336
+ except (TypeError, ValueError, OverflowError) as error:
337
+ raise ReconstructionError('active_indices must be an integer array') from error
338
+ if active.ndim != 1:
339
+ raise ReconstructionError('active_indices must be one dimensional')
340
+ if active.size and active.dtype.kind not in 'iu':
341
+ raise ReconstructionError('active_indices must be integer indices')
342
+ if active.size:
343
+ if np.any(active < 1) or np.any(active >= column_count):
344
+ raise ReconstructionError('active_indices must reference pattern columns')
345
+ if np.unique(active).shape[0] != active.shape[0]:
346
+ raise ReconstructionError('active_indices must be unique')
347
+ try:
348
+ active = active.astype(np.intp, copy=False)
349
+ except OverflowError as error:
350
+ raise ReconstructionError('active_indices must be integer indices') from error
351
+ return active
352
+
353
+
354
+ def _validated_combination(combination):
355
+ if type(combination) is not str or combination not in SUPPORTED_COMBINATIONS:
356
+ raise ReconstructionError('unsupported response combination')
357
+ return combination
358
+
359
+
360
+ def _combine_response(baseline: np.ndarray, selected: np.ndarray,
361
+ weights: np.ndarray, combination='legacy_blas/1') -> np.ndarray:
362
+ """Replay the named legacy BLAS or ordered binary64 accumulation.
363
+
364
+ Both the ordinary reconstruction and the prepared replay delegate to
365
+ this helper, so there is a single numerical implementation. An empty
366
+ active order reduces to the baseline alone.
367
+ """
368
+ combination = _validated_combination(combination)
369
+ if selected.shape[1] == 0:
370
+ result = np.array(baseline, dtype=np.float64, copy=True)
371
+ elif combination == 'ordered_float64/1':
372
+ accumulated = np.zeros(baseline.shape,dtype=np.float64)
373
+ term = np.empty(baseline.shape,dtype=np.float64)
374
+ with np.errstate(over='ignore',invalid='ignore'):
375
+ for column in range(selected.shape[1]):
376
+ np.multiply(selected[:,column],weights[column],out=term)
377
+ np.add(accumulated,term,out=accumulated)
378
+ result = np.add(baseline,accumulated)
379
+ if not np.all(np.isfinite(result)):
380
+ raise ReconstructionError('ordered response replay is nonfinite')
381
+ else:
382
+ result = baseline + selected @ weights
383
+ if combination == 'ordered_float64/1':
384
+ result=np.frombuffer(result.tobytes(),dtype=np.float64).reshape(result.shape)
385
+ result.setflags(write=False)
386
+ return result
387
+
388
+
389
+ def _validated_response_columns(responses: Any, active_indices: Any) -> tuple:
390
+ """Admitted float64 response matrix and its validated active columns."""
391
+ values = _as_float_array(responses, 'responses must be 2-D with a baseline column')
392
+ if values.ndim != 2 or values.shape[1] < 1:
393
+ raise ReconstructionError('responses must be 2-D with a baseline column')
394
+ if active_indices is None:
395
+ active = np.arange(1, values.shape[1], dtype=np.intp)
396
+ else:
397
+ active = _integer_active_indices(active_indices, values.shape[1])
398
+ if not np.all(np.isfinite(values)):
399
+ raise ReconstructionError('response columns must be finite')
400
+ return values, active
401
+
402
+
403
+ def _validated_weights(coefficients: Any, active_count: int) -> np.ndarray:
404
+ """Admitted 1-D finite weights matching the active pattern count."""
405
+ weights = _as_float_array(coefficients, 'coefficients must match the active pattern count')
406
+ if weights.ndim != 1 or weights.shape[0] != active_count:
407
+ raise ReconstructionError('coefficients must match the active pattern count')
408
+ if not np.all(np.isfinite(weights)):
409
+ raise ReconstructionError('coefficients must be finite')
410
+ return weights
411
+
412
+
413
+ def reconstruct_response(responses: Any, coefficients: Any, *, active_indices=None,
414
+ combination='legacy_blas/1') -> np.ndarray:
415
+ """Reconstruct one scalar spatial field from capsule response columns.
416
+
417
+ ``responses`` is a ``(spatial_count, pattern_count+1)`` float64 array whose
418
+ column 0 is the baseline (added exactly once). ``active_indices`` are
419
+ positions in the response columns (column 0 excluded); ``None`` means all
420
+ pattern columns. The default ``legacy_blas/1`` retains the original
421
+ ``responses[:, 0] + responses[:, active] @ coefficients`` expression.
422
+ ``ordered_float64/1`` materializes each multiply/add in active-column order
423
+ and adds the baseline last. Capsule2 consumers must explicitly select it;
424
+ the source exporter separately certifies exact bits or stores corrections.
425
+ """
426
+ values, active = _validated_response_columns(responses, active_indices)
427
+ weights = _validated_weights(coefficients, active.shape[0])
428
+ return _combine_response(values[:, 0], values[:, active], weights,combination)
429
+
430
+
431
+ _CREATE = object()
432
+
433
+
434
+ class PreparedResponse:
435
+ """Immutable owned baseline and selected response columns for replay.
436
+
437
+ Instances are created only by :func:`prepare_response`; the construction
438
+ token keeps archives from inventing a prepared object. ``baseline`` is
439
+ the copied baseline column, ``selected`` the copied original-order active
440
+ columns (exactly the array the ordinary reconstruction expression
441
+ materializes), both read-only float64 without any alias to caller-mutable
442
+ memory. ``nbytes`` is the exact total retained numeric bytes
443
+ (baseline + selected) as a Python int for archive cache accounting.
444
+ """
445
+
446
+ __slots__ = ('_baseline', '_selected', '_nbytes', '_combination')
447
+
448
+ def __setattr__(self,name,value):
449
+ if hasattr(self,name):
450
+ raise AttributeError('prepared responses are immutable')
451
+ object.__setattr__(self,name,value)
452
+
453
+ def __init__(self, baseline: np.ndarray, selected: np.ndarray, _create: Any = None,
454
+ combination='legacy_blas/1') -> None:
455
+ if _create is not _CREATE:
456
+ raise ReconstructionError('prepared responses are created by prepare_response')
457
+ self._baseline = baseline
458
+ self._selected = selected
459
+ self._nbytes = int(baseline.nbytes + selected.nbytes)
460
+ self._combination = _validated_combination(combination)
461
+
462
+ @property
463
+ def combination(self) -> str:
464
+ return self._combination
465
+
466
+ @property
467
+ def baseline(self) -> np.ndarray:
468
+ return self._baseline
469
+
470
+ @property
471
+ def selected(self) -> np.ndarray:
472
+ return self._selected
473
+
474
+ @property
475
+ def nbytes(self) -> int:
476
+ return self._nbytes
477
+
478
+
479
+ def prepare_response(responses: Any, *, active_indices=None,
480
+ combination='legacy_blas/1') -> PreparedResponse:
481
+ """Prepare one response matrix for repeated reconstruction.
482
+
483
+ Validates the same response matrix and original one-based active column
484
+ indices as :func:`reconstruct_response` and owns immutable float64 copies
485
+ of the baseline column and the original-order selected matrix
486
+ (``responses[:, active]``, preserving the exact memory layout the
487
+ ordinary reconstruction uses, including sparse and reversed active
488
+ orders). An empty active order retains the baseline only. The caller
489
+ may freely mutate or release ``responses`` afterwards; replay is
490
+ unaffected.
491
+ """
492
+ values, active = _validated_response_columns(responses, active_indices)
493
+ baseline = np.frombuffer(values[:,0].tobytes(),dtype=np.float64)
494
+ chosen=values[:,active]
495
+ order='F' if chosen.flags.f_contiguous else 'C'
496
+ selected = np.frombuffer(chosen.tobytes(order=order),dtype=np.float64).reshape(chosen.shape,order=order)
497
+ baseline.setflags(write=False)
498
+ selected.setflags(write=False)
499
+ return PreparedResponse(baseline, selected, _CREATE,combination)
500
+
501
+
502
+ def reconstruct_prepared_response(prepared: Any, packed_coefficients: Any, *, combination=None) -> np.ndarray:
503
+ """Reconstruct one field from a :func:`prepare_response` result.
504
+
505
+ Executes precisely the ordinary accumulation expression
506
+ (``baseline + selected @ packed_coefficients``) through the same shared
507
+ helper, returning a read-only float64 array. Only objects created by
508
+ ``prepare_response`` are admitted; per-request weights are validated for
509
+ shape and finiteness exactly like the ordinary reconstruction.
510
+ """
511
+ if type(prepared) is not PreparedResponse:
512
+ raise ReconstructionError('prepared must come from prepare_response')
513
+ algorithm = _validated_combination(prepared._combination)
514
+ if combination is not None and _validated_combination(combination) != algorithm:
515
+ raise ReconstructionError('prepared response combination mismatch')
516
+ weights = _validated_weights(packed_coefficients, prepared._selected.shape[1])
517
+ return _combine_response(prepared._baseline, prepared._selected, weights,algorithm)
@@ -0,0 +1,18 @@
1
+ """Typed runtime errors for the anyfesolver-results capsule runtime."""
2
+ from __future__ import annotations
3
+
4
+
5
+ class CapsuleError(Exception):
6
+ """Base class of every anyfesolver-results runtime error."""
7
+
8
+
9
+ class CapsuleValidationError(CapsuleError):
10
+ """A recovery capsule failed a lightweight structural validation."""
11
+
12
+
13
+ class ReconstructionError(CapsuleError):
14
+ """A response reconstruction could not be performed as requested."""
15
+
16
+
17
+ class DerivationError(CapsuleError):
18
+ """A derived field specification could not be resolved or computed."""