Turn a field value - number, expression, or structured regions - into one expression.
449def lower_value(spec, names=PARTICLE_EXPRESSION_NAMES, random=True, where="value") -> str:
450 """!
451 @brief Turn a field value - number, expression, or structured regions - into one expression.
452 @details A mapping may declare `params` (named numbers usable in its expressions and
453 region parameters) and gives either `value` or `background` with `regions`.
454 Regions paint in order, a later region over an earlier one: a sharp region by
455 `where(d <= 0, v, before)`, a smoothed one by `before*(1 - s) + v*s`, with `d`
456 the signed distance to the region and `s` its edge profile over `edge.width`.
457 The result is validated like any expression.
458 @param[in] spec Number, expression string, or mapping.
459 @param[in] names Variables in scope.
460 @param[in] random Whether random draws may be called.
461 @param[in] where Configuration path, for diagnostics.
462 @return Validated expression text.
463 """
464 params = {}
465 if isinstance(spec, bool):
466 raise ValueError(f"{where} must be a number, an expression, or a mapping.")
467 if isinstance(spec, (int, float)):
468 if not math.isfinite(spec):
469 raise ValueError(f"{where} must be finite.")
470 return repr(float(spec))
471 if isinstance(spec, str):
472 _validate_at(spec, names, random, where)
473 return spec
474 if not isinstance(spec, dict):
475 raise ValueError(f"{where} must be a number, an expression, or a mapping.")
476 unknown = set(spec) - {"params", "value", "background", "regions"}
477 if unknown:
478 raise ValueError(f"{where} does not take {sorted(unknown)}; it takes params, value, background, regions.")
479 params = spec.get("params") or {}
480 if not isinstance(params, dict):
481 raise ValueError(f"{where}.params must be a mapping of names to numbers.")
482 for name, number in params.items():
483 reserved = set(names) | {"pi"} | set(FUNCTION_ARITY) | set(RANDOM_FUNCTIONS) | {"and", "or", "not"}
484 if not isinstance(name, str) or not name.isidentifier() or name in reserved:
485 raise ValueError(f"{where}.params name '{name}' is not a free identifier.")
486 _number_text(number, {}, f"{where}.params.{name}")
487 scope = set(names) | set(params)
488
489 def expression_of(value, path):
490 """!
491 @brief Lower one value (number, `params` name, or expression) with parameters inlined.
492 @param[in] value Configured value.
493 @param[in] path Configuration path, for diagnostics.
494 @return Expression text.
495 """
496 if isinstance(value, str) and value not in params:
497 _validate_at(value, scope, random, path)
498 return f"({_substitute_params(value, params)})"
499 return _number_text(value, params, path)
500
501 if "value" in spec:
502 if "background" in spec or "regions" in spec:
503 raise ValueError(f"{where} gives either value, or background with regions.")
504 lowered = expression_of(spec["value"], f"{where}.value")
505 else:
506 regions = spec.get("regions")
507 if not isinstance(regions, list) or not regions:
508 raise ValueError(f"{where}.regions must be a non-empty list.")
509 lowered = expression_of(spec.get("background", 0.0), f"{where}.background")
510 for index, region in enumerate(regions):
511 path = f"{where}.regions[{index}]"
512 if not isinstance(region, dict) or "value" not in region:
513 raise ValueError(f"{path} must be a mapping with a shape and a value.")
514 distance = _region_distance(region, params, path)
515 value = expression_of(region["value"], f"{path}.value")
516 edge = region.get("edge")
517 if edge is None:
518 lowered = f"where({distance} <= 0, {value}, {lowered})"
519 continue
520 if not isinstance(edge, dict) or set(edge) != {"profile", "width"} or edge["profile"] not in EDGE_PROFILES:
521 raise ValueError(f"{path}.edge must give profile ({', '.join(EDGE_PROFILES)}) and width.")
522 width = _number_text(edge["width"], params, f"{path}.edge.width")
523 if float(width) <= 0.0:
524 raise ValueError(f"{path}.edge.width must be positive.")
525 ramp = f"maximum(0, minimum(1, 0.5 - ({distance})/{width}))"
526 blend = EDGE_PROFILES[edge["profile"]].format(s=ramp)
527 lowered = f"({lowered})*(1 - {blend}) + {value}*{blend}"
528 validate_expression(lowered, names, random)
529 return lowered