Skip to content

Commit 057d8a1

Browse files
authored
Add sketch under-constrained analysis (#9297)
* Use local ezpz with underconstrained analysis * Add new config parameter so that the executor knows if it should do the analysis * Use under-constrained analysis * Use released kcl-ezpz 0.2.2 * Fix to do freedom analysis when editing a sketch
1 parent 5e902ac commit 057d8a1

File tree

9 files changed

+88
-42
lines changed

9 files changed

+88
-42
lines changed

rust/Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ console_error_panic_hook = "0.1.7"
2323
dashmap = { version = "6.1.0" }
2424
http = "1"
2525
indexmap = "2.12.1"
26-
kcl-ezpz = "0.2.1"
26+
kcl-ezpz = "0.2.2"
2727
kittycad = { version = "0.4.2", default-features = false, features = [
2828
"js",
2929
"requests",

rust/kcl-lib/benches/compiler_benchmark_criterion.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,10 @@ pub fn bench_mock_warmed_up(c: &mut Criterion) {
3333
let rt = tokio::runtime::Runtime::new().unwrap();
3434
let ctx = rt.block_on(async { kcl_lib::ExecutorContext::new_mock(None).await });
3535
// First run outside the benchmark measurement, to initialize KCL memory.
36-
#[cfg(feature = "artifact-graph")]
3736
let mock_config = kcl_lib::MockConfig {
3837
use_prev_memory: false,
3938
..Default::default()
4039
};
41-
#[cfg(not(feature = "artifact-graph"))]
42-
let mock_config = kcl_lib::MockConfig { use_prev_memory: false };
4340
let _out = rt.block_on(async { ctx.run_mock(&program, &mock_config).await.unwrap() });
4441
b.iter(|| {
4542
if let Err(err) = rt.block_on(async {

rust/kcl-lib/src/execution/exec_ast.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ impl ExecutorContext {
151151
exec_state.stack().memory.clone(),
152152
Some(module_id),
153153
next_object_id,
154+
exec_state.mod_local.freedom_analysis,
154155
);
155156
if !preserve_mem {
156157
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
@@ -998,6 +999,7 @@ impl Node<SketchBlock> {
998999
},
9991000
segments: Default::default(),
10001001
constraints: Default::default(),
1002+
is_underconstrained: None,
10011003
}),
10021004
label: Default::default(),
10031005
comments: Default::default(),
@@ -1104,7 +1106,13 @@ impl Node<SketchBlock> {
11041106
max_iterations: 50,
11051107
..Default::default()
11061108
};
1107-
let solve_outcome = match kcl_ezpz::solve_with_priority(&constraints, initial_guesses.clone(), config) {
1109+
let solve_result = if exec_state.mod_local.freedom_analysis {
1110+
kcl_ezpz::solve_with_priority_analysis(&constraints, initial_guesses.clone(), config)
1111+
.map(|outcome| (outcome.outcome, Some(outcome.analysis)))
1112+
} else {
1113+
kcl_ezpz::solve_with_priority(&constraints, initial_guesses.clone(), config).map(|outcome| (outcome, None))
1114+
};
1115+
let (solve_outcome, solve_analysis) = match solve_result {
11081116
Ok(o) => o,
11091117
Err(failure) => {
11101118
if let kcl_ezpz::Error::Solver(_) = &failure.error {
@@ -1115,13 +1123,16 @@ impl Node<SketchBlock> {
11151123
annotations::WARN_SOLVER,
11161124
);
11171125
let final_values = initial_guesses.iter().map(|(_, v)| *v).collect::<Vec<_>>();
1118-
kcl_ezpz::SolveOutcome {
1119-
final_values,
1120-
iterations: Default::default(),
1121-
warnings: failure.warnings,
1122-
unsatisfied: Default::default(),
1123-
priority_solved: Default::default(),
1124-
}
1126+
(
1127+
kcl_ezpz::SolveOutcome {
1128+
final_values,
1129+
iterations: Default::default(),
1130+
warnings: failure.warnings,
1131+
unsatisfied: Default::default(),
1132+
priority_solved: Default::default(),
1133+
},
1134+
None,
1135+
)
11251136
} else {
11261137
return Err(KclError::new_internal(KclErrorDetails::new(
11271138
format!("Error from constraint solver: {}", &failure.error),
@@ -1130,6 +1141,8 @@ impl Node<SketchBlock> {
11301141
}
11311142
}
11321143
};
1144+
#[cfg(not(feature = "artifact-graph"))]
1145+
let _ = solve_analysis;
11331146
// Propagate warnings.
11341147
for warning in &solve_outcome.warnings {
11351148
let message = if let Some(index) = warning.about_constraint.as_ref() {
@@ -1196,6 +1209,8 @@ impl Node<SketchBlock> {
11961209
sketch
11971210
.constraints
11981211
.extend(std::mem::take(&mut sketch_block_state.sketch_constraints));
1212+
// Update the sketch object with freedom.
1213+
sketch.is_underconstrained = solve_analysis.map(|analysis| analysis.is_underconstrained);
11991214

12001215
// Push sketch solve operation
12011216
exec_state.push_op(Operation::SketchSolve {

rust/kcl-lib/src/execution/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ pub struct ExecOutcome {
113113
#[derive(Debug, Clone, PartialEq, Eq)]
114114
pub struct MockConfig {
115115
pub use_prev_memory: bool,
116+
/// True to do more costly analysis of whether the sketch block segments are
117+
/// under-constrained.
118+
pub freedom_analysis: bool,
116119
/// The segments that were edited that triggered this execution.
117120
#[cfg(feature = "artifact-graph")]
118121
pub segment_ids_edited: AhashIndexSet<ObjectId>,
@@ -123,6 +126,7 @@ impl Default for MockConfig {
123126
Self {
124127
// By default, use previous memory. This is usually what you want.
125128
use_prev_memory: true,
129+
freedom_analysis: false,
126130
#[cfg(feature = "artifact-graph")]
127131
segment_ids_edited: AhashIndexSet::default(),
128132
}
@@ -617,11 +621,12 @@ impl ExecutorContext {
617621
"To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
618622
);
619623

624+
let use_prev_memory = mock_config.use_prev_memory;
620625
#[cfg(not(feature = "artifact-graph"))]
621626
let mut exec_state = ExecState::new(self);
622627
#[cfg(feature = "artifact-graph")]
623-
let mut exec_state = ExecState::new_sketch_mode(self, mock_config.segment_ids_edited.clone());
624-
if mock_config.use_prev_memory {
628+
let mut exec_state = ExecState::new_sketch_mode(self, mock_config);
629+
if use_prev_memory {
625630
match cache::read_old_memory().await {
626631
Some(mem) => {
627632
*exec_state.mut_stack() = mem.0;

rust/kcl-lib/src/execution/state.rs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
99
use uuid::Uuid;
1010

1111
use crate::{
12-
CompilationError, EngineManager, ExecutorContext, KclErrorWithOutputs, SourceRange,
12+
CompilationError, EngineManager, ExecutorContext, KclErrorWithOutputs, MockConfig, SourceRange,
1313
collections::AhashIndexSet,
1414
errors::{KclError, KclErrorDetails, Severity},
1515
exec::DefaultPlanes,
@@ -128,6 +128,9 @@ pub(super) struct ModuleState {
128128
pub module_exports: Vec<String>,
129129
/// Settings specified from annotations.
130130
pub settings: MetaSettings,
131+
/// True to do more costly analysis of whether the sketch block segments are
132+
/// under-constrained.
133+
pub freedom_analysis: bool,
131134
pub(super) explicit_length_units: bool,
132135
pub(super) path: ModulePath,
133136
/// Artifacts for only this module.
@@ -151,14 +154,24 @@ impl ExecState {
151154
pub fn new(exec_context: &super::ExecutorContext) -> Self {
152155
ExecState {
153156
global: GlobalState::new(&exec_context.settings, Default::default()),
154-
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default(), 0),
157+
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default(), 0, false),
155158
}
156159
}
157160

158-
pub fn new_sketch_mode(exec_context: &super::ExecutorContext, segment_ids_edited: AhashIndexSet<ObjectId>) -> Self {
161+
pub fn new_sketch_mode(exec_context: &super::ExecutorContext, mock_config: &MockConfig) -> Self {
162+
#[cfg(feature = "artifact-graph")]
163+
let segment_ids_edited = mock_config.segment_ids_edited.clone();
164+
#[cfg(not(feature = "artifact-graph"))]
165+
let segment_ids_edited = Default::default();
159166
ExecState {
160167
global: GlobalState::new(&exec_context.settings, segment_ids_edited),
161-
mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default(), 0),
168+
mod_local: ModuleState::new(
169+
ModulePath::Main,
170+
ProgramMemory::new(),
171+
Default::default(),
172+
0,
173+
mock_config.freedom_analysis,
174+
),
162175
}
163176
}
164177

@@ -167,7 +180,13 @@ impl ExecState {
167180

168181
*self = ExecState {
169182
global,
170-
mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default(), 0),
183+
mod_local: ModuleState::new(
184+
self.mod_local.path.clone(),
185+
ProgramMemory::new(),
186+
Default::default(),
187+
0,
188+
false,
189+
),
171190
};
172191
}
173192

@@ -639,6 +658,7 @@ impl ModuleState {
639658
memory: Arc<ProgramMemory>,
640659
module_id: Option<ModuleId>,
641660
next_object_id: usize,
661+
freedom_analysis: bool,
642662
) -> Self {
643663
#[cfg(not(feature = "artifact-graph"))]
644664
let _ = next_object_id;
@@ -653,6 +673,7 @@ impl ModuleState {
653673
explicit_length_units: false,
654674
path,
655675
settings: Default::default(),
676+
freedom_analysis,
656677
#[cfg(not(feature = "artifact-graph"))]
657678
artifacts: Default::default(),
658679
#[cfg(feature = "artifact-graph")]

rust/kcl-lib/src/frontend.rs

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -228,17 +228,19 @@ impl SketchApi for FrontendState {
228228
// Enter sketch mode by setting the sketch_mode.
229229
self.scene_graph.sketch_mode = Some(sketch);
230230

231-
// Execute in mock mode to ensure state is up to date.
232-
let outcome = ctx
233-
.run_mock(&self.program, &MockConfig::default())
234-
.await
235-
.map_err(|err| {
236-
// TODO: sketch-api: Yeah, this needs to change. We need to
237-
// return the full error.
238-
Error {
239-
msg: err.error.message().to_owned(),
240-
}
241-
})?;
231+
// Execute in mock mode to ensure state is up to date. The caller will
232+
// want freedom analysis to display segments correctly.
233+
let mock_config = MockConfig {
234+
freedom_analysis: true,
235+
..Default::default()
236+
};
237+
let outcome = ctx.run_mock(&self.program, &mock_config).await.map_err(|err| {
238+
// TODO: sketch-api: Yeah, this needs to change. We need to
239+
// return the full error.
240+
Error {
241+
msg: err.error.message().to_owned(),
242+
}
243+
})?;
242244

243245
let outcome = self.update_state_after_exec(outcome);
244246
let scene_graph_delta = SceneGraphDelta {
@@ -884,6 +886,7 @@ impl FrontendState {
884886
// Execute.
885887
let mock_config = MockConfig {
886888
use_prev_memory: !is_delete,
889+
freedom_analysis: is_delete,
887890
#[cfg(feature = "artifact-graph")]
888891
segment_ids_edited,
889892
};
@@ -1425,16 +1428,17 @@ impl FrontendState {
14251428
self.program = new_program.clone();
14261429

14271430
// Execute.
1428-
let outcome = ctx
1429-
.run_mock(&new_program, &MockConfig::default())
1430-
.await
1431-
.map_err(|err| {
1432-
// TODO: sketch-api: Yeah, this needs to change. We need to
1433-
// return the full error.
1434-
Error {
1435-
msg: err.error.message().to_owned(),
1436-
}
1437-
})?;
1431+
let mock_config = MockConfig {
1432+
freedom_analysis: true,
1433+
..Default::default()
1434+
};
1435+
let outcome = ctx.run_mock(&new_program, &mock_config).await.map_err(|err| {
1436+
// TODO: sketch-api: Yeah, this needs to change. We need to
1437+
// return the full error.
1438+
Error {
1439+
msg: err.error.message().to_owned(),
1440+
}
1441+
})?;
14381442

14391443
let src_delta = SourceDelta { text: new_source };
14401444
let outcome = self.update_state_after_exec(outcome);
@@ -2056,6 +2060,7 @@ mod tests {
20562060
},
20572061
segments: vec![],
20582062
constraints: vec![],
2063+
is_underconstrained: None,
20592064
})
20602065
);
20612066
assert_eq!(scene_delta.new_graph.objects.len(), 1);
@@ -2159,6 +2164,7 @@ sketch(on = XY) {
21592164
},
21602165
segments: vec![],
21612166
constraints: vec![],
2167+
is_underconstrained: None,
21622168
})
21632169
);
21642170
assert_eq!(scene_delta.new_graph.objects.len(), 1);

rust/kcl-lib/src/frontend/sketch.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ pub struct Sketch {
9090
pub args: SketchArgs,
9191
pub segments: Vec<ObjectId>,
9292
pub constraints: Vec<ObjectId>,
93+
pub is_underconstrained: Option<bool>,
9394
}
9495

9596
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]

src/machines/sketchSolve/tools/moveTool/moveTool.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,7 @@ describe('createOnDragCallback', () => {
12171217
args: { on: { default: 'xy' } },
12181218
segments: [],
12191219
constraints: [],
1220+
is_underconstrained: null,
12201221
},
12211222
label: '',
12221223
comments: '',

0 commit comments

Comments
 (0)