Training with a specification
Vehicle compiles each @property in your specification into a callable loss function. Use one of the backend modules (PyTorch or TensorFlow) to load the compiled declarations into Python.
Loading declarations
Feed a .vcl file to the backend-specific load_specification helper. Each property becomes a callable entry whose signature mirrors the @network declarations referenced inside that property:
import vehicle_lang as vcl
from vehicle_lang.loss import pytorch as loss_pt
declarations = loss_pt.load_specification(
"spec.vcl",
logic=vcl.DifferentiableLogic.Vehicle, # optional, defaults to DL2
)
constraint_loss_fn = declarations["output_bounded"]
Each callable returned by load_specification has a signature that mirrors the @network declarations referenced inside that property. Pass a Python callable for each network so the loss function can evaluate (and differentiate through) your model. A typical PyTorch training step looks like this:
import torch
model = torch.nn.Sequential(
torch.nn.Linear(1, 8),
torch.nn.ReLU(),
torch.nn.Linear(8, 1),
)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-2)
def network(x: torch.Tensor) -> torch.Tensor:
return model(x.reshape(1, 1)).reshape(1)
alpha = 0.5 # Blend task and constraint losses
for epoch in range(num_epochs):
for x_batch, y_batch in train_loader:
optimizer.zero_grad()
preds = model(x_batch)
task_loss = torch.nn.functional.mse_loss(preds, y_batch)
constraint_loss = constraint_loss_fn(network)
total_loss = alpha * task_loss + (1.0 - alpha) * constraint_loss
total_loss.backward()
optimizer.step()
Here train_loader can be any torch.utils.data.DataLoader that yields (x_batch, y_batch) pairs, and the network callable must match the type you declared for the Vehicle network so gradient-based samplers can evaluate it.
TensorFlow works the same way—load the declarations through vehicle_lang.loss.tensorflow and call the compiled property inside a tf.GradientTape so gradients flow through both the task loss and the constraint loss:
import vehicle_lang as vcl
from vehicle_lang.loss import tensorflow as loss_tf
declarations = loss_tf.load_specification(
"spec.vcl",
logic=vcl.DifferentiableLogic.Vehicle,
)
constraint_loss_fn = declarations["output_bounded"]
with tf.GradientTape() as tape:
preds = model(x_batch)
task_loss = tf.reduce_mean(tf.square(preds - y_batch))
constraint_loss = constraint_loss_fn(network)
total_loss = alpha * task_loss + (1.0 - alpha) * constraint_loss
grads = tape.gradient(total_loss, model.trainable_variables)
optimizer.apply_gradients(zip(grads, model.trainable_variables))
By combining your usual task loss with the constraint losses returned by Vehicle, you ensure the optimiser simultaneously keeps the model on task and enforces the specification.
Logic selection
By default, Vehicle compiles properties into loss functions using the Vehicle differentiable logic. You can select a different logic by passing the logic argument to load_specification. Available options are:
- vehicle_lang.DifferentiableLogic.Vehicle (default)
- vehicle_lang.DifferentiableLogic.DL2
Custom samplers and declaration context
Properties with quantifiers like forall or exists need to explore the input space to find satisfying or violating examples. The strategy used to find these examples can be customised by passing different samplers to the backend.
Pass a samplers dictionary if you want to override the default implementation, which is just a defaultdict mapping to the default implementation of a sampler. The keys in the dictionary are the names of the variables being sampled, and the values are instances of the appropriate sampler class for the backend (PyTorchSampler or TensorFlowSampler). For example, to provide a custom sampler for a variable named images in PyTorch, you could do:
custom = {"images": MySampler().get_loss}
context = {"model": model, "epsilon": 0.1}
declarations = loss_pt.load_specification(
"spec.vcl",
samplers=custom,
declaration_context=context,
declarations=["output_bounded"],
)
Loss API reference
PyTorch-specific loss helpers.
- class vehicle_lang.loss.pytorch.DefaultPyTorchSampler(num_samples: int = 10, num_steps: int = 5, seed: int | None = None)
Default sampler implementation for PyTorch that uses FGSM attack.
Uses Fast Gradient Sign Method (FGSM) to generate adversarial samples that explore the search space by perturbing points in the direction of the gradient to maximize or minimize the search_lambda.
- get_loss(dims: ~typing.Sequence[int], lower_bound: torch.Tensor, upper_bound: torch.Tensor, search_lambda: ~typing.Callable[[torch.Tensor], torch.Tensor], minimise: bool) -> jaxtyping.Float.(torch.Tensor, '1 losses')
Use FGSM to generate adversarial samples and evaluate the search lambda.
The step size is automatically inferred from the bounds to provide an out-of-the-box implementation that works for most applications.
- Args:
dims: The dimensions for the sampling (currently unused for scalar sampling) lower_bound: The lower bound tensor upper_bound: The upper bound tensor search_lambda: A callable representing the property to evaluate minimise: Whether to minimize (True) or maximize (False) the search_lambda
- Returns:
A sequence of loss values evaluated at the FGSM-perturbed points
- class vehicle_lang.loss.pytorch.PyTorchSampler
- abstractmethod get_loss(dims: ~typing.Sequence[int], lower_bound: torch.Tensor, upper_bound: torch.Tensor, search_lambda: ~typing.Callable[[torch.Tensor], torch.Tensor], minimise: bool) -> jaxtyping.Float.(torch.Tensor, '1 losses')
Calculates the loss based on the provided bounds and search lambda.
- Args:
dims: The dimensions for the sampling. lower_bound: The lower bound tensor. upper_bound: The upper bound tensor. search_lambda: A callable representing the search lambda. minimise: A flag indicating whether to minimise the search_lambda.
- Returns:
Sequence[vcl.Tensor]: The computed loss as a 1D tensor. If the size is greater than 1, the losses will be combined with reductionOp from the SearchRatTensor node.
- vehicle_lang.loss.pytorch.load_specification(path: str | Path, *, logic: DifferentiableLogic = DifferentiableLogic.DL2, samplers: Mapping[str, Any] | None = None, declarations: Iterable[str] = (), declaration_context: MutableMapping[str, Any] | None = None) dict[str, Any]
Load a loss function compiled for PyTorch.
TensorFlow-specific loss helpers.
- class vehicle_lang.loss.tensorflow.DefaultTensorFlowSampler(num_samples: int = 10, num_steps: int = 5, seed: int | None = None)
Default sampler implementation for TensorFlow that uses FGSM attack.
Uses Fast Gradient Sign Method (FGSM) to generate adversarial samples that explore the search space by perturbing points in the direction of the gradient to maximize or minimize the search_lambda.
- get_loss(dims: ~typing.Sequence[int], lower_bound: tensorflow.Tensor, upper_bound: tensorflow.Tensor, search_lambda: ~typing.Callable[[tensorflow.Tensor], tensorflow.Tensor], minimise: bool) -> jaxtyping.Float.(tensorflow.Tensor, '1 losses')
Use FGSM to generate adversarial samples and evaluate the search lambda.
The step size is automatically inferred from the bounds to provide an out-of-the-box implementation that works for most applications.
- Args:
dims: The dimensions for the sampling (currently unused for scalar sampling) lower_bound: The lower bound tensor upper_bound: The upper bound tensor search_lambda: A callable representing the property to evaluate minimise: Whether to minimize (True) or maximize (False) the search_lambda
- Returns:
A sequence of loss values evaluated at the FGSM-perturbed points
- class vehicle_lang.loss.tensorflow.TensorFlowSampler
- abstractmethod get_loss(dims: ~typing.Sequence[int], lower_bound: tensorflow.Tensor, upper_bound: tensorflow.Tensor, search_lambda: ~typing.Callable[[tensorflow.Tensor], tensorflow.Tensor], minimise: bool) -> jaxtyping.Float.(tensorflow.Tensor, '1 losses')
Calculates the loss based on the provided bounds and search lambda.
- Args:
dims: The dimensions for the sampling. lower_bound: The lower bound tensor. upper_bound: The upper bound tensor. search_lambda: A callable representing the search lambda. minimise: A flag indicating whether to minimise the search_lambda.
- Returns:
Sequence[vcl.Tensor]: The computed loss as a 1D tensor. If the size is greater than 1, the losses will be combined with reductionOp from the SearchRatTensor node.
- vehicle_lang.loss.tensorflow.load_specification(path: str | Path, *, logic: DifferentiableLogic = DifferentiableLogic.DL2, samplers: Mapping[str, Any] | None = None, declarations: Iterable[str] = (), declaration_context: MutableMapping[str, Any] | None = None) dict[str, Any]
Load a loss function compiled for TensorFlow.