21#include <polysolve/nonlinear/Solver.hpp>
23#include <spdlog/sinks/stdout_color_sinks.h>
24#include <spdlog/sinks/basic_file_sink.h>
25#include <spdlog/sinks/ostream_sink.h>
42 spdlog::level::level_enum,
43 {{spdlog::level::level_enum::trace,
"trace"},
44 {spdlog::level::level_enum::debug,
"debug"},
45 {spdlog::level::level_enum::info,
"info"},
46 {spdlog::level::level_enum::warn,
"warning"},
47 {spdlog::level::level_enum::err,
"error"},
48 {spdlog::level::level_enum::critical,
"critical"},
49 {spdlog::level::level_enum::off,
"off"},
50 {spdlog::level::level_enum::trace, 0},
51 {spdlog::level::level_enum::debug, 1},
52 {spdlog::level::level_enum::info, 2},
53 {spdlog::level::level_enum::warn, 3},
54 {spdlog::level::level_enum::err, 3},
55 {spdlog::level::level_enum::critical, 4},
56 {spdlog::level::level_enum::off, 5}})
63 bool is_failed_status(
const polysolve::nonlinear::Status status)
65 using Status = polysolve::nonlinear::Status;
66 return status == Status::NanEncountered
67 || status == Status::NotDescentDirection
68 || status == Status::LineSearchFailed
69 || status == Status::UpdateDirectionFailed
70 || status == Status::NotStarted
71 || status == Status::Continue;
74 std::string parse_remeshing_trigger(
const json &args)
76 const json &remeshing =
args[
"remeshing"];
77 if (remeshing.is_null())
80 const json &trigger = remeshing[
"trigger"];
81 if (trigger.is_null())
84 const bool periodic = trigger.contains(
"periodic") && trigger[
"periodic"].is_object();
85 const bool scaled_jacobian = trigger.contains(
"scaled_jacobian") && trigger[
"scaled_jacobian"].is_object();
86 if (periodic && scaled_jacobian)
91 return "scaled_jacobian";
98 std::vector<json> load_state_jsons(
const std::string &root_path,
const json &args)
100 std::vector<json> result;
101 for (
int i = 0; i <
args.size(); ++i)
105 std::ifstream
file(state_path);
109 state_args[
"root_path"] = state_path;
110 result.push_back(std::move(state_args));
115 void validate_remeshing_json(
const json &args)
117 const std::string trigger = parse_remeshing_trigger(args);
118 if (trigger ==
"multiple")
119 log_and_throw_error(
"Optimization remeshing requires exactly one trigger: periodic or scaled_jacobian.");
120 const json &remeshing =
args[
"remeshing"];
121 const int state = remeshing[
"state"];
122 if (state >= args[
"states"].size())
125 for (
const auto &variable :
args[
"variable_to_simulation"])
127 if (variable[
"type"] !=
"shape")
130 const json &variable_state = variable[
"state"];
131 const bool targets_state = variable_state.is_array()
132 ? std::find(variable_state.begin(), variable_state.end(), state) != variable_state.end()
133 : variable_state.get<
int>() == state;
137 const json &selection = variable[
"active_geometry_nodes"];
138 const bool selects_all_nodes = selection.is_array() && selection.empty();
139 if (selects_all_nodes || selection.is_object())
143 "Optimization remeshing changes mesh vertex numbering. "
144 "Shape variable {} targeting state {} must use a geometry-based "
145 "active_geometry_nodes selector (interior, boundary, or "
146 "boundary_excluding_surface), or an empty selector for all nodes.",
147 variable.value(
"name",
"shape"), state);
151 void validate_remeshing_geometry(
const json &state_args,
const int geometry)
153 if (geometry < 0 || geometry >= state_args[
"geometry"].size())
154 log_and_throw_error(
"Optimization remeshing geometry index {} is out of range.", geometry);
155 if (state_args[
"geometry"][geometry].value(
"is_obstacle",
false))
160 double minimum_scaled_jacobian(
161 const Eigen::MatrixXd &vertices,
162 const Eigen::MatrixXi &elements)
164 Eigen::VectorXd quality;
166 return quality.minCoeff();
170 double minimum_scaled_jacobian(
const varform::DifferentiableVarForm &varform)
173 Eigen::MatrixXi elements;
174 varform.get_vertices(vertices);
175 varform.get_elements(elements);
176 return minimum_scaled_jacobian(vertices, elements);
185 bool remesh_and_write(
186 const OptState &opt_state,
189 const std::filesystem::path &path,
190 const std::optional<double> quality_threshold)
192 mesh::MmgOptions options;
193 options.optim =
true;
196 Eigen::MatrixXi elements;
197 opt_state.varforms[state]->get_vertices(vertices);
198 opt_state.varforms[state]->get_elements(elements);
200 Eigen::MatrixXd remeshed_vertices;
201 Eigen::MatrixXi remeshed_boundary;
202 Eigen::MatrixXi remeshed_elements;
203 bool has_shape_variable =
false;
204 for (
const auto &var2sim : opt_state.variable_to_simulations.data)
206 const auto shape_v2s = std::dynamic_pointer_cast<solver::ShapeVariableToSimulation>(var2sim);
207 if (!shape_v2s || !shape_v2s->affects_varform(*opt_state.varforms[state]))
209 has_shape_variable =
true;
211 for (
int other_state = 0; other_state < opt_state.varforms.size(); ++other_state)
213 if (other_state != state
214 && shape_v2s->affects_varform(*opt_state.varforms[other_state]))
217 "Optimization remeshing cannot remesh state {} while its shape variable also affects state {}. This is not supported currently.",
222 if (!has_shape_variable)
224 logger().error(
"Can not remesh state {} because it's not affected by shape variables. No reason to remesh if you are not doing shape optimization.", state);
228 const mesh::Mesh &mesh = opt_state.varforms[state]->get_mesh();
229 bool success =
false;
230 if (mesh.dimension() == 2)
232 success = mesh::remesh_2d(
233 vertices, elements, remeshed_vertices, remeshed_elements,
238 success = mesh::remesh_3d(
239 vertices, elements, remeshed_vertices, remeshed_boundary,
240 remeshed_elements, options);
247 const double input_quality = minimum_scaled_jacobian(vertices, elements);
248 const double output_quality = minimum_scaled_jacobian(remeshed_vertices, remeshed_elements);
250 "Optimization remeshing minimum scaled Jacobian: {} -> {}.",
251 input_quality, output_quality);
253 if (quality_threshold.has_value() && output_quality <= *quality_threshold)
256 "MMG remeshing failed to satisfy minimum scaled Jacobian threshold: "
257 "input={}, output={}, required > {}. This is because MMG use a different convergence criteria, please switch remeshing trigger to periodic.",
258 input_quality, output_quality, *quality_threshold);
262 path.string(), remeshed_vertices, remeshed_elements,
263 std::vector<int>(remeshed_elements.rows(), body_id),
264 opt_state.varforms[state]->get_mesh().is_volume(),
false);
282 std::string mode = parse_remeshing_trigger(input_args);
283 bool remeshing_enabled = mode !=
"none";
284 if (remeshing_enabled)
285 validate_remeshing_json(input_args);
288 input_args[
"output"][
"directory"], input_args[
"root_path"],
false);
289 int max_restarts = remeshing_enabled
290 ? input_args[
"remeshing"][
"max_restarts"].get<
int>()
292 state_args = load_state_jsons(input_args[
"root_path"], input_args[
"states"]);
297 if (remeshing_enabled)
299 state = input_args[
"remeshing"][
"state"].get<
int>();
300 geometry = input_args[
"remeshing"][
"geometry"].get<
int>();
301 validate_remeshing_geometry(
state_args[state], geometry);
302 body_id =
state_args[state][
"geometry"][geometry].value(
"volume_selection", 0);
305 int remesh_count = 0;
311 json round_args = input_args;
312 if (remeshing_enabled)
314 const std::filesystem::path round_dir = output_root / fmt::format(
"remesh_round_{:d}", remesh_count);
315 round_args[
"output"][
"directory"] = round_dir.string();
316 state_args[state][
"output"][
"directory"] = round_dir.string();
319 init(round_args, strict_validation);
327 if (
args[
"compute_objective"].get<bool>())
333 const polysolve::nonlinear::Status status =
solve(
x);
334 if (is_failed_status(status))
336 logger().error(
"Optimization failed: {}.", polysolve::nonlinear::status_message(status));
343 if (remesh_count >= max_restarts)
345 logger().info(
"Reached the optimization remeshing limit of {} restart(s).", max_restarts);
349 const std::filesystem::path round_dir = output_root / fmt::format(
"remesh_round_{:d}", remesh_count);
350 const std::filesystem::path remeshed_path = std::filesystem::absolute(round_dir /
"remeshed.msh");
351 const std::optional<double> quality_threshold = mode ==
"scaled_jacobian"
352 ? std::optional<double>(
args[
"remeshing"][
"trigger"][
"scaled_jacobian"][
"quality_threshold"].get<double>())
354 if (!remesh_and_write(
355 *
this, state, body_id,
356 remeshed_path, quality_threshold))
358 logger().error(
"MMG failed to produce a valid optimization restart mesh.");
362 state_args[state][
"geometry"][geometry][
"mesh"] = remeshed_path.string();
363 logger().info(
"Restarting optimization from remeshed mesh {}.", remeshed_path.string());
369 const std::string &log_file,
370 const spdlog::level::level_enum log_level,
371 const spdlog::level::level_enum file_log_level,
374 std::vector<spdlog::sink_ptr> sinks;
378 console_sink_ = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
382 if (!log_file.empty())
384 file_sink_ = std::make_shared<spdlog::sinks::basic_file_sink_mt>(log_file,
true);
391 spdlog::flush_every(std::chrono::seconds(3));
396 std::vector<spdlog::sink_ptr> sinks;
397 sinks.emplace_back(std::make_shared<spdlog::sinks::ostream_sink_mt>(os,
false));
402 const std::vector<spdlog::sink_ptr> &sinks,
403 const spdlog::level::level_enum log_level)
405 set_adjoint_logger(std::make_shared<spdlog::logger>(
"adjoint-polyfem", sinks.begin(), sinks.end()));
422 json args_in = p_args_in;
429 std::filesystem::create_directories(
output_dir);
433 std::string out_path_log =
args[
"output"][
"log"][
"path"];
434 if (!out_path_log.empty())
441 args[
"output"][
"log"][
"level"],
442 args[
"output"][
"log"][
"file_level"],
443 args[
"output"][
"log"][
"quiet"]);
447 const int thread_in =
args[
"solver"][
"max_threads"];
456 size_t threads = max_threads <= 0
457 ? std::numeric_limits<unsigned int>::max()
463 if (!
args[
"output"][
"log"].empty())
464 cur_args[
"output"][
"log"].merge_patch(
args[
"output"][
"log"]);
471 diff_cache = std::make_shared<DiffCache>();
481 for (
int i = 0; i <
varforms.size(); ++i)
487 "varform::DifferentiableVarForm {} ({}) does not expose solve data required by optimization.",
495 "varform::DifferentiableVarForm {}: transient linear problem is not supported in optimization.", i);
501 if (!varform.
get_args()[
"contact"][
"use_gcp_formulation"].get<
bool>()
502 && !varform.
get_args()[
"contact"][
"use_convergent_formulation"].get<
bool>())
505 "varform::DifferentiableVarForm {}: non-convergent contact formulation is not supported in optimization.", i);
509 if (varform.
get_args()[
"/solver/contact/barrier_stiffness"_json_pointer].is_string())
512 "varform::DifferentiableVarForm {}: only constant barrier stiffness is supported in optimization.", i);
517 if (varform.
get_args().contains(
"boundary_conditions") && varform.
get_args()[
"boundary_conditions"].contains(
"rhs"))
519 const json &rhs = varform.
get_args()[
"boundary_conditions"][
"rhs"];
520 if (rhs.is_string() || (rhs.is_array() && rhs.size() > 0 && rhs[0].is_string()))
523 "varform::DifferentiableVarForm {}: only constant rhs over space is supported in optimization.", i);
530 for (
const auto &basis : element_bases.bases)
532 if (basis.order() > 1)
535 "varform::DifferentiableVarForm {}: high-order geometry basis is not supported in optimization.", i);
544 const json ¶meters =
args[
"parameters"];
545 bool is_auto = parameters.is_string() && parameters.get<std::string>() ==
"auto";
551 if (
args[
"variable_to_simulation"].size() != 1)
554 "Auto parameters are only supported with a single variable to simulation.");
559 if (composition[
"type"].get<std::string>() ==
"slice")
577 for (
const auto &arg :
args[
"parameters"])
592 int inv_dof = var2sim->inverse_dof();
596 "VariableToSimulation {} (type {}) expects {} DOF, but parameters define {} DOF.",
597 i, var2sim->name(), inv_dof,
ndof);
609 std::vector<std::shared_ptr<solver::AdjointForm>> stopping_conditions;
610 for (
const auto &arg :
args[
"stopping_conditions"])
611 stopping_conditions.push_back(
614 std::function<bool()> remeshing_trigger;
615 const std::string mode = parse_remeshing_trigger(
args);
616 if (mode ==
"periodic")
618 int period =
args[
"remeshing"][
"trigger"][
"periodic"][
"period"];
620 remeshing_trigger = [
this, period, iter = 0]()
mutable {
625 "Periodic optimization remeshing triggered after {} accepted iteration(s).", iter);
629 else if (mode ==
"scaled_jacobian")
631 int state =
args[
"remeshing"][
"state"];
632 double threshold =
args[
"remeshing"][
"trigger"][
"scaled_jacobian"][
"quality_threshold"];
635 remeshing_trigger = [
this, varform, threshold]() {
636 double quality = minimum_scaled_jacobian(*varform);
638 "Minimum scaled Jacobian: {} (remesh threshold: {}).",
645 nl_problem = std::make_unique<solver::AdjointNLProblem>(
647 args, std::move(remeshing_trigger));
666 args[
"solver"][
"nonlinear"],
667 args[
"solver"][
"linear"],
668 args[
"solver"][
"advanced"][
"characteristic_length"],
672 return nl_solver->status();
void initial_guess(Eigen::VectorXd &x)
void set_log_level(const spdlog::level::level_enum log_level)
change log level
json args
main input arguments containing all defaults
std::vector< std::shared_ptr< varform::DifferentiableVarForm > > varforms
Variational formulations used by the optimization.
void init(const json &args, const bool strict_validation)
initialize the polyfem solver with a json settings
int run(json args, const bool strict_validation)
Run optimization, including remeshing restarts when the selected trigger requests them.
std::vector< std::shared_ptr< DiffCache > > diff_caches
spdlog::sink_ptr file_sink_
std::string root_path() const
void check_unsupported() const
Check and throw if any forward simulation varform::DifferentiableVarForm is not supported.
double eval(Eigen::VectorXd &x) const
std::vector< int > variable_sizes
variables
std::unique_ptr< solver::AdjointNLProblem > nl_problem
std::string output_dir
Directory for output files.
std::vector< json > state_args
spdlog::sink_ptr console_sink_
logger sink to stdout
void init_logger(const std::string &log_file, const spdlog::level::level_enum log_level, const spdlog::level::level_enum file_log_level, const bool is_quiet)
initializing the logger
solver::VariableToSimulationGroup variable_to_simulations
void create_varforms(const int max_threads=-1)
Create the optimization variational formulations.
polysolve::nonlinear::Status solve(Eigen::VectorXd &x)
void init_variables()
init variables
bool remeshing_requested_
virtual bool is_time_dependent() const
static void write(const std::string &path, const mesh::Mesh &mesh, const bool binary)
saves the mesh
void update(const Eigen::VectorXd &x)
std::vector< std::shared_ptr< VariableToSimulation > > data
void set_logger(spdlog::logger &logger)
static GeogramUtils & instance()
void set_num_threads(const int max_threads)
std::shared_ptr< solver::AdjointForm > build_form(const json &args, const solver::VariableToSimulationGroup &var2sim, const std::vector< std::shared_ptr< varform::DifferentiableVarForm > > &varforms, const std::vector< std::shared_ptr< DiffCache > > &diff_caches)
std::shared_ptr< varform::DifferentiableVarForm > build_differentiable_varform(const json &args, const size_t max_threads)
solver::VariableToSimulationGroup build_variable_to_simulation_group(const json &args, const std::vector< std::shared_ptr< varform::DifferentiableVarForm > > &varforms, const std::vector< std::shared_ptr< DiffCache > > &diff_caches, const std::vector< int > &variable_sizes)
bool scaled_jacobian(Mesh3DStorage &hmi, Mesh_Quality &mq)
NLOHMANN_JSON_SERIALIZE_ENUM(CollisionProxyTessellation, {{CollisionProxyTessellation::REGULAR, "regular"}, {CollisionProxyTessellation::IRREGULAR, "irregular"}})
std::string resolve_path(const std::string &path, const std::string &input_file_path, const bool only_if_exists=false)
std::vector< T > json_as_array(const json &j)
Return the value of a json object as an array.
spdlog::logger & logger()
Retrieves the current logger.
spdlog::logger & adjoint_logger()
Retrieves the current logger for adjoint.
void set_adjoint_logger(std::shared_ptr< spdlog::logger > p_logger)
Setup a logger object to be used by adjoint Polyfem.
void log_and_throw_adjoint_error(const std::string &msg)
void log_and_throw_error(const std::string &msg)
static int compute_variable_size(const json &args, const std::vector< std::shared_ptr< varform::DifferentiableVarForm > > &varforms)
static json apply_opt_json_spec(const json &input_args, bool strict_validation)
static Eigen::VectorXd inverse_evaluation(const json &args, const int ndof, const std::vector< int > &variable_sizes, VariableToSimulationGroup &var2sim)
static std::shared_ptr< polysolve::nonlinear::Solver > make_nl_solver(const json &solver_params, const json &linear_solver_params, const double characteristic_length, const bool strict_validation=true)