The parameter table¶
A fit does not refine the objects of Patterns, structures and instruments directly. It refines a flat
vector, and the parameter table is what stands between the two. The table walks
the Structure and Instrument trees, gives every Parameter it finds a
dot-path, and records what may be varied and what must follow something else.
(9.1) is the mapping, p_phys = C·p_free + d.
This chapter is that table as a caller sees it: how to address a row, how to read one, and how to change one. How a refinement works is the next question, which rows to free and in what order. Running a refinement is how to run the result. The table itself takes no view on either.
Two properties matter before you refine anything. The table contains rows you cannot free and says why for each one, so “why will this parameter not move” is answerable without running a fit. And it is rebuilt from the models at every stage boundary, so a row is never stale with respect to the objects it came from.
A dot-path names one scalar¶
A path is dot-separated, has no brackets, and starts at one of two roots. Numbers in the middle are list indices, in the order the model stores them.
Path |
Names |
|---|---|
|
the first phase’s a axis |
|
the third atom’s isotropic displacement |
|
that atom’s first site-symmetry direction |
|
the fifth Stephens coefficient of phase 0 |
|
the Gaussian constant width term |
|
the third background coefficient |
|
the specimen-height error |
|
an emission line’s relative weight |
Paths are matched with fnmatch, so * and ? work and a set of parameters
is named by one glob: phases.*.cell.* is every cell parameter of every phase,
phases.*.atoms.*.biso every isotropic displacement. One grammar serves three
places. Refinement.set_vary takes globs, a stage’s turn_on list takes globs,
and Refinement.untie takes globs.
Brackets are the one trap. fnmatch reads [0] as a character class rather
than an index, so phases[0].cell.a matches nothing and raises no error. There
are no brackets anywhere in the scheme, and the index is another dotted
component.
Some paths exist only when the model declares the block they belong to.
phases.0.microstrain.dof.* appears once the phase carries a StephensStrain,
phases.0.atoms.2.adp.* once that atom carries an AnisoU, and
instrument.geometry.capillary_offset_along_beam only on a capillary geometry.
A glob over an absent block matches nothing, so the broad globs in the shipped
plans are safe on any model.
Reading the table¶
Refinement.parameters returns the whole table as a list of ParameterRow, in
the order the free vector uses.
import rietx as rx
lab6 = rx.Structure(phases=[rx.Phase(
name="LaB6",
space_group="P m -3 m",
cell=rx.Cell.cubic(4.15689, vary=True),
atoms=[
rx.Atom(label="La", species="La", x=rx.Parameter(value=0.0),
y=rx.Parameter(value=0.0), z=rx.Parameter(value=0.0)),
rx.Atom(label="B", species="B", x=rx.Parameter(value=0.19964),
y=rx.Parameter(value=0.5), z=rx.Parameter(value=0.5)),
],
)])
ref = rx.Refinement(lab6, rx.Instrument.debye_scherrer(wavelength=0.4139),
history=False)
rows = ref.parameters()
assert len(rows) == 42 # every scalar, held ones included
assert sum(row.refinable for row in rows) == 25 # what set_vary could free
held = {row.path: row.held_because for row in rows if not row.refinable}
assert held["phases.0.cell.b"] == "tied: = 1·phases.0.cell.a"
assert held["phases.0.cell.alpha"] == "structurally fixed by symmetry or by the model"
Forty-two rows for two atoms and a default instrument, of which twenty-five could be freed. Most of a table is parameters you will never touch, and the listing is the cheapest way to see what is there.
Field |
Type |
Meaning |
|---|---|---|
|
str |
the dot-path |
|
float |
the current physical value |
|
bool |
whether it is free in the next fit |
|
float |
inclusive physical bounds as stored, ±inf when unbounded. A cell the data cannot see also gets a per-stage default the row does not show (§ “A cell the data cannot see gets a bound you did not set”) |
|
str |
the reparameterisation, (9.2) |
|
|
what this value follows, if anything |
|
bool |
structurally fixed, |
|
bool |
you declared it does not move, with |
|
bool |
force-fixed by the intensity mode in force |
|
float or None |
the uncertainty from the most recent fit |
The first nine fields mirror the optimiser’s own entry type field for field,
and a test asserts that, so a new field cannot be added to the table without
appearing here. esd and mode_fixed are the deliberate additions. esd is a
property of a completed fit rather than of a parameter, and merging it in lets
one listing answer both “what is this worth” and “how well is it known”.
ParameterRow.refinable and ParameterRow.held_because are derived. The first
is the single predicate a front end should grey a row by, and the second is the
sentence to show beside it.
The five reasons a row is held¶
They are distinguishable on purpose, because the fix differs.
Reason |
What it means |
Can you release it |
|---|---|---|
|
structurally fixed: a symmetry-fixed cell angle, a fully fixed special position, the first emission line’s weight, a non-primary emission line’s wavelength, |
no |
|
an affine function of other rows, so the freedom lives in its sources |
only if it is your own tie |
|
refinable in principle, but the current intensity mode force-fixes it |
switch back to |
|
you declared this parameter does not move, whatever a plan asks for (Holding a parameter against the plan). The only reason you both created and can lift |
|
|
a wavelength that cannot be freed right now, because this histogram’s cell is free and the two are an exactly flat direction. The only dynamic held-reason: hold the cell and the same row becomes refinable (A refinable wavelength) |
no |
ParameterRow.refinable is false if any of the five holds. The five counts do
not add up to the number of held rows, and should not. On the LaB6 table above,
asking for the Le Bail listing marks fourteen rows mode_fixed while the
refinable count only falls from 25 to 19, because eight of those fourteen were
already locked or tied. Read refinable for the decision and the four flags
only to explain it.
import rietx as rx
ref = rx.Refinement(rx.Structure(phases=[rx.Phase(
name="LaB6", space_group="P m -3 m", cell=rx.Cell.cubic(4.15689),
atoms=[rx.Atom(label="La", species="La", x=rx.Parameter(value=0.0),
y=rx.Parameter(value=0.0), z=rx.Parameter(value=0.0))],
)]), rx.Instrument.debye_scherrer(wavelength=0.4139), history=False)
rietveld = sum(row.refinable for row in ref.parameters())
lebail = sum(row.refinable for row in ref.parameters(mode="lebail"))
assert lebail < rietveld
Refinement.parameters takes a mode argument because the mode the object
carries is the one the last stage ran in, which before the first run is the
rietveld default. A caller that knows what the next run will use has to be
able to say so. Without it, a Le Bail project’s atom rows come back looking
editable, which is the one thing mode_fixed exists to prevent. A Le Bail phase
must carry a dummy atom to exist at all, and its biso is not something to
offer anyone.
A tie is data rather than a flag. TieSpec is the serializable form of
value = Σ c·source + k.
Field |
Meaning |
|---|---|
|
the (path, coefficient) pairs |
|
the additive constant k |
|
true for a tie you declared, false for one the symmetry created |
|
the paths this value follows, and the ones to edit instead |
|
the right-hand side as text, e.g. |
TieSpec.user is the field that matters when you are deciding what to offer a
user. Both populations hold a row the same way and held_because reads the
same for either, but a symmetry tie is rederived from the space group every
time the table is built and nothing can remove it, while a tie you declared
lives in the history and Refinement.untie takes it back. How a refinement works has
the verbs that create them.
The two populations also read differently. A cell tie is an identity row,
1·phases.0.cell.a. A coordinate tie carries the starting position in its
constant: on the LaB6 table above, phases.0.atoms.1.x describes itself as
0.19964 + 1·phases.0.atoms.1.dof.0, because a coordinate degree of freedom is
a displacement from the stored coordinate (9.3). ADP and Stephens
degrees of freedom are absolute instead, which enforces their site symmetry
exactly.
Tying a coordinate degree of freedom is how two atoms are constrained to move
together. A coordinate itself refuses a user tie, since symmetry outranks one.
The anchor is then the coordinate as it stood when the tie was declared.
phases.0.atoms.1.dof.0 reads the displacement its tie implies, and
phases.0.atoms.1.x reads the sum of the two. That holds at every rebuild for
as long as the tie is declared, so a second Refinement.fit reports the
amplitude the first one did.
What the optimiser actually varies¶
ParameterRow.value, lo and hi are physical. The solver does not see them.
A parameter with a transform is reparameterised first, and its bounds are
mapped into the internal variable, which is monotonic so the interval survives.
Physical bounds |
Transform |
Internal bounds |
|---|---|---|
[4.0, 4.3] |
|
[4.0, 4.3] |
[0.0, inf] |
|
[−inf, inf] |
[1e-6, 1.0] |
|
[−13.82, 0.5413] |
[0.0, 1.0] |
|
[−inf, 27.63] |
The pattern in the second and fourth rows is the one to know: a lower bound at or below 1e-12 becomes −inf, so the optimiser runs unconstrained instead of pressing a hard zero. That is why a width or a scale can descend smoothly to its off state rather than stalling against a wall, and why Patterns, structures and instruments‘s warning about reaching exactly zero is a consequence rather than a bug.
A Parameter you supply keeps what its field declares¶
A field with a physical range declares it in its default: Atom.biso is
[0, 25] Ų, Phase.scale is [0, inf] under softplus, ProfileTCHZ.u is
[−0.05, 1.0] deg². A Parameter you pass for such a field inherits each of
min, max and unit that you left unset, and the transform together with
the bounds it enforces. So Parameter(value=5e-3, vary=True) passed as a phase
scale arrives softplus from zero, as the default would. The same value passed
as Phase.lor_size with min=-1.0 keeps your bound and the identity
transform, because softplus under a lower bound of −1 would move a negative
value to zero at the first step. Whatever you state wins, in either direction.
A value outside the inherited range is refused when the model is built, naming
the field and the range. State the range you mean on the Parameter, or, for a
coarse instrument’s widths, build them with ProfileTCHZ.coarse.
Until schema 0.17 for Atom, and 0.35 for every other class, a Parameter
you supplied came out unbounded, with no unit and the identity transform. In
issue #204 that let an iron Biso refine to −165 Ų at unchanged Rwp. A history
log written by such a release is repaired when it is read (Files and projects).
A cell the data cannot see gets a bound you did not set¶
One case reverses the direction. Every structural parameter of a phase reaches
the pattern only through scale × |F|² × profile, so a phase whose scale has
fallen to its floor contributes nothing the fit can see, and its cell is then
free to wander without changing Rwp at all. Unbounded it leaves the physical
range entirely, and the run fails much later, when the reflection list for a
cell that size is refused.
So when a stage begins, any phase whose strongest modelled point sits below 1σ
of the counting noise has its cell bounded to ±5 % of the value that stage
starts from, on whichever side you left at ±inf. Set a bound yourself and that
side is yours. Such a cell can therefore report BOUND_HIT while its
ParameterRow.hi still reads inf. The row is the bound you stored, and the
window is the solver’s bound for one stage.
Only that phase’s cell is bounded. A bound is not free: the solver takes its step scale from the distance to the bounds, so bounding a cell changes how it moves even when the bound is never reached. A phase the data can see is left alone, and a fit of one gives the identical answer it gave before this existed.
The window bounds the symptom. The cause is reported separately, as
PHASE_UNCONSTRAINED: which phase the data cannot distinguish from absent, and
what the run did with its parameters. Both read the same measurement, so they
can never disagree about which phase is which.
In a staged refinement the window is rarely reached now, because the stage
holds those parameters instead of bounding them; see StageResult.held in
Running a refinement. The window stays for the case a hold cannot cover: a phase
that is visible when the stage starts and is refined by it.
The transform is also in the esd chain. (8.5) gives the uncertainty of
the internal variable. Multiplying by dp/du at the solution is what makes it
physical, and only then is it propagated through C to the rows that follow
it.
Editing the table¶
Two verbs, and both record a history node, because freeing a parameter and setting one are refinement moves rather than bookkeeping.
Refinement.set_vary takes a glob or a list of them and returns the paths it
actually changed. The return value is the useful part. A locked or tied entry
never matches, however broad the glob, so the list of hits is the honest account
of what your glob did.
ref.set_vary("phases.*.cell.*") # -> ['phases.0.cell.a'] on a cubic phase
ref.set_vary("phases.*.cell.alpha") # -> [] : locked by symmetry
ref.set_vary("phases.*.atoms.*.x") # -> [] : tied to a site-symmetry DOF
ref.set_vary("instrument.profile.u", vary=False)
A cubic cell returns one path from a glob that names six. Nothing went wrong;
five of the six are held, and the one hit is the whole of the freedom. Paths the
current mode force-fixes are the exception to the rule: set_vary will free
them, and a stage then drops them again, reporting them as mode_fixed.
The GUI calls the same verb from its Model panel: a box beside each value frees
that one path, and a held value shows which of the four reasons holds it in
place of the box. The atom table’s vary column is the one box that names a
family rather than a path. It names phases.0.atoms.2.dof.*, because a site’s
coordinate degrees of freedom are freed together and per-axis intent does not
map onto a direction such as [1 1 0]. Other globs stay in the parameter panel,
since a family freed by a glob is one call and one history node.
Coordinates are typed as x, y and z there rather than as displacements. The
panel sends the whole position and the server projects it onto the site’s own
basis. A position the site cannot reach is refused, naming the directions it
allows and the nearest position they lead to, so the choice stays the
caller’s. On a
general position the projection is the identity and every value is accepted.
Either way what lands in θ is set_values on the …dof.k paths, so the
history reads the same as any other value edit.
Refinement.set_values takes a dict of paths to values. It is plural because a
table is edited a set of cells at a time, and one node per keystroke would bury
the log.
It raises rather than guessing, and the four refusals have four different fixes:
Refusal |
Message |
The fix |
|---|---|---|
unknown path |
|
a typo |
locked |
|
nothing to set; the model owns it |
tied |
|
set the source |
out of bounds |
|
a value the bounded solver could not start from |
Dependents follow their sources. Setting phases.0.cell.a on a cubic phase
moves b and c with it, and the change reaches the objects:
Refinement.structure and Refinement.instrument are the refinement’s own deep
copies of what you passed in, and they are what the table writes back to.
Refinement.fitted_structure and Refinement.fitted_instrument return those
same objects. The two pairs of names differ in what they claim about when you
are reading, and not in what they return.
Setting a value also invalidates the fitted curve and its statistics, which described the previous values.
Note
Both verbs change the working state whether or not a history tree exists, but
the node is recorded only once it does. The tree is created on the first fit
or run_stage, because it is pinned to its pattern by a fingerprint and no
pattern has been seen before then. A set_vary before the first fit is
therefore not in the log, while the one after it is. The refinement history is that log.
Declared extra peaks¶
Sometimes the pattern has a sharp peak no phase in your model can put there: a sample holder diffracting at its own distance from the focusing circle, a mount, a window, an unidentified impurity line. If it sits in empty background you can exclude the region and lose nothing. The case this feature exists for is the other one, where the intruder overlaps peaks that matter, so excluding the region masks the sample peak underneath along with it.
A PeakComponent declares the intruder so the channels stay in the fit:
import rietx as rx
from rietx.schemas.common import Parameter
from rietx.schemas.instrument import PeakComponent
instrument = rx.Instrument.bragg_brentano(radiation="CuKa")
holder = PeakComponent(
label="steel holder 110",
center=Parameter(value=44.6, min=44.3, max=44.9, unit="deg"),
area=Parameter(value=800.0, min=0.0, unit="counts*deg",
transform="softplus"),
)
instrument.extra_components.append(holder)
print(instrument.extra_components[0].kind) # peak
print(instrument.extra_components[0].fwhm.min) # 0.005
It joins Instrument.extra_components, the same list a broad hump goes on
(Explicit humps), and the two are different members of one union. The
difference that matters to you is where each one lands. A hump is part of the
reported background and appears in result.y_background. A peak is not
background at all, and its positions join result.ticks under the key
"(extra)" instead, so the report stops calling your declared peak an unindexed
impurity.
The package will neither refuse a declared peak nor detect one for you. It fits what you declare and reports evidence about what happened. That division is deliberate: you can see your specimen and the package cannot.
Field |
Is |
Bound |
|---|---|---|
|
the apparent 2θ of the primary emission line |
finite |
|
its integrated intensity, counts·deg |
softplus, |
|
its width in 2θ |
softplus, floored at |
|
the Lorentzian fraction of its pseudo-Voigt |
logit, |
|
whether to place an image at every emission line |
|
|
a free-text tag, rendered in diagnostics |
not a parameter and not part of any dot-path |
|
the union discriminator, |
fixed; it names which member of |
All four parameters default to vary=False, and an area of zero means a
declared-but-never-freed peak is bit-identical to no peak at all.
The centre is apparent, and it is bounded¶
center is the apparent 2θ of the primary emission line. No zero shift, no
sample-displacement or transparency correction, no axial asymmetry is applied to
it. A holder sits at its own distance and carries its own aberrations, so
correcting it with the specimen’s would be worse than not correcting it. All of
them are absorbed into the free centre instead. For the same reason the centre
carries no crystallographic meaning: do not read a d-spacing off it.
center and fwhm must both carry finite min and max, and a component
without them is refused with a message saying what to write. The bounds size the
evaluation window, which is frozen once per stage like every other window in the
package. Because the window is built from the bounds rather than from the
starting value, a centre free to move anywhere its bounds allow is inside its
window by construction.
The price is that a loose bound buys a wide window. fwhm.max defaults to 0.5°
and the tail multiplier at eta.max = 1 is about 16, so the default window is
roughly ±8°. Stating bounds you actually believe is worth doing.
Every emission line, unless it is not diffraction¶
The holder diffracts the same source your sample does, so its Kα2 is physically
present. By default each emission line gets an image at its own Bragg angle,
scaled by the line’s weight times the two lines’ Lorentz-polarisation ratio. The
bare weight alone is a measured bias rather than a simplification. Set
all_lines=False for something that does not diffract at all, such as a
fluorescence line or a detector artefact, where a Kα2 image would be a claim
about physics that is not happening.
Freeing one¶
One preset frees a declared component and the rest leave it alone.
mccusker_structural has an extra_components stage, sixth of eleven, after
the scale, background, zero, cell and profile and before the coordinates, and it
frees whatever you declared and nothing more. Every other preset stops short of
these paths.
Nothing ever adds a component, which is the safety property that matters. A sharp peak with a free area improves any Rwp, so one exists only because you declared it. Once you have, being freed by the structural plan is the same treatment a declared hump gets.
To free one under any other plan, say so:
from rietx.schemas.plan import PlanSpec, StageSpec
plan = PlanSpec(stages=[
StageSpec(name="scale_bkg",
turn_on=["phases.*.scale", "instrument.background.*"]),
StageSpec(name="holder", turn_on=["instrument.extra_components.*"]),
StageSpec(name="cell", turn_on=["phases.*.cell.*", "instrument.zero_shift"]),
])
Free it after the scale and background, for the reason every ordering in How a refinement works exists: a peak turned on over a pattern whose general level has not been set yet will absorb whatever is nearest. Stages are cumulative, so the component keeps refining in the stages that follow.
Under Le Bail and Pawley a declared peak stays refinable, so its intensity field
is called area and not scale, every *.scale path being force-fixed in
those modes. Its curve is subtracted from the pattern before the
intensities are partitioned, so the phases are not handed counts that belong to
your holder.
A declared peak adds no reflections, so it does not change
effective_observations. The observation count is a fact about the phases.
What the fit tells you afterwards¶
Two findings, both advice:
EXTRA_PEAK_ON_REFLECTION says the component ended up within 0.08° of a
position your model already predicts. The two now describe one peak between them
and nothing in Rwp says which owns the counts. If the overlap is real this is
the feature working as intended. If you declared the peak to make a misfitting
reflection go away, fix the model instead.
EXTRA_PEAK_NO_INTENSITY says the area refined onto its zero bound, so the data
does not see the peak. A peak reaches the pattern only through area × profile,
so at zero area nothing constrains its centre either. The centre you get back is
a walk rather than a measurement, and the area’s esd comes back absent rather
than small. Either the feature is not in this specimen, or the centre
bounds do not bracket it.
Identifiability.extra_peak_absorption carries a number beside those, on
result.identifiability: the fraction of each structural parameter’s effect the
declared peaks could reproduce. It is reported and never thresholded, because
nothing has yet measured what separates a healthy declared peak from a parasitic
one across real cases.
A sequence of patterns¶
For an in-situ or operando series, carry=["*"] warm-starts the component from
each pattern into the next, and its area and centre come back as trajectories
like any other parameter. Excluding instrument.extra_components.* from carry
pins the component to the initial model instead, which suits a holder that
genuinely does not move. Refining many patterns has the carry
semantics.
The package reports what it measured. Whether a holder peak belongs in the figure is a question about your writeup rather than about the fit.
For agents
A PeakComponent is never something to add on your own initiative. It is a
declaration about the specimen, and only the user can make it. When a user
declares one, free it in its own stage after scale and background, and read
EXTRA_PEAK_ON_REFLECTION before quoting any intensity that overlaps it.
EXTRA_PEAK_NO_INTENSITY means the centre is not quotable, rather than
imprecise.
the agent skill
§7 has both rows.
What a fit reports back¶
A result carries its own view of the table. RefinementResult.parameters is a
list of RefinedParameter, and the membership rule is the contract: a row
appears if the entry varied or was tied. A fixed parameter is absent rather than
present with vary=False.
Field |
Meaning |
|---|---|
|
the dot-path |
|
the value the fit ended at |
|
the esd, or None if it could not be estimated |
|
false on the tied rows, which is how to spot them |
|
true, false, or None where the row was not tested |
A parameter the data said nothing about reports no esd rather than a small one.
A free parameter can end up in a direction the residual does not move at all (a
width whose peak shape does not need it, a scale for a phase that is not in the
specimen), and there is no variance to report for it, so
stderr is None. It is None on the tied rows that draw on such a parameter
too: a tie whose source measured nothing measured nothing. Read a None esd on
a row you meant to refine as a signal to take that parameter out of the plan.
A parameter sitting on its bound is not a measurement, so do not quote one.
That is what at_bound is for, and it has three states rather than two:
Value |
Meaning |
|---|---|
|
the fit stopped against a bound; the same rows the |
|
tested, and interior |
|
not tested, so no answer either way |
None covers three cases. A tied row is never tested. It is not in the free
vector the fit solves, so nothing looked at it, and its value can sit on its own
declared bound while every source is interior. A result built without a fit
behind it has nothing to report: replay recomputes a recorded
node’s curves without running the guard, so every one of its rows is None.
And a free row can sit on a floor the solver cannot see. A scale or width
declared with min=0 refines through a softplus transform, whose limit is at
minus infinity in the solver’s coordinates, so a scale that ends at 0 has no
bound there to test against. An absent phase’s scale is the usual case, and
QPA_ESD_UNAVAILABLE names it when its esd goes too.
What counts as being on a bound is the solver’s own test rather than a second
one. A value is on a bound when it sits within 1e-10 of that bound’s own
magnitude, floored at 1, which is the rule scipy.optimize.least_squares uses
to fill its active_mask. The test is relative to the bound it is near and
never to the gap between the two, so writing min=1e-14, max=1e14 to mean
“leave this alone” does not make every value in between read as pinned.
Both channels carry the same fact, and by construction rather than by agreement.
The flag is the BOUND_HIT findings projected onto the rows, from one bound
test. Read whichever suits the shape of your code: the diagnostic when you want
the list, the flag when you are already iterating rows.
The two views differ in size, and the difference is the point. A single-phase
NAC refinement over 2 to 24° measured here gives 73 rows from
Refinement.parameters and 32 from RefinementResult.parameters: 14 free, 18
tied, and 41 fixed rows that the result omits entirely. Use the result to report
a fit and the table to decide what to do next.
That split is the one at_bound reports against. In this fit all 14 free rows
come back False and all 18 tied rows come back None: cell.b, cell.c and
the sixteen symmetry-tied coordinates. Capping cell.a at 10.2500, against a
free optimum of 10.2513, turns exactly one row True and takes Rwp from 0.1403
to 0.2068. The fit spends its other parameters covering for a cell it is not
allowed to reach, and that is why a bound-sitting value is not a measurement.
Esds cross between them. Refinement.parameters merges the most recent fit’s
esds onto ParameterRow.esd, so one listing carries both the value and its
uncertainty. A tied row gets one too: the free parameters’ covariance is
propagated through C as σ² = diag(C·Cov·Cᵀ), so an identity tie reports
exactly its source’s number. In that NAC fit phases.0.cell.b and .c both
come back at 6.27e-05, which is a’s esd. The tied coordinate rows carry none,
because a row is given an esd only when at least one of its sources was free,
and that plan did not free the coordinate degrees of freedom. None means the
uncertainty is unavailable rather than zero.
For a single value, RefinementResult.parameter takes a path and returns the
one row, which is less work than filtering the list.
For agents
Refinement.parameters is the surface to work the table from without running
anything: every row, each held one saying why, and the esds from the last fit
merged in. ParameterRow.refinable is one predicate to gate an offer on, and
TieSpec.user separates a tie you may release from one you may not, without
having to try it and read the error.
the agent skill
§2 has the order to free them in.