import dataclasses
import itertools
import typing
from ._util import MechId
from .definitions import (
CableProperties,
CableType,
Definition,
Ion,
Mechanism,
Synapse,
_parse_dict_def,
)
[docs]
class Constraint:
def __init__(self):
self._upper = None
self._lower = None
self._tolerance = None
@property
def tolerance(self):
return self._tolerance
@property
def lower(self):
value = self._lower
if self.tolerance is not None:
value *= 1 - self.tolerance
return value
@lower.setter
def lower(self, value: float):
self._lower = value
@property
def upper(self):
value = self._upper
if self.tolerance is not None:
value *= 1 - self.tolerance
return value
@upper.setter
def upper(self, value: float):
self._upper = value
[docs]
@classmethod
def from_value(cls, value: "ConstraintValue") -> "Constraint":
if isinstance(value, Constraint):
return value
elif isinstance(value, list | tuple):
constraint = cls()
constraint.lower = value[0]
constraint.upper = value[1]
else:
constraint = cls()
constraint.upper = value
constraint.lower = value
return constraint
[docs]
def set_tolerance(self, tolerance=None):
self._tolerance = tolerance
return self
ConstraintValue = Constraint | float | tuple[float, float] | list[float]
"""
Type alias for values used to specify constraints.
Can be:
- A single :class:`Constraint` instance,
- A single float value,
- A tuple of two floats representing a range (lower, upper),
- A list of floats representing a range (lower, upper).
This flexible type allows defining constraints either as explicit
`Constraint` objects or as simple numeric bounds.
"""
[docs]
@dataclasses.dataclass
class CablePropertyConstraints(CableProperties):
Ra: Constraint
"""
Axial resistivity in ohm/cm.
"""
cm: Constraint
"""
Membrane conductance.
"""
def __post_init__(self):
for field in dataclasses.fields(self):
_convert_field(self, field)
[docs]
class CablePropertyConstraintsDict(typing.TypedDict, total=False):
Ra: ConstraintValue
cm: ConstraintValue
[docs]
@dataclasses.dataclass
class IonConstraints(Ion):
rev_pot: Constraint
int_con: Constraint
ext_con: Constraint
def __post_init__(self):
for field in dataclasses.fields(self):
_convert_field(self, field)
[docs]
class IonConstraintsDict(typing.TypedDict, total=False):
rev_pot: ConstraintValue
int_con: ConstraintValue
ext_con: ConstraintValue
[docs]
class MechanismConstraints(Mechanism):
parameters: dict[str, Constraint]
def __init__(self, parameters: dict[str, ConstraintValue]):
super().__init__({k: Constraint.from_value(v) for k, v in parameters.items()})
[docs]
class SynapseConstraints(Synapse, MechanismConstraints):
pass
SynapseConstraintsDict = dict[str, ConstraintValue] | typing.TypedDict(
"SynapseConstraintsDict",
{"mechanism": MechId, "parameters": dict[str, ConstraintValue]},
total=False,
)
"""
Type definition for synapse constraints.
This can be either:
- A dictionary mapping parameter names to :data:`ConstraintValues <ConstraintValue>`, or
- A TypedDict with optional keys:
- ``mechanism``: Identifier for the synapse mechanism (type
:class:`~arborize._util.MechId`).
- ``parameters``: Dictionary of parameter names to
:data:`ConstraintValues <ConstraintValue>`.
This flexible type supports simple parameter dicts or more structured dicts
including the synapse mechanism identifier.
"""
[docs]
class CableTypeConstraints(CableType):
cable: CablePropertyConstraints
mechs: dict[MechId, MechanismConstraints]
ions: dict[str, IonConstraints]
synapses: dict[str, SynapseConstraints]
[docs]
@classmethod
def default(cls, ion_class=IonConstraints):
default = super().default(ion_class=ion_class)
for field in dataclasses.fields(default.cable):
setattr(
default.cable,
field.name,
Constraint.from_value(getattr(default.cable, field.name)),
)
return default
[docs]
class CableTypeConstraintsDict(typing.TypedDict, total=False):
cable: CablePropertyConstraintsDict
mechanisms: dict[MechId, dict[str, ConstraintValue]]
ions: dict[str, IonConstraintsDict]
synapses: dict[str, SynapseConstraintsDict]
"""
Typed dictionary representing constraints for a cable type.
Fields:
- ``cable``: Dictionary of cable property constraints (e.g., Ra, cm).
- ``mechanisms``: Mapping from mechanism IDs to their parameter constraints.
- ``ions``: Mapping from ion names to their ion-specific constraints.
- ``synapses``: Mapping from synapse names to their synapse-specific constraints.
All fields are optional.
"""
[docs]
class ConstraintsDefinition(
Definition[
CableTypeConstraints,
CablePropertyConstraints,
IonConstraints,
MechanismConstraints,
SynapseConstraints,
]
):
"""
A specialized Definition that supports parameter constraints for cable types, ions,
mechanisms, and synapses.
This class wraps all components with `Constraint` instances, allowing ranges or
tolerances to be applied to physiological parameters. Use `define_constraints` to
create an instance from a dictionary and apply a global tolerance.
Example::
constraints = define_constraints(
{
"cable_types": {
"dend": {
"cable": {"Ra": (100, 200), "cm": 1.0},
"ions": {
"na": {
"rev_pot": -65.0,
"int_con": (10.0, 15.0),
"ext_con": 150.0,
},
},
"mechanisms": {"hh": {"gnabar": (0.1, 0.3), "gl": 0.0003}},
}
}
},
tolerance=0.1,
)
:ivar cable_type_class: The class used for representing constrained cable types.
:vartype cable_type_class: type[CableTypeConstraints]
:ivar cable_properties_class: The class used for constrained cable properties
(e.g., Ra, cm).
:vartype cable_properties_class: type[CablePropertyConstraints]
:ivar ion_class: The class used for constrained ion properties.
:vartype ion_class: type[IonConstraints]
:ivar mechanism_class: The class used for constrained mechanism parameters.
:vartype mechanism_class: type[MechanismConstraints]
:ivar synapse_class: The class used for constrained synapse parameters.
:vartype synapse_class: type[SynapseConstraints]
:param tolerance: Optional tolerance to apply to all parameter constraints
(e.g., 0.1 = ±10%).
:type tolerance: float or None
:return: A fully structured `ConstraintsDefinition` with all values wrapped in
`Constraint` objects.
:rtype: ConstraintsDefinition
"""
[docs]
@classmethod
@property
def cable_type_class(cls):
return CableTypeConstraints
[docs]
@classmethod
@property
def cable_properties_class(cls):
return CablePropertyConstraints
[docs]
@classmethod
@property
def ion_class(cls):
return IonConstraints
[docs]
@classmethod
@property
def mechanism_class(cls):
return MechanismConstraints
[docs]
@classmethod
@property
def synapse_class(cls):
return SynapseConstraints
[docs]
def set_tolerance(self, tolerance=None):
for syn in self._synapse_types.values():
for p in syn.parameters.values():
p.set_tolerance(tolerance)
for ct in self._cable_types.values():
for field in dataclasses.fields(ct.cable):
getattr(ct.cable, field.name).set_tolerance(tolerance)
for ion in ct.ions.values():
for field in dataclasses.fields(ion):
getattr(ion, field.name).set_tolerance(tolerance)
for mech in itertools.chain(ct.mechs.values(), ct.synapses.values()):
for p in mech.parameters.values():
p.set_tolerance(tolerance)
def _convert_field(obj, field):
constraint = Constraint.from_value(getattr(obj, field.name))
setattr(obj, field.name, constraint)
[docs]
class ConstraintsDefinitionDict(typing.TypedDict, total=False):
cable_types: dict[str, CableTypeConstraintsDict]
synapse_types: dict[MechId, SynapseConstraintsDict]
"""
Typed dictionary for the overall constraints definition structure.
Fields:
- ``cable_types``: A dictionary mapping cable type names to their constraint dictionaries.
- ``synapse_types``: A dictionary mapping mechanism ID to synapse constraint dictionaries.
Both fields are optional and represent the hierarchical constraint configuration
used to build a `ConstraintsDefinition` instance.
"""
[docs]
def define_constraints(
constraints: ConstraintsDefinitionDict, tolerance=None, use_defaults=False
) -> ConstraintsDefinition:
"""
Create a `ConstraintsDefinition` instance from a dictionary of constraints, applying
an optional global tolerance and default values.
:param constraints: Dictionary specifying constraint values structured as
`ConstraintsDefinitionDict`.
:type constraints: ConstraintsDefinitionDict
:param tolerance: Optional tolerance to apply to all parameter constraints
(e.g., 0.1 means ±10%), defaults to None.
:type tolerance: float or None
:param use_defaults: Whether to fill in missing constraint values with defaults,
defaults to False.
:type use_defaults: bool
:return: A fully constructed `ConstraintsDefinition` instance with all values
wrapped in `Constraint` objects and tolerance applied.
:rtype: ConstraintsDefinition
"""
constraints = _parse_dict_def(ConstraintsDefinition, constraints)
constraints.set_tolerance(tolerance)
constraints.use_defaults = use_defaults
return constraints