@@ -65,7 +65,7 @@ static RegisterMetaType<LambdaSchedule> r_schedule;
6565
6666QDataStream &operator <<(QDataStream &ds, const LambdaSchedule &schedule)
6767{
68- writeHeader (ds, r_schedule, 3 );
68+ writeHeader (ds, r_schedule, 4 );
6969
7070 SharedDataStream sds (ds);
7171
@@ -75,6 +75,7 @@ QDataStream &operator<<(QDataStream &ds, const LambdaSchedule &schedule)
7575 << schedule.default_equations
7676 << schedule.stage_equations
7777 << schedule.mol_schedules
78+ << schedule.coupled_levers
7879 << static_cast <const Property &>(schedule);
7980
8081 return ds;
@@ -91,21 +92,30 @@ QDataStream &operator>>(QDataStream &ds, LambdaSchedule &schedule)
9192{
9293 VersionID v = readHeader (ds, r_schedule);
9394
94- if (v == 1 or v == 2 or v == 3 )
95+ if (v == 1 or v == 2 or v == 3 or v == 4 )
9596 {
9697 SharedDataStream sds (ds);
9798
9899 sds >> schedule.constant_values ;
99100
100- if (v = = 3 )
101+ if (v > = 3 )
101102 sds >> schedule.force_names ;
102103
103104 sds >> schedule.lever_names >> schedule.stage_names >>
104105 schedule.default_equations >> schedule.stage_equations ;
105106
106- if (v == 2 or v == 3 )
107+ if (v >= 2 )
107108 sds >> schedule.mol_schedules ;
108109
110+ if (v >= 4 )
111+ sds >> schedule.coupled_levers ;
112+ else
113+ {
114+ // Populate the default coupling for streams written before v4
115+ schedule.coupled_levers [_get_lever_name (" cmap" , " cmap_grid" )] =
116+ _get_lever_name (" torsion" , " torsion_k" );
117+ }
118+
109119 if (v < 3 )
110120 {
111121 // need to make sure that the lever names are namespaced
@@ -146,13 +156,17 @@ QDataStream &operator>>(QDataStream &ds, LambdaSchedule &schedule)
146156 }
147157 }
148158 else
149- throw version_error (v, " 1, 2, 3" , r_schedule, CODELOC);
159+ throw version_error (v, " 1, 2, 3, 4 " , r_schedule, CODELOC);
150160
151161 return ds;
152162}
153163
154164LambdaSchedule::LambdaSchedule () : ConcreteProperty<LambdaSchedule, Property>()
155165{
166+ // By default, cmap_grid falls back to torsion_k so that customising
167+ // torsion scaling automatically keeps the CMAP correction in sync.
168+ coupled_levers[_get_lever_name (" cmap" , " cmap_grid" )] =
169+ _get_lever_name (" torsion" , " torsion_k" );
156170}
157171
158172LambdaSchedule::LambdaSchedule (const LambdaSchedule &other)
@@ -162,7 +176,8 @@ LambdaSchedule::LambdaSchedule(const LambdaSchedule &other)
162176 force_names(other.force_names),
163177 lever_names(other.lever_names), stage_names(other.stage_names),
164178 default_equations(other.default_equations),
165- stage_equations(other.stage_equations)
179+ stage_equations(other.stage_equations),
180+ coupled_levers(other.coupled_levers)
166181{
167182}
168183
@@ -1006,6 +1021,32 @@ void LambdaSchedule::removeEquation(const QString &stage,
10061021 this ->stage_equations [idx].remove (_get_lever_name (force, lever));
10071022}
10081023
1024+ /* * Couple the lever 'force::lever' to 'fallback_force::fallback_lever'.
1025+ * If no custom equation has been set for 'force::lever' at any stage,
1026+ * the equation for 'fallback_force::fallback_lever' will be used instead
1027+ * of the stage default. This is a single level of indirection — the
1028+ * fallback lever is not itself followed further.
1029+ *
1030+ * By default, 'cmap::cmap_grid' is coupled to 'torsion::torsion_k' so
1031+ * that custom torsion schedules automatically keep the CMAP correction
1032+ * in sync.
1033+ */
1034+ void LambdaSchedule::coupleLever (const QString &force, const QString &lever,
1035+ const QString &fallback_force,
1036+ const QString &fallback_lever)
1037+ {
1038+ coupled_levers[_get_lever_name (force, lever)] =
1039+ _get_lever_name (fallback_force, fallback_lever);
1040+ }
1041+
1042+ /* * Remove any coupling for the lever 'force::lever', reverting it to
1043+ * use the stage default equation when no custom equation is set.
1044+ */
1045+ void LambdaSchedule::removeCoupledLever (const QString &force, const QString &lever)
1046+ {
1047+ coupled_levers.remove (_get_lever_name (force, lever));
1048+ }
1049+
10091050/* * Return whether or not the specified 'lever' in the specified 'force'
10101051 * at the specified 'stage' has a custom equation set for it
10111052 */
@@ -1077,6 +1118,32 @@ SireCAS::Expression LambdaSchedule::_getEquation(int stage,
10771118 return it.value ();
10781119 }
10791120
1121+ // Check coupled levers: if this lever is coupled to another, try to
1122+ // find a custom equation for the coupled lever before falling back to
1123+ // the stage default. This is a single level of indirection (no recursion)
1124+ // to prevent loops.
1125+ auto coupled_it = this ->coupled_levers .find (lever_name);
1126+
1127+ if (coupled_it != this ->coupled_levers .end ())
1128+ {
1129+ const auto &coupled_name = coupled_it.value ();
1130+ const int sep = coupled_name.indexOf (" ::" );
1131+ const QString coupled_force = sep >= 0 ? coupled_name.left (sep) : " *" ;
1132+ const QString coupled_lever_part = sep >= 0 ? coupled_name.mid (sep + 2 ) : coupled_name;
1133+
1134+ it = equations.find (coupled_name);
1135+ if (it != equations.end ())
1136+ return it.value ();
1137+
1138+ it = equations.find (_get_lever_name (coupled_force, " *" ));
1139+ if (it != equations.end ())
1140+ return it.value ();
1141+
1142+ it = equations.find (_get_lever_name (" *" , coupled_lever_part));
1143+ if (it != equations.end ())
1144+ return it.value ();
1145+ }
1146+
10801147 // we don't have any match, so return the default equation for this stage
10811148 return this ->default_equations [stage];
10821149}
0 commit comments