diff --git a/Workflow/EXECUTION/RunPythonInThread.cpp b/Workflow/EXECUTION/RunPythonInThread.cpp index aa2ed9d57..b2d869320 100644 --- a/Workflow/EXECUTION/RunPythonInThread.cpp +++ b/Workflow/EXECUTION/RunPythonInThread.cpp @@ -24,7 +24,7 @@ RunPythonInThread::RunPythonInThread(const QString &theScript, const QStringList void RunPythonInThread::runProcess(void) { - RunPython *worker = new RunPython(script, args, workDir); + worker = new RunPython(script, args, workDir); worker->moveToThread(&workerThread); // @@ -84,6 +84,13 @@ RunPythonInThread::statusMessage(const QString &message) { } +void +RunPythonInThread::terminateProcess(void) { + if (worker != nullptr) { + QMetaObject::invokeMethod(worker, "terminate", Qt::QueuedConnection); + } +} + // // and now the RunPython that runs the python script in the new thread using QProcess @@ -234,3 +241,14 @@ void RunPython::handleProcessFinished(int exitCode, QProcess::ExitStatus exitSta emit processFinished(0); } +void +RunPython::terminate() { + if (process != nullptr) { + process->terminate(); + if (!process->waitForFinished(2000)) { + process->kill(); + process->waitForFinished(1000); + } + } +} + diff --git a/Workflow/EXECUTION/RunPythonInThread.h b/Workflow/EXECUTION/RunPythonInThread.h index 31c094890..3d0ef3388 100644 --- a/Workflow/EXECUTION/RunPythonInThread.h +++ b/Workflow/EXECUTION/RunPythonInThread.h @@ -22,6 +22,7 @@ public slots: void handleProcessTextOutput(void); void handleProcessErrorOutput(void); void handleProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + void terminate(); signals: void processFinished(int errorCode); @@ -44,6 +45,7 @@ class RunPythonInThread : public QObject RunPythonInThread(const QString &script, const QStringList &args, const QString &); virtual ~RunPythonInThread(); void runProcess(void); + void terminateProcess(void); public slots: @@ -59,4 +61,5 @@ public slots: QString script; QStringList args; QString workDir; + RunPython *worker {nullptr}; }; diff --git a/Workflow/SIM/SIM_Selection.cpp b/Workflow/SIM/SIM_Selection.cpp index c1d865c17..1ab247705 100644 --- a/Workflow/SIM/SIM_Selection.cpp +++ b/Workflow/SIM/SIM_Selection.cpp @@ -48,6 +48,7 @@ UPDATES, ENHANCEMENTS, OR MODIFICATIONS. #include #include #include +#include #include @@ -103,6 +104,10 @@ SIM_Selection::SIM_Selection(bool includeC, this->addComponent(QString("CustomPy"), QString("CustomPyInput"), custom_py); if (appName == "EE-UQ") { + + SimCenterAppWidget *ssi_simulation = new SSI_Simulation(); + this->addComponent(QString("SSI"), QString("SSISimulation"), ssi_simulation); + SimCenterAppWidget *femora = new Femora(); this->addComponent(QString("Femora"), QString("FemoraInput"), femora); } diff --git a/Workflow/SIM/SSI/SSI_BuildingWidgetBase.h b/Workflow/SIM/SSI/SSI_BuildingWidgetBase.h new file mode 100644 index 000000000..a85799341 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_BuildingWidgetBase.h @@ -0,0 +1,37 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#ifndef SSI_BUILDING_WIDGET_BASE_H +#define SSI_BUILDING_WIDGET_BASE_H + +#include +#include +#include +class QString; + +class SSI_BuildingWidgetBase : public QWidget { + Q_OBJECT +public: + explicit SSI_BuildingWidgetBase(QWidget* parent = nullptr) : QWidget(parent) {} + ~SSI_BuildingWidgetBase() override = default; + + virtual QString typeId() const = 0; + + virtual bool validate(QStringList& errors, bool interactiveIfModelMissing = false) const = 0; + virtual bool outputToJSON(QJsonObject& structureInfo) const = 0; + virtual bool inputFromJSON(const QJsonObject& structureInfo) = 0; + virtual void plot() const = 0; + virtual int getNumberOfCores() const = 0; + + // Copy any external files required for this building configuration into destDir + virtual bool copyFiles(QString &destDir) = 0; + + // Return the list of random variable names used by this building widget + virtual QStringList getRandomVariableNames() const = 0; +}; + +#endif // SSI_BUILDING_WIDGET_BASE_H + + diff --git a/Workflow/SIM/SSI/SSI_Custom3DBuildingWidget.cpp b/Workflow/SIM/SSI/SSI_Custom3DBuildingWidget.cpp new file mode 100644 index 000000000..1821e161a --- /dev/null +++ b/Workflow/SIM/SSI/SSI_Custom3DBuildingWidget.cpp @@ -0,0 +1,437 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#include "SSI_Custom3DBuildingWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SSI_Custom3DBuildingWidget::SSI_Custom3DBuildingWidget(QWidget* parent) : SSI_BuildingWidgetBase(parent) { + auto topLayout = new QVBoxLayout(); + + auto filesCoresBox = new QGroupBox("Files & Cores"); + setupFilesAndCoresGroup(filesCoresBox); + topLayout->addWidget(filesCoresBox); + + auto columnsBox = new QGroupBox("Columns base"); + auto boundsBox = new QGroupBox("Bounds"); + setupBoundsGroup(boundsBox); + topLayout->addWidget(boundsBox); + + setupColumnsBaseTable(columnsBox); + // Give the columns group stretch so the table expands vertically + topLayout->addWidget(columnsBox, 1); + + this->setLayout(topLayout); +} + +void SSI_Custom3DBuildingWidget::setupFilesAndCoresGroup(QWidget* parentWidget) { + auto grid = new QGridLayout(parentWidget); + int row = 0; + + grid->addWidget(new QLabel("Model file"), row, 0); + modelFileLineEdit = new QLineEdit(); + modelFileLineEdit->setPlaceholderText("/path/to/model.tcl"); + auto chooseModel = new QPushButton("Choose"); + grid->addWidget(modelFileLineEdit, row, 1); + grid->addWidget(chooseModel, row, 2); + row++; + + grid->addWidget(new QLabel("Mesh file (optional)"), row, 0); + meshFileLineEdit = new QLineEdit(); + meshFileLineEdit->setPlaceholderText("(Optional)"); + auto chooseMesh = new QPushButton("Choose"); + grid->addWidget(meshFileLineEdit, row, 1); + grid->addWidget(chooseMesh, row, 2); + row++; + + // Response nodes (space/comma-separated list like OpenSeesBuildingModel) + grid->addWidget(new QLabel("Response Nodes"), row, 0); + responseNodesLineEdit = new QLineEdit(); + responseNodesLineEdit->setPlaceholderText("e.g. 101 201 301 or 101,201,301"); + grid->addWidget(responseNodesLineEdit, row, 1); + row++; + + grid->addWidget(new QLabel("Num partitions"), row, 0); + numPartitionsSpin = new QSpinBox(); + numPartitionsSpin->setMinimum(1); + numPartitionsSpin->setMaximum(1000000); + numPartitionsSpin->setValue(1); + grid->addWidget(numPartitionsSpin, row, 1); + row++; + + grid->setColumnStretch(1, 1); + + connect(chooseModel, &QPushButton::clicked, this, [this]() { + auto fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QString(), tr("OpenSees tcl file (*.tcl);;All files (*.*)")); + if (!fileName.isEmpty()) { + modelFileLineEdit->setText(fileName); + onModelFileChanged(fileName); + } + }); + connect(modelFileLineEdit, &QLineEdit::textChanged, this, [this](const QString& path){ onModelFileChanged(path); }); + connect(chooseMesh, &QPushButton::clicked, this, [this]() { + auto fileName = QFileDialog::getOpenFileName(this, tr("Open File"), QString(), tr("All files (*.*)")); + if (!fileName.isEmpty()) + meshFileLineEdit->setText(fileName); + }); +} + +void SSI_Custom3DBuildingWidget::setupColumnsBaseTable(QWidget* parentWidget) { + auto vbox = new QVBoxLayout(parentWidget); + columnsTable = new QTableWidget(); + columnsTable->setColumnCount(4); + QStringList headers; headers << "tag" << "x" << "y" << "z"; + columnsTable->setHorizontalHeaderLabels(headers); + columnsTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + columnsTable->setSelectionBehavior(QAbstractItemView::SelectRows); + columnsTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); + vbox->addWidget(columnsTable); + + auto hbox = new QHBoxLayout(); + auto addRowBtn = new QPushButton("Add row"); + auto removeRowBtn = new QPushButton("Remove"); + auto clearBtn = new QPushButton("Clear"); + hbox->addWidget(addRowBtn); + hbox->addWidget(removeRowBtn); + hbox->addWidget(clearBtn); + hbox->addStretch(1); + vbox->addLayout(hbox); + + connect(addRowBtn, &QPushButton::clicked, this, [this]() { + int row = columnsTable->rowCount(); + columnsTable->insertRow(row); + for (int c = 0; c < 4; ++c) { + columnsTable->setItem(row, c, new QTableWidgetItem()); + } + }); + connect(removeRowBtn, &QPushButton::clicked, this, [this]() { + auto ranges = columnsTable->selectedRanges(); + if (!ranges.isEmpty()) { + int first = ranges.first().topRow(); + int last = ranges.first().bottomRow(); + for (int r = last; r >= first; --r) + columnsTable->removeRow(r); + } else if (columnsTable->rowCount() > 0) { + columnsTable->removeRow(columnsTable->rowCount() - 1); + } + }); + connect(clearBtn, &QPushButton::clicked, this, [this]() { + columnsTable->setRowCount(0); + }); +} + +void SSI_Custom3DBuildingWidget::setupBoundsGroup(QWidget* parentWidget) { + auto grid = new QGridLayout(parentWidget); + int row = 0; + + // First row: x_min, x_max + grid->addWidget(new QLabel("x_min"), row, 0); + xMinSpin = new QDoubleSpinBox(); xMinSpin->setDecimals(6); xMinSpin->setRange(-1e9, 1e9); xMinSpin->setValue(-13.716); + grid->addWidget(xMinSpin, row, 1); + grid->addWidget(new QLabel("x_max"), row, 2); + xMaxSpin = new QDoubleSpinBox(); xMaxSpin->setDecimals(6); xMaxSpin->setRange(-1e9, 1e9); xMaxSpin->setValue(13.716); + grid->addWidget(xMaxSpin, row, 3); + row++; + + // Second row: y_min, y_max + grid->addWidget(new QLabel("y_min"), row, 0); + yMinSpin = new QDoubleSpinBox(); yMinSpin->setDecimals(6); yMinSpin->setRange(-1e9, 1e9); yMinSpin->setValue(-13.716); + grid->addWidget(yMinSpin, row, 1); + grid->addWidget(new QLabel("y_max"), row, 2); + yMaxSpin = new QDoubleSpinBox(); yMaxSpin->setDecimals(6); yMaxSpin->setRange(-1e9, 1e9); yMaxSpin->setValue(13.716); + grid->addWidget(yMaxSpin, row, 3); + row++; + + // Third row: z_min, z_max + grid->addWidget(new QLabel("z_min"), row, 0); + zMinSpin = new QDoubleSpinBox(); zMinSpin->setDecimals(6); zMinSpin->setRange(-1e9, 1e9); zMinSpin->setValue(0.0); + grid->addWidget(zMinSpin, row, 1); + grid->addWidget(new QLabel("z_max"), row, 2); + zMaxSpin = new QDoubleSpinBox(); zMaxSpin->setDecimals(6); zMaxSpin->setRange(-1e9, 1e9); zMaxSpin->setValue(40.8432); + grid->addWidget(zMaxSpin, row, 3); + + grid->setColumnStretch(1, 1); + grid->setColumnStretch(3, 1); +} + +bool SSI_Custom3DBuildingWidget::getTableRow(int row, int& tag, double& x, double& y, double& z) const { + auto itemTag = columnsTable->item(row, 0); + auto itemX = columnsTable->item(row, 1); + auto itemY = columnsTable->item(row, 2); + auto itemZ = columnsTable->item(row, 3); + if (!itemTag || !itemX || !itemY || !itemZ) + return false; + bool okTag = false, okX = false, okY = false, okZ = false; + tag = itemTag->text().toInt(&okTag); + x = itemX->text().toDouble(&okX); + y = itemY->text().toDouble(&okY); + z = itemZ->text().toDouble(&okZ); + return okTag && okX && okY && okZ; +} + +bool SSI_Custom3DBuildingWidget::validate(QStringList& errors, bool interactiveIfModelMissing) const { + errors.clear(); + + if (numPartitionsSpin->value() < 1) + errors << "num_partitions must be >= 1"; + + if (!(xMinSpin->value() < xMaxSpin->value())) errors << "x_min must be < x_max"; + if (!(yMinSpin->value() < yMaxSpin->value())) errors << "y_min must be < y_max"; + if (!(zMinSpin->value() < zMaxSpin->value())) errors << "z_min must be < z_max"; + + const QString modelPath = modelFileLineEdit->text(); + if (modelPath.isEmpty()) { + errors << "model_file is required"; + } else if (!QFileInfo::exists(modelPath)) { + if (!interactiveIfModelMissing) + errors << QString("model_file does not exist: %1").arg(modelPath); + } + + QSet seenTags; + const double xmin = xMinSpin->value(), xmax = xMaxSpin->value(); + const double ymin = yMinSpin->value(), ymax = yMaxSpin->value(); + const double zmin = zMinSpin->value(), zmax = zMaxSpin->value(); + + if (columnsTable->rowCount() == 0) { + errors << "columns table is empty"; + } + + + for (int r = 0; r < columnsTable->rowCount(); ++r) { + int tag; double x, y, z; + if (!getTableRow(r, tag, x, y, z)) { + errors << QString("Row %1 has invalid data").arg(r + 1); + continue; + } + if (seenTags.contains(tag)) + errors << QString("Row %1 tag %2 is duplicated").arg(r + 1).arg(tag); + seenTags.insert(tag); + if (!(x >= xmin && x <= xmax && y >= ymin && y <= ymax && z >= zmin && z <= zmax)) + errors << QString("Row %1 point is outside bounds").arg(r + 1); + } + + return errors.isEmpty(); +} + +bool SSI_Custom3DBuildingWidget::outputToJSON(QJsonObject& structureInfo) const { + structureInfo["num_partitions"] = numPartitionsSpin->value(); + structureInfo["x_min"] = xMinSpin->value(); + structureInfo["y_min"] = yMinSpin->value(); + structureInfo["z_min"] = zMinSpin->value(); + structureInfo["x_max"] = xMaxSpin->value(); + structureInfo["y_max"] = yMaxSpin->value(); + structureInfo["z_max"] = zMaxSpin->value(); + + // response nodes: parse whitespace/comma separated + QJsonArray responseNodeTags; + { + const QString txt = responseNodesLineEdit ? responseNodesLineEdit->text() : QString(); + QString cleaned = txt; + cleaned.replace(',', ' '); + QTextStream ts(&cleaned, QIODevice::ReadOnly); + while (!ts.atEnd()) { + QString token; ts >> token; + bool ok=false; int tag = token.toInt(&ok); + if (ok) responseNodeTags.append(tag); + } + } + structureInfo["responseNodes"] = responseNodeTags; + + // Build NodeMapping from response nodes (cline="response", floor starting at 0) + { + QJsonArray mappingArray; + int floor = 0; + for (int i = 0; i < responseNodeTags.size(); ++i) { + const int tag = responseNodeTags.at(i).toInt(); + QJsonObject nodeEntry; + nodeEntry["node"] = tag; + nodeEntry["cline"] = QString("response"); + nodeEntry["floor"] = QString::number(floor++); + mappingArray.append(nodeEntry); + } + structureInfo["NodeMapping"] = mappingArray; + } + structureInfo["useDamping"] = false; + structureInfo["ndf"] = 6; + structureInfo["ndm"] = 3; + structureInfo["dampingRatio"] = 0; + structureInfo["centroidNodes"] = QJsonArray(); + + + + + const QString modelPath = modelFileLineEdit->text(); + structureInfo["model_file"] = modelPath; + const QString meshPath = meshFileLineEdit->text(); + if (meshPath.isEmpty()) + structureInfo["mesh_file"] = QJsonValue(); + else + structureInfo["mesh_file"] = meshPath; + + QJsonArray columnsArr; + for (int r = 0; r < columnsTable->rowCount(); ++r) { + int tag; double x, y, z; + if (!getTableRow(r, tag, x, y, z)) + continue; + QJsonObject col; + col["tag"] = tag; + col["x"] = x; + col["y"] = y; + col["z"] = z; + columnsArr.append(col); + } + structureInfo["columns_base"] = columnsArr; + return true; +} + +bool SSI_Custom3DBuildingWidget::inputFromJSON(const QJsonObject& structureInfo) { + // No validation here; simply populate fields if keys exist + if (structureInfo.contains("num_partitions")) + numPartitionsSpin->setValue(structureInfo.value("num_partitions").toInt(1)); + + xMinSpin->setValue(structureInfo.value("x_min").toDouble(xMinSpin->value())); + yMinSpin->setValue(structureInfo.value("y_min").toDouble(yMinSpin->value())); + zMinSpin->setValue(structureInfo.value("z_min").toDouble(zMinSpin->value())); + xMaxSpin->setValue(structureInfo.value("x_max").toDouble(xMaxSpin->value())); + yMaxSpin->setValue(structureInfo.value("y_max").toDouble(yMaxSpin->value())); + zMaxSpin->setValue(structureInfo.value("z_max").toDouble(zMaxSpin->value())); + + modelFileLineEdit->setText(structureInfo.value("model_file").toString()); + auto meshVal = structureInfo.value("mesh_file"); + if (meshVal.isUndefined() || meshVal.isNull()) + meshFileLineEdit->clear(); + else + meshFileLineEdit->setText(meshVal.toString()); + + // response nodes + if (structureInfo.contains("responseNodes")) { + QString nodesStr; + QJsonArray nodeTags = structureInfo.value("responseNodes").toArray(); + for (int i = 0; i < nodeTags.size(); ++i) { + if (i>0) nodesStr += " "; + nodesStr += QString::number(nodeTags.at(i).toInt()); + } + if (responseNodesLineEdit) + responseNodesLineEdit->setText(nodesStr); + } + + // columns_base + columnsTable->setRowCount(0); + auto cols = structureInfo.value("columns_base").toArray(); + for (const auto& v : cols) { + auto obj = v.toObject(); + int row = columnsTable->rowCount(); + columnsTable->insertRow(row); + columnsTable->setItem(row, 0, new QTableWidgetItem(QString::number(obj.value("tag").toInt()))); + columnsTable->setItem(row, 1, new QTableWidgetItem(QString::number(obj.value("x").toDouble()))); + columnsTable->setItem(row, 2, new QTableWidgetItem(QString::number(obj.value("y").toDouble()))); + columnsTable->setItem(row, 3, new QTableWidgetItem(QString::number(obj.value("z").toDouble()))); + } + + + // collect the random variable names from the model file + OpenSeesParser parser; + // empty the list + psetVarNamesAndValues.clear(); + psetVarNamesAndValues = parser.getVariables(modelFileLineEdit->text()); + return true; +} + +void SSI_Custom3DBuildingWidget::plot() const { + QMessageBox::information(nullptr, QString("Plot"), QString("Plotting not implemented yet.")); +} + +bool SSI_Custom3DBuildingWidget::copyFiles(QString &destDir) { + // Copy model directory and create a modified copy of the main TCL with RV handling (pset) + const QString modelPath = modelFileLineEdit ? modelFileLineEdit->text() : QString(); + if (!modelPath.isEmpty()) { + QFileInfo fi(modelPath); + const QString modelDir = fi.path(); + const QString modelFile = fi.fileName(); + + // Copy entire model directory to preserve relative includes + SimCenterAppWidget::copyPath(modelDir, destDir, false); + + // Rewrite the main TCL in destination with RV-aware modifications + RandomVariablesContainer *rvc = RandomVariablesContainer::getInstance(); + QStringList varNames = rvc->getRandomVariableNames(); + OpenSeesParser parser; + const QString copiedFile = destDir + QDir::separator() + modelFile; + parser.writeFile(modelPath, copiedFile, varNames); + } + + // Mesh file: ensure present if outside model directory + const QString meshPath = meshFileLineEdit ? meshFileLineEdit->text() : QString(); + if (!meshPath.isEmpty()) { + SimCenterAppWidget::copyFile(meshPath, destDir); + } + + return true; +} + +SSI_Custom3DBuildingWidget::~SSI_Custom3DBuildingWidget() { + removeRegisteredPsets(); +} + +void SSI_Custom3DBuildingWidget::removeRegisteredPsets() { + if (psetVarNamesAndValues.isEmpty()) + return; + QStringList names; + for (int i = 0; i < psetVarNamesAndValues.size() - 1; i += 2) + names.append(psetVarNamesAndValues.at(i)); + RandomVariablesContainer::getInstance()->removeRandomVariables(names); + psetVarNamesAndValues.clear(); +} + +void SSI_Custom3DBuildingWidget::onModelFileChanged(const QString& filePath) { + // Clear previously registered psets + removeRegisteredPsets(); + + if (filePath.isEmpty() || !QFileInfo::exists(filePath)) + return; + + // Parse TCL for pset variables and register them as constants + OpenSeesParser parser; + psetVarNamesAndValues = parser.getVariables(filePath); + if (psetVarNamesAndValues.isEmpty()) + return; + + RandomVariablesContainer* rvc = RandomVariablesContainer::getInstance(); + rvc->addConstantRVs(psetVarNamesAndValues); +} + +QStringList SSI_Custom3DBuildingWidget::getRandomVariableNames() const { + QStringList names; + for (int i = 0; i < psetVarNamesAndValues.size() - 1; i += 2) + names.append(psetVarNamesAndValues.at(i)); + return names; +} + +int SSI_Custom3DBuildingWidget::getNumberOfCores() const { + int numCores = int(numPartitionsSpin->value()); + return numCores; +} \ No newline at end of file diff --git a/Workflow/SIM/SSI/SSI_Custom3DBuildingWidget.h b/Workflow/SIM/SSI/SSI_Custom3DBuildingWidget.h new file mode 100644 index 000000000..1a1b7beb5 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_Custom3DBuildingWidget.h @@ -0,0 +1,59 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#ifndef SSI_CUSTOM3D_BUILDING_WIDGET_H +#define SSI_CUSTOM3D_BUILDING_WIDGET_H + +#include "SSI_BuildingWidgetBase.h" + +class QGroupBox; +class QLineEdit; +class QSpinBox; +class QDoubleSpinBox; +class QTableWidget; + +class SSI_Custom3DBuildingWidget : public SSI_BuildingWidgetBase { + Q_OBJECT +public: + explicit SSI_Custom3DBuildingWidget(QWidget* parent = nullptr); + ~SSI_Custom3DBuildingWidget() override; + + QString typeId() const override { return QStringLiteral("custom_3d_building"); } + + bool validate(QStringList& errors, bool interactiveIfModelMissing = false) const override; + bool outputToJSON(QJsonObject& structureInfo) const override; + bool inputFromJSON(const QJsonObject& structureInfo) override; + void plot() const override; + int getNumberOfCores() const override; + + bool copyFiles(QString &destDir) override; + QStringList getRandomVariableNames() const override; + +private: + QLineEdit* modelFileLineEdit {nullptr}; + QLineEdit* meshFileLineEdit {nullptr}; + QLineEdit* responseNodesLineEdit {nullptr}; + QSpinBox* numPartitionsSpin {nullptr}; + QTableWidget* columnsTable {nullptr}; + QDoubleSpinBox *xMinSpin {nullptr}, *xMaxSpin {nullptr}; + QDoubleSpinBox *yMinSpin {nullptr}, *yMaxSpin {nullptr}; + QDoubleSpinBox *zMinSpin {nullptr}, *zMaxSpin {nullptr}; + QStringList psetVarNamesAndValues; + + void setupFilesAndCoresGroup(QWidget* parentWidget); + void setupColumnsBaseTable(QWidget* parentWidget); + void setupBoundsGroup(QWidget* parentWidget); + + bool getTableRow(int row, int& tag, double& x, double& y, double& z) const; + + void onModelFileChanged(const QString& filePath); + void removeRegisteredPsets(); +}; + +#endif // SSI_CUSTOM3D_BUILDING_WIDGET_H + + + + diff --git a/Workflow/SIM/SSI/SSI_PlotterWidget.cpp b/Workflow/SIM/SSI/SSI_PlotterWidget.cpp new file mode 100644 index 000000000..27be4c6a1 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_PlotterWidget.cpp @@ -0,0 +1,53 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#include "SSI_PlotterWidget.h" + +#include +#include +#include +#include +#include +#include +#include + +SSI_PlotterWidget::SSI_PlotterWidget(QWidget* parent) : QWidget(parent) { + setupUi(); +} + +void SSI_PlotterWidget::setupUi() { + auto layout = new QVBoxLayout(this); + auto header = new QLabel("Plotter (VTK placeholder)\nThis will render the current model. For now, it echoes JSON."); + header->setWordWrap(true); + layout->addWidget(header); + + renderWidget = new QVTKOpenGLNativeWidget(); + renderWidget->setMinimumHeight(300); + layout->addWidget(renderWidget, 2); + + jsonView = new QPlainTextEdit(); + jsonView->setReadOnly(true); + layout->addWidget(jsonView, 1); +} + +void SSI_PlotterWidget::setupOrUpdateVtk() { + if(!renderer) { + renderer = vtkSmartPointer::New(); + renderWindow = vtkSmartPointer::New(); + renderWindow->AddRenderer(renderer); + renderWidget->setRenderWindow(renderWindow); + } + renderWindow->Render(); +} + +void SSI_PlotterWidget::renderModel(const QJsonObject& modelingObj) { + // Update VTK scene here later; for now show JSON + QJsonDocument doc(modelingObj); + jsonView->setPlainText(QString::fromUtf8(doc.toJson(QJsonDocument::Indented))); + setupOrUpdateVtk(); +} + + + diff --git a/Workflow/SIM/SSI/SSI_PlotterWidget.h b/Workflow/SIM/SSI/SSI_PlotterWidget.h new file mode 100644 index 000000000..70845f0b7 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_PlotterWidget.h @@ -0,0 +1,45 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#ifndef SSI_PLOTTER_WIDGET_H +#define SSI_PLOTTER_WIDGET_H + +#include +#include +#include +#include + +class vtkRenderer; +class vtkGenericOpenGLRenderWindow; + +class QPlainTextEdit; +class QLabel; + +class SSI_PlotterWidget : public QWidget { + Q_OBJECT +public: + explicit SSI_PlotterWidget(QWidget* parent = nullptr); + ~SSI_PlotterWidget() override = default; + + // Render/update view from a Modeling JSON payload + void renderModel(const QJsonObject& modelingObj); + +private: + QVTKOpenGLNativeWidget* renderWidget {nullptr}; + QPlainTextEdit* jsonView {nullptr}; + + vtkSmartPointer renderer; + vtkSmartPointer renderWindow; + + void setupUi(); + void setupOrUpdateVtk(); +}; + +#endif // SSI_PLOTTER_WIDGET_H + + + + + diff --git a/Workflow/SIM/SSI/SSI_Simulation.cpp b/Workflow/SIM/SSI/SSI_Simulation.cpp new file mode 100644 index 000000000..c1b5c7d60 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_Simulation.cpp @@ -0,0 +1,429 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#include "SSI_Simulation.h" +#include "SSI_Custom3DBuildingWidget.h" +#include "SSI_SoilFoundationType1Widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SSI_Simulation::SSI_Simulation(QWidget* parent) : SimCenterAppWidget(parent) { + auto mainLayout = new QVBoxLayout(); + + tabWidget = new QTabWidget(); + soilTabContainer = new QWidget(); + buildingTabContainer = new QWidget(); + auto buildingContainerLayout = new QVBoxLayout(buildingTabContainer); + buildingSelector = new QComboBox(); + buildingStack = new QStackedWidget(); + + // Register available building widgets + auto custom3D = new SSI_Custom3DBuildingWidget(); + buildingSelector->addItem("Custom 3D Building", custom3D->typeId()); + buildingStack->addWidget(custom3D); + currentBuilding = custom3D; + + buildingContainerLayout->addWidget(buildingSelector); + buildingContainerLayout->addWidget(buildingStack, 1); + + // Soil tab UI with selector and stacked widget + auto soilContainerLayout = new QVBoxLayout(soilTabContainer); + soilSelector = new QComboBox(); + soilStack = new QStackedWidget(); + // register Soil & Foundation Type 1 + auto soil1 = new SSI_SoilFoundationType1Widget(); + soilSelector->addItem("Soil & Foundation Type 1", soil1->typeId()); + soilStack->addWidget(soil1); + currentSoil = soil1; + soilContainerLayout->addWidget(soilSelector); + soilContainerLayout->addWidget(soilStack, 1); + + tabWidget->addTab(buildingTabContainer, "Building"); + tabWidget->addTab(soilTabContainer, "Soil and Foundation"); + mainLayout->addWidget(tabWidget, 1); + + auto buttonsLayout = new QHBoxLayout(); + plotButton = new QPushButton("Plot"); + validateButton = new QPushButton("Validate"); + + // Make key action buttons larger and more prominent + { + const int minHeight = 44; + const int minWidth = 160; + plotButton->setMinimumHeight(minHeight); + validateButton->setMinimumHeight(minHeight); + plotButton->setMinimumWidth(minWidth); + validateButton->setMinimumWidth(minWidth); + + QFont btnFont = plotButton->font(); + if (btnFont.pointSize() > 0) { + btnFont.setPointSize(btnFont.pointSize() + 4); + } else { + btnFont.setPointSize(14); + } + btnFont.setBold(true); + plotButton->setFont(btnFont); + validateButton->setFont(btnFont); + } + buttonsLayout->addStretch(1); + buttonsLayout->addWidget(plotButton); + buttonsLayout->addWidget(validateButton); + mainLayout->addLayout(buttonsLayout); + + setLayout(mainLayout); + + connect(plotButton, &QPushButton::clicked, this, &SSI_Simulation::onPlotClicked); + connect(validateButton, &QPushButton::clicked, this, &SSI_Simulation::onValidateClicked); + + connect(buildingSelector, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int idx) { + buildingStack->setCurrentIndex(idx); + currentBuilding = qobject_cast(buildingStack->currentWidget()); + }); + + connect(soilSelector, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int idx) { + soilStack->setCurrentIndex(idx); + currentSoil = qobject_cast(soilStack->currentWidget()); + }); +} + +SSI_Simulation::~SSI_Simulation() {} + +bool SSI_Simulation::outputToJSON(QJsonObject &jsonObj) { + // first validate + QStringList errors; + if (!this->validate(errors)) { + // error message box + QMessageBox msgBox(QMessageBox::Critical, + tr("Validation Error"), + tr("Cannot output to for SImulation SSI. \nPlease fix the following errors on SSI:\n\n --") + errors.join("\n --"), + QMessageBox::Ok, + this); + msgBox.ensurePolished(); + const QPoint parentCenterGlobal = this->mapToGlobal(this->rect().center()); + const QSize dialogSize = msgBox.sizeHint(); + msgBox.move(parentCenterGlobal - QPoint(dialogSize.width() / 2, dialogSize.height() / 2)); + msgBox.exec(); + return false; + } + // } else { + // // continue + // QMessageBox msgBox(QMessageBox::Information, + // tr("Validation"), + // tr("Validation passed."), + // QMessageBox::Ok, + // this); + // msgBox.ensurePolished(); + // const QPoint parentCenterGlobal = this->mapToGlobal(this->rect().center()); + // const QSize dialogSize = msgBox.sizeHint(); + // msgBox.move(parentCenterGlobal - QPoint(dialogSize.width() / 2, dialogSize.height() / 2)); + // msgBox.exec(); + // } + + jsonObj["type"] = "SSISimulation"; + jsonObj["building_type"] = currentBuilding->typeId(); + QJsonObject structureInfo; + if (currentBuilding->outputToJSON(structureInfo)) { + jsonObj["structure_info"] = structureInfo; + } else { + QMessageBox::critical(this, "Error", "Failed to output structure info to JSON for SSI Simulation."); + return false; + } + if (currentSoil) { + jsonObj["soil_type"] = currentSoil->typeId(); + QJsonObject soilInfo; + if (currentSoil->outputToJSON(soilInfo)) { + jsonObj["soil_foundation_info"] = soilInfo; + } else { + QMessageBox::critical(this, "Error", "Failed to output soil & foundation info to JSON for SSI Simulation."); + return false; + } + } + // Aggregate Random Variables reported by active widgets + { + QJsonArray rvArray; + QSet seen; + + if (currentBuilding) { + const QStringList names = currentBuilding->getRandomVariableNames(); + for (const QString &name : names) { + if (name.isEmpty() || seen.contains(name)) continue; + seen.insert(name); + QJsonObject rvObj; rvObj["name"] = name; rvObj["value"] = QString("RV.") + name; rvArray.append(rvObj); + } + } + if (currentSoil) { + const QStringList names = currentSoil->getRandomVariableNames(); + for (const QString &name : names) { + if (name.isEmpty() || seen.contains(name)) continue; + seen.insert(name); + QJsonObject rvObj; rvObj["name"] = name; rvObj["value"] = QString("RV.") + name; rvArray.append(rvObj); + } + } + + jsonObj["randomVar"] = rvArray; + } + + // add numCores + int const totalCores = getNumberOfCores(); + jsonObj["numCores"] = totalCores; + + return true; +} + +bool SSI_Simulation::inputFromJSON(QJsonObject &jsonObj) { + if (jsonObj.contains("type") && jsonObj.value("type").toString() != "SSISimulation") { + QMessageBox::critical(this, "Error", "The provided JSON is not of type 'SSISimulation'."); + return false; + } + if (jsonObj.contains("type") && jsonObj.value("type").toString() == "SSISimulation") { + if (jsonObj.contains("building_type")) { + const QString type = jsonObj.value("building_type").toString(); + for (int i = 0; i < buildingSelector->count(); ++i) { + if (buildingSelector->itemData(i).toString() == type) { + buildingSelector->setCurrentIndex(i); + break; + } + } + } + // Delegate to current building + if (currentBuilding && jsonObj.contains("structure_info")) { + const QJsonObject structureInfo = jsonObj.value("structure_info").toObject(); + if (!currentBuilding->inputFromJSON(structureInfo)) + QMessageBox::critical(this, "Error", "Failed to load structure info from JSON to the current building widget."); + } else if (!currentBuilding) { + QMessageBox::critical(this, "Error", "No building widget is currently selected to load structure info."); + return false; + } else if (!jsonObj.contains("structure_info")) { + QMessageBox::warning(this, "Warning", "No structure_info found in JSON to load."); + } + + if (jsonObj.contains("soil_type")) { + const QString soilType = jsonObj.value("soil_type").toString(); + for (int i = 0; i < soilSelector->count(); ++i) { + if (soilSelector->itemData(i).toString() == soilType) { + soilSelector->setCurrentIndex(i); + break; + } + } + } + if (currentSoil && jsonObj.contains("soil_foundation_info")) { + const QJsonObject soilInfo = jsonObj.value("soil_foundation_info").toObject(); + if (!currentSoil->inputFromJSON(soilInfo)) + QMessageBox::critical(this, "Error", "Failed to load soil & foundation info from JSON to the current soil widget."); + } else if (jsonObj.contains("soil_foundation_info") && !currentSoil) { + QMessageBox::warning(this, "Warning", "Soil & foundation info is present but no soil widget is registered."); + } + } + return true; +} + +bool SSI_Simulation::outputAppDataToJSON(QJsonObject &jsonObj) { + // Ensure Applications.Modeling is present when saving + jsonObj["Application"] = QString("SSISimulation"); + QJsonObject appData; // reserved for future options + jsonObj["ApplicationData"] = appData; + return true; +} + +bool SSI_Simulation::inputAppDataFromJSON(QJsonObject &jsonObj) { + // Accept legacy or minimal Modeling entries gracefully + Q_UNUSED(jsonObj); + return true; +} + +bool SSI_Simulation::copyFiles(QString &destDir) { + // Delegate to child widgets when possible; copy any referenced files needed to run + bool ok = true; + + // Building: invoke widget's own copyFiles + if (currentBuilding) { + ok = currentBuilding->copyFiles(destDir) && ok; + } + + // Soil/Foundation: invoke widget's own copyFiles + if (currentSoil) { + ok = currentSoil->copyFiles(destDir) && ok; + } + + return ok; +} + +bool SSI_Simulation::validate(QStringList &errors) { + bool valid = currentBuilding ? currentBuilding->validate(errors, true) : false; + if (!valid) + return false; + if (currentSoil) { + QStringList soilErrors; + bool soilValid = currentSoil->validate(soilErrors, true); + if (!soilValid) { + errors.append(soilErrors); + return false; + } + } + return true; +} + +void SSI_Simulation::onValidateClicked() { + + // call the validate function + QStringList errors; + bool valid = true; + valid = this->validate(errors); + + // show the message box + if (!valid) { + // Show the warning dialog 200px down and to the right of this widget + QMessageBox msgBox(QMessageBox::Warning, + tr("Validation"), + errors.join("\n"), + QMessageBox::Ok, + this); + + // Center the message box over the parent widget + msgBox.ensurePolished(); + const QPoint parentCenterGlobal = this->mapToGlobal(this->rect().center()); + const QSize dialogSize = msgBox.sizeHint(); + msgBox.move(parentCenterGlobal - QPoint(dialogSize.width() / 2, dialogSize.height() / 2)); + + msgBox.exec(); + + return; + } + + // Show the information dialog centered over this widget + QMessageBox msgBox(QMessageBox::Information, + tr("Validation"), + tr("Validation passed."), + QMessageBox::Ok, + this); + msgBox.ensurePolished(); + const QPoint parentCenterGlobal = this->mapToGlobal(this->rect().center()); + const QSize dialogSize = msgBox.sizeHint(); + msgBox.move(parentCenterGlobal - QPoint(dialogSize.width() / 2, dialogSize.height() / 2)); + msgBox.exec(); +} + +void SSI_Simulation::onPlotClicked() { + // Build a minimal Modeling JSON and save to file + QJsonObject modeling; + modeling["type"] = "SSISimulation"; + + if (currentBuilding) { + modeling["building_type"] = currentBuilding->typeId(); + QJsonObject structureInfo; + if (currentBuilding->outputToJSON(structureInfo)) { + modeling["structure_info"] = structureInfo; + } + } + + if (currentSoil) { + modeling["soil_type"] = currentSoil->typeId(); + QJsonObject sfi; + if (currentSoil->outputToJSON(sfi)) { + modeling["soil_foundation_info"] = sfi; + } + } + + // Determine working directory for the plotter artifacts + QString workDir = SimCenterPreferences::getInstance()->getLocalWorkDir() + QDir::separator() + "SSI_Plotter"; + QDir dirWork(workDir); + if (!dirWork.exists()) { + dirWork.mkpath("."); + } + + // Write JSON file + const QString jsonPath = workDir + QDir::separator() + "ssi_modeling.json"; + { + QJsonDocument doc(modeling); + QFile out(jsonPath); + if (out.open(QIODevice::WriteOnly)) { + out.write(doc.toJson(QJsonDocument::Indented)); + out.close(); + } else { + QMessageBox::critical(this, "Error", QString("Failed to write modeling JSON to %1").arg(jsonPath)); + return; + } + } + + // Create a small Python runner that loads the JSON and starts the viewer + const QString runnerPath = workDir + QDir::separator() + "ssi_plot_runner.py"; + { + static const char runnerSrc[] = + "import sys, json\n" + "from femora.components.simcenter.eeuq.soil_foundation_type_one_plot import SoilFoundationPlotter\n" + "def main():\n" + " if len(sys.argv) < 2:\n" + " print('Usage: python ssi_plot_runner.py [port]')\n" + " sys.exit(2)\n" + " json_path = sys.argv[1]\n" + " port = int(sys.argv[2]) if len(sys.argv) > 2 else 0\n" + " with open(json_path, 'r') as f:\n" + " data = json.load(f)\n" + " si = data.get('structure_info')\n" + " sfi = data.get('soil_foundation_info', {})\n" + " soi = sfi.get('soil_info')\n" + " fi = sfi.get('foundation_info')\n" + " pi = sfi.get('pile_info')\n" + " plotter = SoilFoundationPlotter(structure_info=si, soil_info=soi, foundation_info=fi, pile_info=pi, port=port, title='SSI Soil/Foundation Plotter')\n" + " plotter.start_server()\n" + "if __name__ == '__main__':\n" + " main()\n"; + + QFile runner(runnerPath); + if (runner.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + runner.write(runnerSrc); + runner.close(); + } else { + QMessageBox::critical(this, "Error", QString("Failed to write Python runner to %1").arg(runnerPath)); + return; + } + } + + // Launch the Python runner using the preferred Python from preferences + QStringList args; + args << jsonPath; // optional port can be appended here + if (plotProcess != nullptr) { + // ensure previous process is stopped before starting a new one + plotProcess->terminateProcess(); + plotProcess = nullptr; + } + plotProcess = new RunPythonInThread(runnerPath, args, workDir); + connect(plotProcess, &RunPythonInThread::processFinished, this, [this](int){ plotProcess = nullptr; }); + plotProcess->runProcess(); +} + + + + +int SSI_Simulation::getNumberOfCores() const { + int numCores = 0; + if (currentBuilding) { + numCores += currentBuilding->getNumberOfCores(); + } + if (currentSoil) { + numCores += currentSoil->getNumberOfCores(); + } + return numCores; +} \ No newline at end of file diff --git a/Workflow/SIM/SSI/SSI_Simulation.h b/Workflow/SIM/SSI/SSI_Simulation.h new file mode 100644 index 000000000..40e4ac514 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_Simulation.h @@ -0,0 +1,61 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#ifndef SSI_SIMULATION_H +#define SSI_SIMULATION_H + +#include +#include "SSI_BuildingWidgetBase.h" +#include "SSI_SoilFoundationBaseWidget.h" + +class QTabWidget; +class QWidget; +class QPushButton; +class QStackedWidget; +class QComboBox; +class RunPythonInThread; + +class SSI_Simulation : public SimCenterAppWidget { + Q_OBJECT +public: + explicit SSI_Simulation(QWidget* parent = nullptr); + ~SSI_Simulation() override; + + bool outputToJSON(QJsonObject &jsonObj) override; + bool inputFromJSON(QJsonObject &jsonObj) override; + bool outputAppDataToJSON(QJsonObject &jsonObj) override; + bool inputAppDataFromJSON(QJsonObject &jsonObj) override; + bool copyFiles(QString &destDir) override; + + +private slots: + void onPlotClicked(); + void onValidateClicked(); + +private: + bool validate(QStringList &errors); + int getNumberOfCores() const; + + QTabWidget* tabWidget {nullptr}; + QWidget* buildingTabContainer {nullptr}; + QStackedWidget* buildingStack {nullptr}; + QComboBox* buildingSelector {nullptr}; + SSI_BuildingWidgetBase* currentBuilding {nullptr}; + QWidget* soilTabContainer {nullptr}; + QStackedWidget* soilStack {nullptr}; + QComboBox* soilSelector {nullptr}; + SSI_SoilFoundationBaseWidget* currentSoil {nullptr}; + + QPushButton* plotButton {nullptr}; + QPushButton* validateButton {nullptr}; + + RunPythonInThread* plotProcess {nullptr}; +}; + +#endif // SSI_SIMULATION_H + + + + diff --git a/Workflow/SIM/SSI/SSI_SoilFoundationBaseWidget.h b/Workflow/SIM/SSI/SSI_SoilFoundationBaseWidget.h new file mode 100644 index 000000000..587444d93 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_SoilFoundationBaseWidget.h @@ -0,0 +1,47 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#ifndef SSI_SOIL_FOUNDATION_BASE_WIDGET_H +#define SSI_SOIL_FOUNDATION_BASE_WIDGET_H + +#include +#include +#include +class QString; + +class SSI_SoilFoundationBaseWidget : public QWidget { + Q_OBJECT +public: + explicit SSI_SoilFoundationBaseWidget(QWidget* parent = nullptr) : QWidget(parent) {} + ~SSI_SoilFoundationBaseWidget() override = default; + + // Unique string identifier for this soil/foundation widget implementation + virtual QString typeId() const = 0; + + // Validate current configuration; interactiveIfModelMissing mirrors building base semantics + virtual bool validate(QStringList& errors, bool interactiveIfModelMissing = false) const = 0; + + // Serialize/deserialize soil & foundation configuration + virtual bool outputToJSON(QJsonObject& soilFoundationInfo) const = 0; + virtual bool inputFromJSON(const QJsonObject& soilFoundationInfo) = 0; + + // Optional visualization hook + virtual void plot() const = 0; + + // Copy any external files required for this soil/foundation configuration into destDir + virtual bool copyFiles(QString &destDir) = 0; + + // Return the list of random variable names used by this soil/foundation widget + virtual QStringList getRandomVariableNames() const = 0; + + virtual int getNumberOfCores() const = 0; +}; + +#endif // SSI_SOIL_FOUNDATION_BASE_WIDGET_H + + + + + diff --git a/Workflow/SIM/SSI/SSI_SoilFoundationType1Widget.cpp b/Workflow/SIM/SSI/SSI_SoilFoundationType1Widget.cpp new file mode 100644 index 000000000..cd2f9d645 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_SoilFoundationType1Widget.cpp @@ -0,0 +1,826 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#include "SSI_SoilFoundationType1Widget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +SSI_SoilFoundationType1Widget::SSI_SoilFoundationType1Widget(QWidget* parent) + : SSI_SoilFoundationBaseWidget(parent) +{ + auto topLayout = new QVBoxLayout(); + + auto tabs = new QTabWidget(); + + auto soilTab = new QWidget(); + setupSoilGroup(soilTab); + tabs->addTab(soilTab, "Soil"); + + auto foundationTab = new QWidget(); + setupFoundationGroup(foundationTab); + tabs->addTab(foundationTab, "Foundation"); + + auto pilesTab = new QWidget(); + setupPilesGroup(pilesTab); + tabs->addTab(pilesTab, "Piles"); + + topLayout->addWidget(tabs); + this->setLayout(topLayout); +} + +void SSI_SoilFoundationType1Widget::setupSoilGroup(QWidget* parentWidget) { + auto hbox = new QHBoxLayout(parentWidget); + auto leftWidget = new QWidget(); + auto vbox = new QVBoxLayout(leftWidget); + + auto grid = new QGridLayout(); + int row = 0; + + // Row 0: x_min | x_max + grid->addWidget(new QLabel("x_min"), row, 0); xMinSoil = new QDoubleSpinBox(); xMinSoil->setRange(-1e9, 1e9); xMinSoil->setDecimals(6); xMinSoil->setValue(-64.0); grid->addWidget(xMinSoil, row, 1); + grid->addWidget(new QLabel("x_max"), row, 2); xMaxSoil = new QDoubleSpinBox(); xMaxSoil->setRange(-1e9, 1e9); xMaxSoil->setDecimals(6); xMaxSoil->setValue(64.0); grid->addWidget(xMaxSoil, row, 3); row++; + + // Row 1: y_min | y_max + grid->addWidget(new QLabel("y_min"), row, 0); yMinSoil = new QDoubleSpinBox(); yMinSoil->setRange(-1e9, 1e9); yMinSoil->setDecimals(6); yMinSoil->setValue(-64.0); grid->addWidget(yMinSoil, row, 1); + grid->addWidget(new QLabel("y_max"), row, 2); yMaxSoil = new QDoubleSpinBox(); yMaxSoil->setRange(-1e9, 1e9); yMaxSoil->setDecimals(6); yMaxSoil->setValue(64.0); grid->addWidget(yMaxSoil, row, 3); row++; + + // Row 2: nx | ny + grid->addWidget(new QLabel("nx"), row, 0); nxSoil = new QSpinBox(); nxSoil->setRange(1, 1000000); nxSoil->setValue(32); grid->addWidget(nxSoil, row, 1); + grid->addWidget(new QLabel("ny"), row, 2); nySoil = new QSpinBox(); nySoil->setRange(1, 1000000); nySoil->setValue(32); grid->addWidget(nySoil, row, 3); row++; + + // Row 3: gravity_z | num_partitions + grid->addWidget(new QLabel("gravity_z"), row, 0); gravZSoil = new QDoubleSpinBox(); gravZSoil->setRange(-1e6, 1e6); gravZSoil->setDecimals(6); gravZSoil->setValue(-9.81); grid->addWidget(gravZSoil, row, 1); + grid->addWidget(new QLabel("num_partitions"), row, 2); numPartsSoil = new QSpinBox(); numPartsSoil->setRange(1, 1000000); numPartsSoil->setValue(4); grid->addWidget(numPartsSoil, row, 3); row++; + + // Row 5: boundary_conditions (label + combo on the left) + grid->addWidget(new QLabel("boundary_conditions"), row, 0); boundaryCombo = new QComboBox(); boundaryCombo->addItem("periodic"); boundaryCombo->addItem("DRM"); grid->addWidget(boundaryCombo, row, 1); row++; + + grid->setColumnStretch(1, 1); + grid->setColumnStretch(3, 1); + vbox->addLayout(grid); + + drmOptionsGroup = new QGroupBox("DRM options"); + auto drmGrid = new QGridLayout(drmOptionsGroup); + int drow = 0; + drmGrid->addWidget(new QLabel("absorbing_layer_type"), drow, 0); absorbingLayerCombo = new QComboBox(); absorbingLayerCombo->addItem("Rayleigh"); absorbingLayerCombo->addItem("PML"); drmGrid->addWidget(absorbingLayerCombo, drow, 1); drow++; + drmGrid->addWidget(new QLabel("num_partitions"), drow, 0); drmNumPartitions = new QSpinBox(); drmNumPartitions->setRange(1, 1000000); drmNumPartitions->setValue(2); drmGrid->addWidget(drmNumPartitions, drow, 1); drow++; + drmGrid->addWidget(new QLabel("number_of_layers"), drow, 0); drmNumLayers = new QSpinBox(); drmNumLayers->setRange(1, 1000000); drmNumLayers->setValue(4); drmGrid->addWidget(drmNumLayers, drow, 1); drow++; + drmGrid->addWidget(new QLabel("Rayleigh_damping"), drow, 0); drmRayleighDamping = new QDoubleSpinBox(); drmRayleighDamping->setRange(0.0, 1.0); drmRayleighDamping->setDecimals(6); drmRayleighDamping->setValue(0.95); drmGrid->addWidget(drmRayleighDamping, drow, 1); drow++; + drmGrid->addWidget(new QLabel("match_damping"), drow, 0); drmMatchDamping = new QCheckBox(); drmMatchDamping->setChecked(false); drmGrid->addWidget(drmMatchDamping, drow, 1); drow++; + drmOptionsGroup->setVisible(false); + vbox->addWidget(drmOptionsGroup); + + connect(boundaryCombo, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int idx){ + Q_UNUSED(idx); + drmOptionsGroup->setVisible(boundaryCombo->currentText() == "DRM"); + }); + + // Soil profile table + soilProfileTable = new QTableWidget(); + soilProfileTable->setColumnCount(8); + QStringList headers; headers << "z_bot" << "z_top" << "nz" << "material" << "mat_props" << "damping" << "damping_props" << ""; + soilProfileTable->setHorizontalHeaderLabels(headers); + soilProfileTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + soilProfileTable->setSelectionBehavior(QAbstractItemView::SelectRows); + soilProfileTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); + vbox->addWidget(soilProfileTable); + + auto soilBtns = new QHBoxLayout(); + auto addRowBtn = new QPushButton("Add row"); + auto removeRowBtn = new QPushButton("Remove"); + auto clearBtn = new QPushButton("Clear"); + soilBtns->addWidget(addRowBtn); + soilBtns->addWidget(removeRowBtn); + soilBtns->addWidget(clearBtn); + soilBtns->addStretch(1); + vbox->addLayout(soilBtns); + + // Help panel on the right (30% width) with rich, colorized content + auto help = new QTextBrowser(); + help->setStyleSheet("QTextBrowser { background: #f8fbff; border: 1px solid #d0e7ff; border-radius: 6px; padding: 8px; }"); + help->setHtml( + "
" + "

Soil parameters

" + "
    " + "
  • x_min, x_max, y_min, y_max: Plan bounds (m)
  • " + "
  • nx, ny: Elements along x and y (≥1)
  • " + "
  • gravity_z: Typically -9.81 m/s²
  • " + "
  • num_partitions: MPI partitions for soil mesh
  • " + "
  • boundary_conditions: periodic or DRM
  • " + "
" + "

DRM options

" + "
    " + "
  • absorbing_layer_type: Rayleigh or PML
  • " + "
  • num_partitions: Partitions in absorbing layer
  • " + "
  • number_of_layers: Count of absorbing layers
  • " + "
  • Rayleigh_damping: Additional damping ratio
  • " + "
  • match_damping: Match soil-layer damping when checked
  • " + "
" + "

Soil profile table

" + "
    " + "
  • z_bot, z_top, nz: z_top > z_bot, vertical subdivisions
  • " + "
  • material: Elastic (3 mat_props: E, ν, ρ)
  • " + "
  • damping: Frequency‑Rayleigh (3 damping_props: ζ, f_low, f_high)
  • " + "
" + "
" + ); + + hbox->addWidget(leftWidget, 7); + hbox->addWidget(help, 3); + + connect(addRowBtn, &QPushButton::clicked, this, [this]() { + int row = soilProfileTable->rowCount(); + soilProfileTable->insertRow(row); + // z_bot/z_top/nz + for (int c = 0; c < 3; ++c) soilProfileTable->setItem(row, c, new QTableWidgetItem()); + // material combo + auto mat = new QComboBox(); mat->addItem("Elastic"); soilProfileTable->setCellWidget(row, 3, mat); + // mat_props + soilProfileTable->setItem(row, 4, new QTableWidgetItem()); + // damping combo + auto damp = new QComboBox(); damp->addItem("Frequency-Rayleigh"); soilProfileTable->setCellWidget(row, 5, damp); + // damping_props + soilProfileTable->setItem(row, 6, new QTableWidgetItem()); + }); + connect(removeRowBtn, &QPushButton::clicked, this, [this]() { + auto ranges = soilProfileTable->selectedRanges(); + if (!ranges.isEmpty()) { + int first = ranges.first().topRow(); + int last = ranges.first().bottomRow(); + for (int r = last; r >= first; --r) soilProfileTable->removeRow(r); + } else if (soilProfileTable->rowCount() > 0) { + soilProfileTable->removeRow(soilProfileTable->rowCount() - 1); + } + }); + connect(clearBtn, &QPushButton::clicked, this, [this]() { soilProfileTable->setRowCount(0); }); + + // auto-register RVs when user edits mat_props + connect(soilProfileTable, &QTableWidget::itemChanged, this, [this](QTableWidgetItem* item){ + if (!item) return; + if (item->column() == 4) { + registerRVsFromCsv(item->text()); + } + }); +} + +void SSI_SoilFoundationType1Widget::setupFoundationGroup(QWidget* parentWidget) { + auto hbox = new QHBoxLayout(parentWidget); + auto leftWidget = new QWidget(); + auto vbox = new QVBoxLayout(leftWidget); + + // Top compact grid: three columns layout + auto grid = new QGridLayout(); + int row = 0; + // Row 0: dx | dy | dz + grid->addWidget(new QLabel("dx"), row, 0); dxFound = new QDoubleSpinBox(); dxFound->setRange(1e-9, 1e9); dxFound->setDecimals(6); dxFound->setValue(2.0); grid->addWidget(dxFound, row, 1); + grid->addWidget(new QLabel("dy"), row, 2); dyFound = new QDoubleSpinBox(); dyFound->setRange(1e-9, 1e9); dyFound->setDecimals(6); dyFound->setValue(2.0); grid->addWidget(dyFound, row, 3); + grid->addWidget(new QLabel("dz"), row, 4); dzFound = new QDoubleSpinBox(); dzFound->setRange(1e-9, 1e9); dzFound->setDecimals(6); dzFound->setValue(0.3); grid->addWidget(dzFound, row, 5); row++; + // Row 1: gravity_z | num_partitions | embedded + grid->addWidget(new QLabel("gravity_z"), row, 0); gravZFound = new QDoubleSpinBox(); gravZFound->setRange(-1e6, 1e6); gravZFound->setDecimals(6); gravZFound->setValue(-9.81); grid->addWidget(gravZFound, row, 1); + grid->addWidget(new QLabel("num_partitions"), row, 2); numPartsFound = new QSpinBox(); numPartsFound->setRange(1, 1000000); numPartsFound->setValue(1); grid->addWidget(numPartsFound, row, 3); + grid->addWidget(new QLabel("embedded"), row, 4); embeddedFoundation = new QCheckBox(); embeddedFoundation->setChecked(true); grid->addWidget(embeddedFoundation, row, 5); row++; + + grid->setColumnStretch(1, 1); + grid->setColumnStretch(3, 1); + grid->setColumnStretch(5, 1); + vbox->addLayout(grid); + + // Building–foundation connection box for embedment depth and section props + auto connBox = new QGroupBox("Building foundation connection"); + auto connVBox = new QVBoxLayout(connBox); + // Toggle for advanced section properties + auto toggleLayout = new QHBoxLayout(); + toggleLayout->addWidget(new QLabel("Advanced section properties:")); + showSectionPropsCheck = new QCheckBox("Show"); + showSectionPropsCheck->setChecked(false); + toggleLayout->addWidget(showSectionPropsCheck); + toggleLayout->addStretch(1); + connVBox->addLayout(toggleLayout); + + // Embedment depth always visible + auto embedLayout = new QHBoxLayout(); + embedLayout->addWidget(new QLabel("column_embedment_depth")); + columnEmbedDepth = new QDoubleSpinBox(); columnEmbedDepth->setRange(0.0, 1e9); columnEmbedDepth->setDecimals(6); columnEmbedDepth->setValue(0.6); + embedLayout->addWidget(columnEmbedDepth); + embedLayout->addStretch(1); + connVBox->addLayout(embedLayout); + + // Container for advanced props; hidden by default + sectionPropsContainer = new QWidget(); + auto connGrid = new QGridLayout(sectionPropsContainer); + int crow = 0; + // Section properties in three columns: E A G + connGrid->addWidget(new QLabel("Section E"), crow, 0); secE = new QDoubleSpinBox(); secE->setRange(0.0, 1e12); secE->setDecimals(6); secE->setValue(30e6); connGrid->addWidget(secE, crow, 1); + connGrid->addWidget(new QLabel("Section A"), crow, 2); secA = new QDoubleSpinBox(); secA->setRange(0.0, 1e9); secA->setDecimals(6); secA->setValue(0.282); connGrid->addWidget(secA, crow, 3); + connGrid->addWidget(new QLabel("Section G"), crow, 4); secG = new QDoubleSpinBox(); secG->setRange(0.0, 1e12); secG->setDecimals(6); secG->setValue(12.5e6); connGrid->addWidget(secG, crow, 5); crow++; + // Next row: Iy Iz J + connGrid->addWidget(new QLabel("Section Iy"), crow, 0); secIy = new QDoubleSpinBox(); secIy->setRange(0.0, 1e9); secIy->setDecimals(7); secIy->setValue(0.0063585); connGrid->addWidget(secIy, crow, 1); + connGrid->addWidget(new QLabel("Section Iz"), crow, 2); secIz = new QDoubleSpinBox(); secIz->setRange(0.0, 1e9); secIz->setDecimals(7); secIz->setValue(0.0063585); connGrid->addWidget(secIz, crow, 3); + connGrid->addWidget(new QLabel("Section J"), crow, 4); secJ = new QDoubleSpinBox(); secJ->setRange(0.0, 1e9); secJ->setDecimals(6); secJ->setValue(0.012717); connGrid->addWidget(secJ, crow, 5); crow++; + connGrid->setColumnStretch(1, 1); + connGrid->setColumnStretch(3, 1); + connGrid->setColumnStretch(5, 1); + sectionPropsContainer->setVisible(false); + connVBox->addWidget(sectionPropsContainer); + vbox->addWidget(connBox); + + connect(showSectionPropsCheck, &QCheckBox::toggled, this, [this](bool checked){ + sectionPropsContainer->setVisible(checked); + }); + + // Foundation profile table + foundationProfileTable = new QTableWidget(); + foundationProfileTable->setColumnCount(10); + QStringList headers; headers << "x_min" << "x_max" << "y_min" << "y_max" << "z_top" << "z_bot" << "material" << "mat_props" << "damping" << "damping_props"; + foundationProfileTable->setHorizontalHeaderLabels(headers); + foundationProfileTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + foundationProfileTable->setSelectionBehavior(QAbstractItemView::SelectRows); + foundationProfileTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); + vbox->addWidget(foundationProfileTable); + + auto fBtns = new QHBoxLayout(); + auto addRowBtn = new QPushButton("Add row"); + auto removeRowBtn = new QPushButton("Remove"); + auto clearBtn = new QPushButton("Clear"); + fBtns->addWidget(addRowBtn); + fBtns->addWidget(removeRowBtn); + fBtns->addWidget(clearBtn); + fBtns->addStretch(1); + vbox->addLayout(fBtns); + + // Help panel on the right (30% width) with rich, colorized content + auto help = new QTextBrowser(); + help->setStyleSheet("QTextBrowser { background: #fff8e1; border: 1px solid #ffe0b2; border-radius: 6px; padding: 8px; }"); + help->setHtml( + "
" + "

Foundation parameters

" + "
    " + "
  • dx, dy, dz: Mesh sizes of the foundation block (m)
  • " + "
  • gravity_z: Typically -9.81 m/s²
  • " + "
  • num_partitions: MPI partitions for foundation
  • " + "
  • embedded: If checked, foundation is embedded in soil
  • " + "
  • column_embedment_depth: Embedment depth of building columns (m)
  • " + "
" + "

Advanced section properties

" + "

Set E, A, G, Iy, Iz, J for column sections.

" + "

Foundation profile table

" + "
    " + "
  • x/y bounds, z_top, z_bot: Define foundation extent
  • " + "
  • material: Elastic (3 mat_props: E, ν, ρ)
  • " + "
  • damping: Frequency‑Rayleigh (3 damping_props: ζ, f_low, f_high)
  • " + "
" + "
" + ); + + hbox->addWidget(leftWidget, 7); + hbox->addWidget(help, 3); + + connect(addRowBtn, &QPushButton::clicked, this, [this]() { + int row = foundationProfileTable->rowCount(); + foundationProfileTable->insertRow(row); + for (int c = 0; c < 6; ++c) foundationProfileTable->setItem(row, c, new QTableWidgetItem()); + auto mat = new QComboBox(); mat->addItem("Elastic"); foundationProfileTable->setCellWidget(row, 6, mat); + foundationProfileTable->setItem(row, 7, new QTableWidgetItem()); + auto damp = new QComboBox(); damp->addItem("Frequency-Rayleigh"); foundationProfileTable->setCellWidget(row, 8, damp); + foundationProfileTable->setItem(row, 9, new QTableWidgetItem()); + }); + connect(removeRowBtn, &QPushButton::clicked, this, [this]() { + auto ranges = foundationProfileTable->selectedRanges(); + if (!ranges.isEmpty()) { + int first = ranges.first().topRow(); + int last = ranges.first().bottomRow(); + for (int r = last; r >= first; --r) foundationProfileTable->removeRow(r); + } else if (foundationProfileTable->rowCount() > 0) { + foundationProfileTable->removeRow(foundationProfileTable->rowCount() - 1); + } + }); + connect(clearBtn, &QPushButton::clicked, this, [this]() { foundationProfileTable->setRowCount(0); }); + + // auto-register RVs when user edits mat_props + connect(foundationProfileTable, &QTableWidget::itemChanged, this, [this](QTableWidgetItem* item){ + if (!item) return; + if (item->column() == 7) { + registerRVsFromCsv(item->text()); + } + }); +} + +void SSI_SoilFoundationType1Widget::setupPilesGroup(QWidget* parentWidget) { + auto hbox = new QHBoxLayout(parentWidget); + auto leftWidget = new QWidget(); + auto vbox = new QVBoxLayout(leftWidget); + + pileProfileTable = new QTableWidget(); + pileProfileTable->setColumnCount(11); + QStringList headers; headers << "type" << "params" << "z_top" << "z_bot" << "nz" << "r" << "section" << "material" << "mat_props" << "transformation" << "transformation_vector"; + pileProfileTable->setHorizontalHeaderLabels(headers); + pileProfileTable->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + pileProfileTable->setSelectionBehavior(QAbstractItemView::SelectRows); + pileProfileTable->setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::SelectedClicked | QAbstractItemView::EditKeyPressed); + vbox->addWidget(pileProfileTable); + + auto pGrid = new QGridLayout(); + int row = 0; + pGrid->addWidget(new QLabel("num_points_on_perimeter"), row, 0); pileNumPointsPerimeter = new QSpinBox(); pileNumPointsPerimeter->setRange(1, 10000); pileNumPointsPerimeter->setValue(8); pGrid->addWidget(pileNumPointsPerimeter, row, 1); row++; + pGrid->addWidget(new QLabel("num_points_along_length"), row, 0); pileNumPointsLength = new QSpinBox(); pileNumPointsLength->setRange(1, 10000); pileNumPointsLength->setValue(4); pGrid->addWidget(pileNumPointsLength, row, 1); row++; + pGrid->addWidget(new QLabel("penalty_parameter"), row, 0); pilePenaltyParameter = new QDoubleSpinBox(); pilePenaltyParameter->setRange(0.0, 1e20); pilePenaltyParameter->setDecimals(6); pilePenaltyParameter->setValue(1.0e12); pGrid->addWidget(pilePenaltyParameter, row, 1); row++; + pGrid->setColumnStretch(1, 1); + vbox->addLayout(pGrid); + + auto pBtns = new QHBoxLayout(); + auto addRowBtn = new QPushButton("Add row"); + auto removeRowBtn = new QPushButton("Remove"); + auto clearBtn = new QPushButton("Clear"); + pBtns->addWidget(addRowBtn); + pBtns->addWidget(removeRowBtn); + pBtns->addWidget(clearBtn); + pBtns->addStretch(1); + vbox->addLayout(pBtns); + + // Help panel on the right (30% width) with rich, colorized content + auto help = new QTextBrowser(); + help->setStyleSheet("QTextBrowser { background: #f3e5f5; border: 1px solid #e1bee7; border-radius: 6px; padding: 8px; }"); + help->setHtml( + "
" + "

Pile parameters

" + "
    " + "
  • type: Grid or Single
  • " + "
  • Grid params (csv): x_start, y_start, spacing_x, spacing_y, nx, ny
  • " + "
  • Single params (csv): x_top, x_bot, y_top, y_bot
  • " + "
  • z_top, z_bot, nz, r: Vertical geometry and pile radius
  • " + "
  • section: Only No‑Section currently supported
  • " + "
  • material: Elastic with 6 mat_props: E, A, Iy, Iz, G, J
  • " + "
  • transformation: Linear or PDelta
  • " + "
  • transformation_vector: csv of ux, uy, uz (e.g., 0.0, 1.0, 0.0)
  • " + "
" + "

Interface discretization

" + "
    " + "
  • num_points_on_perimeter, num_points_along_length, penalty_parameter
  • " + "
" + "
" + ); + + hbox->addWidget(leftWidget, 7); + hbox->addWidget(help, 3); + + connect(addRowBtn, &QPushButton::clicked, this, [this]() { + int row = pileProfileTable->rowCount(); + pileProfileTable->insertRow(row); + // type combo + auto type = new QComboBox(); type->addItem("Grid"); type->addItem("Single"); pileProfileTable->setCellWidget(row, 0, type); + // params (csv depending on type) + pileProfileTable->setItem(row, 1, new QTableWidgetItem()); + // z_top, z_bot, nz, r + for (int c = 2; c <= 5; ++c) pileProfileTable->setItem(row, c, new QTableWidgetItem()); + // section combo + auto section = new QComboBox(); section->addItem("No-Section"); pileProfileTable->setCellWidget(row, 6, section); + // material combo + auto mat = new QComboBox(); mat->addItem("Elastic"); pileProfileTable->setCellWidget(row, 7, mat); + // mat props + pileProfileTable->setItem(row, 8, new QTableWidgetItem()); + // transformation combo + vector (csv) + auto trans = new QComboBox(); trans->addItem("Linear"); trans->addItem("PDelta"); pileProfileTable->setCellWidget(row, 9, trans); + pileProfileTable->setItem(row, 10, new QTableWidgetItem("0.0, 1.0, 0.0")); + }); + connect(removeRowBtn, &QPushButton::clicked, this, [this]() { + auto ranges = pileProfileTable->selectedRanges(); + if (!ranges.isEmpty()) { + int first = ranges.first().topRow(); + int last = ranges.first().bottomRow(); + for (int r = last; r >= first; --r) pileProfileTable->removeRow(r); + } else if (pileProfileTable->rowCount() > 0) { + pileProfileTable->removeRow(pileProfileTable->rowCount() - 1); + } + }); + connect(clearBtn, &QPushButton::clicked, this, [this]() { pileProfileTable->setRowCount(0); }); + + // auto-register RVs when user edits mat_props + connect(pileProfileTable, &QTableWidget::itemChanged, this, [this](QTableWidgetItem* item){ + if (!item) return; + if (item->column() == 8) { + registerRVsFromCsv(item->text()); + } + }); +} + +QList SSI_SoilFoundationType1Widget::parseCsvDoubles(const QString& text, bool* ok) { + QList values; + *ok = true; + const auto parts = text.split(',', Qt::SkipEmptyParts); + for (const auto& p : parts) { + bool localOk = false; + double v = p.trimmed().toDouble(&localOk); + if (!localOk) { *ok = false; return {}; } + values.append(v); + } + return values; +} + +static bool isNumberToken(const QString &s) { + bool ok=false; s.toDouble(&ok); return ok; +} + +static bool isRvToken(const QString &s) { + return s.startsWith("RV.") && s.size() > 3; +} + +QStringList SSI_SoilFoundationType1Widget::parseCsvTokens(const QString& text, bool* ok) { + QStringList tokens; + *ok = true; + const auto parts = text.split(',', Qt::SkipEmptyParts); + for (const auto &raw : parts) { + const QString p = raw.trimmed(); + if (p.isEmpty()) { *ok = false; return {}; } + if (isNumberToken(p) || isRvToken(p)) { + tokens.append(p); + } else { + // accept bare word as RV.name + if (!p.contains(' ')) tokens.append(QString("RV.") + p); + else { *ok = false; return {}; } + } + } + return tokens; +} + +void SSI_SoilFoundationType1Widget::registerRVsFromCsv(const QString& text) const { + bool ok=true; const QStringList tokens = parseCsvTokens(text, &ok); + if (!ok) return; + RandomVariablesContainer *rvc = RandomVariablesContainer::getInstance(); + for (const QString &t : tokens) { + if (isRvToken(t)) { + QString name = t.mid(3); + rvc->addRandomVariable(name); + } + } +} + +bool SSI_SoilFoundationType1Widget::validate(QStringList& errors, bool interactiveIfModelMissing) const { + Q_UNUSED(interactiveIfModelMissing); + errors.clear(); + + if (!(xMinSoil->value() < xMaxSoil->value())) errors << "soil: x_min must be < x_max"; + if (!(yMinSoil->value() < yMaxSoil->value())) errors << "soil: y_min must be < y_max"; + if (nxSoil->value() < 1) errors << "soil: nx must be >= 1"; + if (nySoil->value() < 1) errors << "soil: ny must be >= 1"; + + // Soil profile rows check + if (soilProfileTable->rowCount() == 0) errors << "soil_profile: table is empty"; + for (int r = 0; r < soilProfileTable->rowCount(); ++r) { + // z_bot, z_top, nz + bool ok1=false, ok2=false; int nz = soilProfileTable->item(r,2) ? soilProfileTable->item(r,2)->text().toInt(&ok1) : 0; + double zbot = soilProfileTable->item(r,0) ? soilProfileTable->item(r,0)->text().toDouble(&ok2) : 0.0; + bool ok3=false; double ztop = soilProfileTable->item(r,1) ? soilProfileTable->item(r,1)->text().toDouble(&ok3) : 0.0; + if (!(ok1 && ok2 && ok3) || nz < 1 || !(ztop > zbot)) { + errors << QString("soil_profile row %1: invalid z_bot/z_top/nz").arg(r+1); + } + // material + auto mat = qobject_cast(soilProfileTable->cellWidget(r,3)); + if (!mat || mat->currentText() != "Elastic") errors << QString("soil_profile row %1: material must be Elastic").arg(r+1); + // mat props: 3 tokens (numbers or RV names) + bool okcsv=false; const QString mp = soilProfileTable->item(r,4)?soilProfileTable->item(r,4)->text():QString(); + auto mtoks = parseCsvTokens(mp, &okcsv); + if (!okcsv || mtoks.size() != 3) errors << QString("soil_profile row %1: mat_props must have 3 values (numbers or RV names)").arg(r+1); + // damping: Frequency-Rayleigh with 3 props + auto damp = qobject_cast(soilProfileTable->cellWidget(r,5)); + if (!damp || damp->currentText() != "Frequency-Rayleigh") errors << QString("soil_profile row %1: damping must be Frequency-Rayleigh").arg(r+1); + const QString dp = soilProfileTable->item(r,6)?soilProfileTable->item(r,6)->text():QString(); + auto dvals = parseCsvDoubles(dp, &okcsv); + if (!okcsv || dvals.size() != 3) errors << QString("soil_profile row %1: damping_props must have 3 values").arg(r+1); + } + + if (boundaryCombo->currentText() == "DRM") { + if (drmNumPartitions->value() < 1) errors << "DRM: num_partitions must be >= 1"; + if (drmNumLayers->value() < 1) errors << "DRM: number_of_layers must be >= 1"; + if (absorbingLayerCombo->currentText() != "Rayleigh" && absorbingLayerCombo->currentText() != "PML") + errors << "DRM: absorbing_layer_type must be Rayleigh or PML"; + } + + // Foundation profile + if (foundationProfileTable->rowCount() == 0) errors << "foundation_profile: table is empty"; + for (int r = 0; r < foundationProfileTable->rowCount(); ++r) { + bool ok = true; for (int c = 0; c < 6; ++c) { if (!foundationProfileTable->item(r,c) || foundationProfileTable->item(r,c)->text().isEmpty()) ok = false; } + if (!ok) errors << QString("foundation_profile row %1: missing bounds or z values").arg(r+1); + auto mat = qobject_cast(foundationProfileTable->cellWidget(r,6)); if (!mat || mat->currentText() != "Elastic") errors << QString("foundation_profile row %1: material must be Elastic").arg(r+1); + bool okcsv=false; auto mtoksF = parseCsvTokens(foundationProfileTable->item(r,7)?foundationProfileTable->item(r,7)->text():QString(), &okcsv); + if (!okcsv || mtoksF.size() != 3) errors << QString("foundation_profile row %1: mat_props must have 3 values (numbers or RV names)").arg(r+1); + auto damp = qobject_cast(foundationProfileTable->cellWidget(r,8)); if (!damp || damp->currentText() != "Frequency-Rayleigh") errors << QString("foundation_profile row %1: damping must be Frequency-Rayleigh").arg(r+1); + auto dvals = parseCsvDoubles(foundationProfileTable->item(r,9)?foundationProfileTable->item(r,9)->text():QString(), &okcsv); + if (!okcsv || dvals.size() != 3) errors << QString("foundation_profile row %1: damping_props must have 3 values").arg(r+1); + } + + // Column section props for columns embedded in foundation + if (!(secE->value() > 0 && secA->value() > 0 && secIy->value() > 0 && secIz->value() > 0 && secG->value() > 0 && secJ->value() > 0)) + errors << "foundation: all column_section_props must be > 0"; + + // Piles + for (int r = 0; r < pileProfileTable->rowCount(); ++r) { + auto type = qobject_cast(pileProfileTable->cellWidget(r,0)); + if (!type) { errors << QString("pile_profile row %1: missing type").arg(r+1); continue; } + const QString t = type->currentText().toLower(); + bool okcsv=false; auto params = pileProfileTable->item(r,1)?pileProfileTable->item(r,1)->text():QString(); + auto pvals = parseCsvDoubles(params, &okcsv); + if (t == "grid") { + if (!okcsv || pvals.size() != 6) errors << QString("pile_profile row %1: grid params need 6 values (x_start,y_start,spacing_x,spacing_y,nx,ny)").arg(r+1); + } else if (t == "single") { + if (!okcsv || pvals.size() != 4) errors << QString("pile_profile row %1: single params need 4 values (x_top,x_bot,y_top,y_bot)").arg(r+1); + } else { + errors << QString("pile_profile row %1: type must be Grid or Single").arg(r+1); + } + // z_top, z_bot, nz, r + bool okz=false, okzb=false, oknz=false, okr=false; + double zt = pileProfileTable->item(r,2)?pileProfileTable->item(r,2)->text().toDouble(&okz):0.0; + double zb = pileProfileTable->item(r,3)?pileProfileTable->item(r,3)->text().toDouble(&okzb):0.0; + int nz = pileProfileTable->item(r,4)?pileProfileTable->item(r,4)->text().toInt(&oknz):0; + double rr = pileProfileTable->item(r,5)?pileProfileTable->item(r,5)->text().toDouble(&okr):0.0; + if (!(okz && okzb && oknz && okr) || !(zt > zb) || nz < 1 || rr <= 0.0) errors << QString("pile_profile row %1: invalid z_top/z_bot/nz/r").arg(r+1); + // section + auto section = qobject_cast(pileProfileTable->cellWidget(r,6)); if (!section || section->currentText() != "No-Section") errors << QString("pile_profile row %1: section must be No-Section").arg(r+1); + // material elastic with 6 props + auto mat = qobject_cast(pileProfileTable->cellWidget(r,7)); if (!mat || mat->currentText() != "Elastic") errors << QString("pile_profile row %1: material must be Elastic").arg(r+1); + bool okmp=false; auto mtoksP = parseCsvTokens(pileProfileTable->item(r,8)?pileProfileTable->item(r,8)->text():QString(), &okmp); + if (!okmp || mtoksP.size() != 6) errors << QString("pile_profile row %1: mat_props must have 6 values (numbers or RV names for E,A,Iy,Iz,G,J)").arg(r+1); + // transformation combo + vector csv + auto trans = qobject_cast(pileProfileTable->cellWidget(r,9)); if (!trans) errors << QString("pile_profile row %1: missing transformation").arg(r+1); + okcsv = false; auto tvals = parseCsvDoubles(pileProfileTable->item(r,10)?pileProfileTable->item(r,10)->text():QString(), &okcsv); + if (!okcsv || tvals.size() != 3) errors << QString("pile_profile row %1: transformation_vector must have 3 values (ux,uy,uz)").arg(r+1); + } + + return errors.isEmpty(); +} + +bool SSI_SoilFoundationType1Widget::outputToJSON(QJsonObject& soilFoundationInfo) const { + QJsonObject soilInfo; + soilInfo["x_min"] = xMinSoil->value(); soilInfo["x_max"] = xMaxSoil->value(); + soilInfo["y_min"] = yMinSoil->value(); soilInfo["y_max"] = yMaxSoil->value(); + soilInfo["nx"] = nxSoil->value(); soilInfo["ny"] = nySoil->value(); + soilInfo["gravity_x"] = 0.0; soilInfo["gravity_y"] = 0.0; soilInfo["gravity_z"] = gravZSoil->value(); + soilInfo["num_partitions"] = numPartsSoil->value(); + soilInfo["boundary_conditions"] = boundaryCombo->currentText(); + if (boundaryCombo->currentText() == "DRM") { + QJsonObject drm; + drm["absorbing_layer_type"] = absorbingLayerCombo->currentText(); + drm["num_partitions"] = drmNumPartitions->value(); + drm["number_of_layers"] = drmNumLayers->value(); + drm["Rayleigh_damping"] = drmRayleighDamping->value(); + drm["match_damping"] = drmMatchDamping->isChecked(); + soilInfo["DRM_options"] = drm; + } + + QJsonArray soilProfile; + for (int r = 0; r < soilProfileTable->rowCount(); ++r) { + QJsonObject layer; + if (soilProfileTable->item(r,0)) layer["z_bot"] = soilProfileTable->item(r,0)->text().toDouble(); + if (soilProfileTable->item(r,1)) layer["z_top"] = soilProfileTable->item(r,1)->text().toDouble(); + if (soilProfileTable->item(r,2)) layer["nz"] = soilProfileTable->item(r,2)->text().toInt(); + auto mat = qobject_cast(soilProfileTable->cellWidget(r,3)); if (mat) layer["material"] = mat->currentText(); + bool ok=false; const QString mpTxt = soilProfileTable->item(r,4)?soilProfileTable->item(r,4)->text():QString(); + auto mtoks = parseCsvTokens(mpTxt, &ok); + QJsonArray mp; for (const QString &t : mtoks) { if (t.startsWith("RV.")) mp.append(t); else { bool lk=false; double v=t.toDouble(&lk); mp.append(lk?QJsonValue(v):QJsonValue()); } } layer["mat_props"] = mp; + auto damp = qobject_cast(soilProfileTable->cellWidget(r,5)); if (damp) layer["damping"] = damp->currentText(); + auto dvals = parseCsvDoubles(soilProfileTable->item(r,6)?soilProfileTable->item(r,6)->text():QString(), &ok); QJsonArray dp; for (double v : dvals) dp.append(v); layer["damping_props"] = dp; + soilProfile.append(layer); + } + soilInfo["soil_profile"] = soilProfile; + + QJsonObject foundationInfo; + foundationInfo["gravity_x"] = 0.0; foundationInfo["gravity_y"] = 0.0; foundationInfo["gravity_z"] = gravZFound->value(); + foundationInfo["embedded"] = embeddedFoundation->isChecked(); + foundationInfo["dx"] = dxFound->value(); foundationInfo["dy"] = dyFound->value(); foundationInfo["dz"] = dzFound->value(); + foundationInfo["num_partitions"] = numPartsFound->value(); + foundationInfo["column_embedment_depth"] = columnEmbedDepth->value(); + { + QJsonObject sec; sec["E"] = secE->value(); sec["A"] = secA->value(); sec["Iy"] = secIy->value(); sec["Iz"] = secIz->value(); sec["G"] = secG->value(); sec["J"] = secJ->value(); + foundationInfo["column_section_props"] = sec; + } + QJsonArray foundationProfile; + for (int r = 0; r < foundationProfileTable->rowCount(); ++r) { + QJsonObject layer; + if (foundationProfileTable->item(r,0)) layer["x_min"] = foundationProfileTable->item(r,0)->text().toDouble(); + if (foundationProfileTable->item(r,1)) layer["x_max"] = foundationProfileTable->item(r,1)->text().toDouble(); + if (foundationProfileTable->item(r,2)) layer["y_min"] = foundationProfileTable->item(r,2)->text().toDouble(); + if (foundationProfileTable->item(r,3)) layer["y_max"] = foundationProfileTable->item(r,3)->text().toDouble(); + if (foundationProfileTable->item(r,4)) layer["z_top"] = foundationProfileTable->item(r,4)->text().toDouble(); + if (foundationProfileTable->item(r,5)) layer["z_bot"] = foundationProfileTable->item(r,5)->text().toDouble(); + auto mat = qobject_cast(foundationProfileTable->cellWidget(r,6)); if (mat) layer["material"] = mat->currentText(); + bool ok=false; auto mtoks = parseCsvTokens(foundationProfileTable->item(r,7)?foundationProfileTable->item(r,7)->text():QString(), &ok); QJsonArray mp; for (const QString &t : mtoks) { if (t.startsWith("RV.")) mp.append(t); else { bool lk=false; double v=t.toDouble(&lk); mp.append(lk?QJsonValue(v):QJsonValue()); } } layer["mat_props"] = mp; + auto damp = qobject_cast(foundationProfileTable->cellWidget(r,8)); if (damp) layer["damping"] = damp->currentText(); + auto dvals = parseCsvDoubles(foundationProfileTable->item(r,9)?foundationProfileTable->item(r,9)->text():QString(), &ok); QJsonArray dp; for (double v : dvals) dp.append(v); layer["damping_props"] = dp; + foundationProfile.append(layer); + } + foundationInfo["foundation_profile"] = foundationProfile; + + QJsonObject pileInfo; + QJsonArray pileProfile; + for (int r = 0; r < pileProfileTable->rowCount(); ++r) { + QJsonObject item; + auto type = qobject_cast(pileProfileTable->cellWidget(r,0)); if (type) item["type"] = type->currentText().toLower(); + bool ok=false; auto pvals = parseCsvDoubles(pileProfileTable->item(r,1)?pileProfileTable->item(r,1)->text():QString(), &ok); + if (type && type->currentText().toLower() == "grid" && pvals.size() == 6) { + item["x_start"] = pvals[0]; item["y_start"] = pvals[1]; item["spacing_x"] = pvals[2]; item["spacing_y"] = pvals[3]; item["nx"] = (int)pvals[4]; item["ny"] = (int)pvals[5]; + } else if (type && type->currentText().toLower() == "single" && pvals.size() == 4) { + item["x_top"] = pvals[0]; item["x_bot"] = pvals[1]; item["y_top"] = pvals[2]; item["y_bot"] = pvals[3]; + } + if (pileProfileTable->item(r,2)) item["z_top"] = pileProfileTable->item(r,2)->text().toDouble(); + if (pileProfileTable->item(r,3)) item["z_bot"] = pileProfileTable->item(r,3)->text().toDouble(); + if (pileProfileTable->item(r,4)) item["nz"] = pileProfileTable->item(r,4)->text().toInt(); + if (pileProfileTable->item(r,5)) item["r"] = pileProfileTable->item(r,5)->text().toDouble(); + auto section = qobject_cast(pileProfileTable->cellWidget(r,6)); if (section) item["section"] = section->currentText(); + auto mat = qobject_cast(pileProfileTable->cellWidget(r,7)); if (mat) item["material"] = mat->currentText(); + auto mtoksP = parseCsvTokens(pileProfileTable->item(r,8)?pileProfileTable->item(r,8)->text():QString(), &ok); QJsonArray mp; for (const QString &t : mtoksP) { if (t.startsWith("RV.")) mp.append(t); else { bool lk=false; double v=t.toDouble(&lk); mp.append(lk?QJsonValue(v):QJsonValue()); } } item["mat_props"] = mp; + auto trans = qobject_cast(pileProfileTable->cellWidget(r,9)); + if (trans) { + bool okcsv=false; auto tvals = parseCsvDoubles(pileProfileTable->item(r,10)?pileProfileTable->item(r,10)->text():QString(), &okcsv); + double ux = (tvals.size() > 0) ? tvals[0] : 0.0; + double uy = (tvals.size() > 1) ? tvals[1] : 1.0; + double uz = (tvals.size() > 2) ? tvals[2] : 0.0; + QJsonArray tarr; tarr.append(trans->currentText()); tarr.append(ux); tarr.append(uy); tarr.append(uz); + item["transformation"] = tarr; + } + pileProfile.append(item); + } + pileInfo["pile_profile"] = pileProfile; + { + QJsonObject iface; iface["num_points_on_perimeter"] = pileNumPointsPerimeter->value(); iface["num_points_along_length"] = pileNumPointsLength->value(); iface["penalty_parameter"] = pilePenaltyParameter->value(); + pileInfo["pile_interface"] = iface; + } + + soilFoundationInfo["soil_info"] = soilInfo; + soilFoundationInfo["foundation_info"] = foundationInfo; + soilFoundationInfo["pile_info"] = pileInfo; + + // Add random variables found in soil & foundation mat_props only under soilFoundationInfo + { + QSet names; + // soil mat_props (col 4) + for (int r = 0; r < soilProfileTable->rowCount(); ++r) { + auto it = soilProfileTable->item(r,4); + if (!it) continue; + bool okTok=true; const QStringList toks = parseCsvTokens(it->text(), &okTok); + if (!okTok) continue; + for (const QString &t : toks) if (t.startsWith("RV.")) names.insert(t.mid(3)); + } + // foundation mat_props (col 7) + for (int r = 0; r < foundationProfileTable->rowCount(); ++r) { + auto it = foundationProfileTable->item(r,7); + if (!it) continue; + bool okTok=true; const QStringList toks = parseCsvTokens(it->text(), &okTok); + if (!okTok) continue; + for (const QString &t : toks) if (t.startsWith("RV.")) names.insert(t.mid(3)); + } + if (!names.isEmpty()) { + QJsonArray rvArray; + for (const QString &n : names) { + QJsonObject o; o["name"] = n; o["value"] = QString("RV.") + n; rvArray.append(o); + } + soilFoundationInfo["randomVar"] = rvArray; + } + } + return true; +} + +bool SSI_SoilFoundationType1Widget::inputFromJSON(const QJsonObject& soilFoundationInfo) { + // Soil + const auto soilInfo = soilFoundationInfo.value("soil_info").toObject(); + xMinSoil->setValue(soilInfo.value("x_min").toDouble(xMinSoil->value())); + xMaxSoil->setValue(soilInfo.value("x_max").toDouble(xMaxSoil->value())); + yMinSoil->setValue(soilInfo.value("y_min").toDouble(yMinSoil->value())); + yMaxSoil->setValue(soilInfo.value("y_max").toDouble(yMaxSoil->value())); + nxSoil->setValue(soilInfo.value("nx").toInt(nxSoil->value())); + nySoil->setValue(soilInfo.value("ny").toInt(nySoil->value())); + // gravity_x and gravity_y are fixed to 0.0 in the UI + gravZSoil->setValue(soilInfo.value("gravity_z").toDouble(gravZSoil->value())); + numPartsSoil->setValue(soilInfo.value("num_partitions").toInt(numPartsSoil->value())); + const auto bc = soilInfo.value("boundary_conditions").toString(boundaryCombo->currentText()); + int bcIdx = boundaryCombo->findText(bc); if (bcIdx >= 0) boundaryCombo->setCurrentIndex(bcIdx); + if (bc == "DRM") { + drmOptionsGroup->setVisible(true); + const auto drm = soilInfo.value("DRM_options").toObject(); + int ablIdx = absorbingLayerCombo->findText(drm.value("absorbing_layer_type").toString(absorbingLayerCombo->currentText())); if (ablIdx >= 0) absorbingLayerCombo->setCurrentIndex(ablIdx); + drmNumPartitions->setValue(drm.value("num_partitions").toInt(drmNumPartitions->value())); + drmNumLayers->setValue(drm.value("number_of_layers").toInt(drmNumLayers->value())); + drmRayleighDamping->setValue(drm.value("Rayleigh_damping").toDouble(drmRayleighDamping->value())); + drmMatchDamping->setChecked(drm.value("match_damping").toBool(drmMatchDamping->isChecked())); + } else { + drmOptionsGroup->setVisible(false); + } + soilProfileTable->setRowCount(0); + for (const auto& v : soilInfo.value("soil_profile").toArray()) { + const auto obj = v.toObject(); + int row = soilProfileTable->rowCount(); soilProfileTable->insertRow(row); + soilProfileTable->setItem(row, 0, new QTableWidgetItem(QString::number(obj.value("z_bot").toDouble()))); + soilProfileTable->setItem(row, 1, new QTableWidgetItem(QString::number(obj.value("z_top").toDouble()))); + soilProfileTable->setItem(row, 2, new QTableWidgetItem(QString::number(obj.value("nz").toInt()))); + auto mat = new QComboBox(); mat->addItem("Elastic"); int mi = mat->findText(obj.value("material").toString("Elastic")); if (mi>=0) mat->setCurrentIndex(mi); soilProfileTable->setCellWidget(row, 3, mat); + QJsonArray mp = obj.value("mat_props").toArray(); QStringList mpStr; for (const auto& x : mp) mpStr << (x.isString()? x.toString() : QString::number(x.toDouble())); soilProfileTable->setItem(row, 4, new QTableWidgetItem(mpStr.join(", "))); + auto damp = new QComboBox(); damp->addItem("Frequency-Rayleigh"); int di = damp->findText(obj.value("damping").toString("Frequency-Rayleigh")); if (di>=0) damp->setCurrentIndex(di); soilProfileTable->setCellWidget(row,5,damp); + QJsonArray dp = obj.value("damping_props").toArray(); QStringList dpStr; for (const auto& x : dp) dpStr << QString::number(x.toDouble()); soilProfileTable->setItem(row, 6, new QTableWidgetItem(dpStr.join(", "))); + } + + // Foundation + const auto fInfo = soilFoundationInfo.value("foundation_info").toObject(); + // gravity_x and gravity_y are fixed to 0.0 in the UI + gravZFound->setValue(fInfo.value("gravity_z").toDouble(gravZFound->value())); + embeddedFoundation->setChecked(fInfo.value("embedded").toBool(embeddedFoundation->isChecked())); + dxFound->setValue(fInfo.value("dx").toDouble(dxFound->value())); + dyFound->setValue(fInfo.value("dy").toDouble(dyFound->value())); + dzFound->setValue(fInfo.value("dz").toDouble(dzFound->value())); + numPartsFound->setValue(fInfo.value("num_partitions").toInt(numPartsFound->value())); + columnEmbedDepth->setValue(fInfo.value("column_embedment_depth").toDouble(columnEmbedDepth->value())); + const auto sec = fInfo.value("column_section_props").toObject(); + secE->setValue(sec.value("E").toDouble(secE->value())); + secA->setValue(sec.value("A").toDouble(secA->value())); + secIy->setValue(sec.value("Iy").toDouble(secIy->value())); + secIz->setValue(sec.value("Iz").toDouble(secIz->value())); + secG->setValue(sec.value("G").toDouble(secG->value())); + secJ->setValue(sec.value("J").toDouble(secJ->value())); + + foundationProfileTable->setRowCount(0); + for (const auto& v : fInfo.value("foundation_profile").toArray()) { + const auto obj = v.toObject(); + int row = foundationProfileTable->rowCount(); foundationProfileTable->insertRow(row); + foundationProfileTable->setItem(row, 0, new QTableWidgetItem(QString::number(obj.value("x_min").toDouble()))); + foundationProfileTable->setItem(row, 1, new QTableWidgetItem(QString::number(obj.value("x_max").toDouble()))); + foundationProfileTable->setItem(row, 2, new QTableWidgetItem(QString::number(obj.value("y_min").toDouble()))); + foundationProfileTable->setItem(row, 3, new QTableWidgetItem(QString::number(obj.value("y_max").toDouble()))); + foundationProfileTable->setItem(row, 4, new QTableWidgetItem(QString::number(obj.value("z_top").toDouble()))); + foundationProfileTable->setItem(row, 5, new QTableWidgetItem(QString::number(obj.value("z_bot").toDouble()))); + auto mat = new QComboBox(); mat->addItem("Elastic"); foundationProfileTable->setCellWidget(row, 6, mat); + QJsonArray mp = obj.value("mat_props").toArray(); QStringList mpStr; for (const auto& x : mp) mpStr << (x.isString()? x.toString() : QString::number(x.toDouble())); foundationProfileTable->setItem(row, 7, new QTableWidgetItem(mpStr.join(", "))); + auto damp = new QComboBox(); damp->addItem("Frequency-Rayleigh"); int di = damp->findText(obj.value("damping").toString("Frequency-Rayleigh")); if (di>=0) damp->setCurrentIndex(di); foundationProfileTable->setCellWidget(row,8,damp); + QJsonArray dp = obj.value("damping_props").toArray(); QStringList dpStr; for (const auto& x : dp) dpStr << QString::number(x.toDouble()); foundationProfileTable->setItem(row, 9, new QTableWidgetItem(dpStr.join(", "))); + } + + // Piles + const auto pile = soilFoundationInfo.value("pile_info").toObject(); + pileProfileTable->setRowCount(0); + for (const auto& v : pile.value("pile_profile").toArray()) { + const auto obj = v.toObject(); + int row = pileProfileTable->rowCount(); pileProfileTable->insertRow(row); + auto type = new QComboBox(); type->addItem("Grid"); type->addItem("Single"); int ti = type->findText(obj.value("type").toString("Grid"), Qt::MatchFixedString | Qt::MatchCaseSensitive); if (ti<0) ti = type->findText(obj.value("type").toString("Grid"), Qt::MatchFixedString); if (ti>=0) type->setCurrentIndex(ti); pileProfileTable->setCellWidget(row,0,type); + QString params; + if (type->currentText().toLower() == "grid") { + params = QString("%1, %2, %3, %4, %5, %6").arg(obj.value("x_start").toDouble()).arg(obj.value("y_start").toDouble()).arg(obj.value("spacing_x").toDouble()).arg(obj.value("spacing_y").toDouble()).arg(obj.value("nx").toInt()).arg(obj.value("ny").toInt()); + } else { + params = QString("%1, %2, %3, %4").arg(obj.value("x_top").toDouble()).arg(obj.value("x_bot").toDouble()).arg(obj.value("y_top").toDouble()).arg(obj.value("y_bot").toDouble()); + } + pileProfileTable->setItem(row, 1, new QTableWidgetItem(params)); + pileProfileTable->setItem(row, 2, new QTableWidgetItem(QString::number(obj.value("z_top").toDouble()))); + pileProfileTable->setItem(row, 3, new QTableWidgetItem(QString::number(obj.value("z_bot").toDouble()))); + pileProfileTable->setItem(row, 4, new QTableWidgetItem(QString::number(obj.value("nz").toInt()))); + pileProfileTable->setItem(row, 5, new QTableWidgetItem(QString::number(obj.value("r").toDouble()))); + auto section = new QComboBox(); section->addItem("No-Section"); pileProfileTable->setCellWidget(row, 6, section); + auto mat = new QComboBox(); mat->addItem("Elastic"); pileProfileTable->setCellWidget(row, 7, mat); + QJsonArray mp = obj.value("mat_props").toArray(); QStringList mpStr; for (const auto& x : mp) mpStr << (x.isString()? x.toString() : QString::number(x.toDouble())); pileProfileTable->setItem(row, 8, new QTableWidgetItem(mpStr.join(", "))); + QJsonArray tr = obj.value("transformation").toArray(); QString trName = tr.size() > 0 ? tr.at(0).toString("Linear") : "Linear"; auto trans = new QComboBox(); trans->addItem("Linear"); trans->addItem("PDelta"); int tri = trans->findText(trName); if (tri>=0) trans->setCurrentIndex(tri); pileProfileTable->setCellWidget(row, 9, trans); + double tx = tr.size() > 1 ? tr.at(1).toDouble(0.0) : 0.0; + double ty = tr.size() > 2 ? tr.at(2).toDouble(1.0) : 1.0; + double tz = tr.size() > 3 ? tr.at(3).toDouble(0.0) : 0.0; + pileProfileTable->setItem(row, 10, new QTableWidgetItem(QString("%1, %2, %3").arg(tx).arg(ty).arg(tz))); + } + const auto iface = pile.value("pile_interface").toObject(); + pileNumPointsPerimeter->setValue(iface.value("num_points_on_perimeter").toInt(pileNumPointsPerimeter->value())); + pileNumPointsLength->setValue(iface.value("num_points_along_length").toInt(pileNumPointsLength->value())); + pilePenaltyParameter->setValue(iface.value("penalty_parameter").toDouble(pilePenaltyParameter->value())); + + return true; +} + +void SSI_SoilFoundationType1Widget::plot() const { + // Placeholder: no plotting implemented yet +} + +QStringList SSI_SoilFoundationType1Widget::getRandomVariableNames() const { return QStringList(); } + + +int SSI_SoilFoundationType1Widget::getNumberOfCores() const { + int ncoresofsoil = int(numPartsSoil->value()); + int ncoresoffoundation = int(numPartsFound->value()); + // if using DRM, add its partitions + if (boundaryCombo->currentText() == "DRM") { + ncoresofsoil += int(drmNumPartitions->value()); + } + return ncoresofsoil + ncoresoffoundation; +} + + diff --git a/Workflow/SIM/SSI/SSI_SoilFoundationType1Widget.h b/Workflow/SIM/SSI/SSI_SoilFoundationType1Widget.h new file mode 100644 index 000000000..1b1cbd9b5 --- /dev/null +++ b/Workflow/SIM/SSI/SSI_SoilFoundationType1Widget.h @@ -0,0 +1,88 @@ +/* ***************************************************************************** +Copyright (c) 2016-2025, The Regents of the University of California (Regents). +All rights reserved. +*************************************************************************** */ + +#ifndef SSI_SOIL_FOUNDATION_TYPE1_WIDGET_H +#define SSI_SOIL_FOUNDATION_TYPE1_WIDGET_H + +#include "SSI_SoilFoundationBaseWidget.h" + +class QGroupBox; +class QGridLayout; +class QVBoxLayout; +class QHBoxLayout; +class QTableWidget; +class QTableWidgetItem; +class QComboBox; +class QLineEdit; +class QSpinBox; +class QDoubleSpinBox; +class QPushButton; +class QCheckBox; + +class SSI_SoilFoundationType1Widget : public SSI_SoilFoundationBaseWidget { + Q_OBJECT +public: + explicit SSI_SoilFoundationType1Widget(QWidget* parent = nullptr); + ~SSI_SoilFoundationType1Widget() override = default; + + QString typeId() const override { return QStringLiteral("soil_foundation_type_1"); } + + bool validate(QStringList& errors, bool interactiveIfModelMissing = false) const override; + bool outputToJSON(QJsonObject& soilFoundationInfo) const override; + bool inputFromJSON(const QJsonObject& soilFoundationInfo) override; + void plot() const override; + int getNumberOfCores() const override; + + bool copyFiles(QString &destDir) override { Q_UNUSED(destDir); return true; } + QStringList getRandomVariableNames() const override; + +private: + // Soil + QDoubleSpinBox *xMinSoil {nullptr}, *xMaxSoil {nullptr}; + QDoubleSpinBox *yMinSoil {nullptr}, *yMaxSoil {nullptr}; + QSpinBox *nxSoil {nullptr}, *nySoil {nullptr}; + QDoubleSpinBox *gravZSoil {nullptr}; + QSpinBox *numPartsSoil {nullptr}; + QComboBox *boundaryCombo {nullptr}; + QGroupBox *drmOptionsGroup {nullptr}; + QComboBox *absorbingLayerCombo {nullptr}; + QSpinBox *drmNumPartitions {nullptr}; + QSpinBox *drmNumLayers {nullptr}; + QDoubleSpinBox *drmRayleighDamping {nullptr}; + QCheckBox *drmMatchDamping {nullptr}; + QTableWidget* soilProfileTable {nullptr}; + + // Foundation + QDoubleSpinBox *gravZFound {nullptr}; + QCheckBox *embeddedFoundation {nullptr}; + QDoubleSpinBox *dxFound {nullptr}, *dyFound {nullptr}, *dzFound {nullptr}; + QSpinBox *numPartsFound {nullptr}; + QDoubleSpinBox *columnEmbedDepth {nullptr}; + QDoubleSpinBox *secE {nullptr}, *secA {nullptr}, *secIy {nullptr}, *secIz {nullptr}, *secG {nullptr}, *secJ {nullptr}; + QCheckBox* showSectionPropsCheck {nullptr}; + QWidget* sectionPropsContainer {nullptr}; + QTableWidget* foundationProfileTable {nullptr}; + + // Piles + QTableWidget* pileProfileTable {nullptr}; + QSpinBox *pileNumPointsPerimeter {nullptr}; + QSpinBox *pileNumPointsLength {nullptr}; + QDoubleSpinBox *pilePenaltyParameter {nullptr}; + + void setupSoilGroup(QWidget* parentWidget); + void setupFoundationGroup(QWidget* parentWidget); + void setupPilesGroup(QWidget* parentWidget); + + static QList parseCsvDoubles(const QString& text, bool* ok); + static QStringList parseCsvTokens(const QString& text, bool* ok); + void registerRVsFromCsv(const QString& text) const; +}; + +#endif // SSI_SOIL_FOUNDATION_TYPE1_WIDGET_H + + + + + diff --git a/Workflow/Workflow.pri b/Workflow/Workflow.pri index 287cd152c..85e7cc6aa 100644 --- a/Workflow/Workflow.pri +++ b/Workflow/Workflow.pri @@ -18,6 +18,7 @@ INCLUDEPATH += $$PWD/WORKFLOW INCLUDEPATH += $$PWD/WORKFLOW/Utils INCLUDEPATH += $$PWD/WORKFLOW/ModelViewItems INCLUDEPATH += $$PWD/SIM +INCLUDEPATH += $$PWD/SIM/SSI INCLUDEPATH += $$PWD/EXECUTION INCLUDEPATH += $$PWD/EVENTS INCLUDEPATH += $$PWD/EVENTS/earthquake @@ -134,6 +135,10 @@ SOURCES += $$PWD/UQ/UQ_EngineSelection.cpp \ $$PWD/SIM/SIM_Selection.cpp \ $$PWD/SIM/InputWidgetBIM.cpp \ $$PWD/SIM/OpenSeesParser.cpp \ + $$PWD/SIM/SSI/SSI_Simulation.cpp \ + $$PWD/SIM/SSI/SSI_Custom3DBuildingWidget.cpp \ + $$PWD/SIM/SSI/SSI_SoilFoundationType1Widget.cpp \ + $$PWD/SIM/Femora.cpp \ $$PWD/ANALYSIS/FEA_Selection.cpp \ $$PWD/ANALYSIS/InputWidgetOpenSeesAnalysis.cpp \ $$PWD/EXECUTION/RunLocalWidget.cpp \ @@ -164,8 +169,7 @@ SOURCES += $$PWD/UQ/UQ_EngineSelection.cpp \ $$PWD/EDP/StandardEDP.cpp \ $$PWD/EDP/UserDefinedEDP.cpp \ $$PWD/EDP/SurrogateEDP.cpp \ - $$PWD/EDP/EDP.cpp \ - $$PWD/SIM/Femora.cpp + $$PWD/EDP/EDP.cpp HEADERS += $$PWD/UQ/UQ_EngineSelection.h \ $$PWD/ANALYSIS/CustomPySimulation.h \ @@ -271,6 +275,12 @@ HEADERS += $$PWD/UQ/UQ_EngineSelection.h \ $$PWD/SIM/SIM_Selection.h \ $$PWD/SIM/InputWidgetBIM.h \ $$PWD/SIM/OpenSeesParser.h \ + $$PWD/SIM/SSI/SSI_Simulation.h \ + $$PWD/SIM/SSI/SSI_BuildingWidgetBase.h \ + $$PWD/SIM/SSI/SSI_Custom3DBuildingWidget.h \ + $$PWD/SIM/SSI/SSI_SoilFoundationBaseWidget.h \ + $$PWD/SIM/SSI/SSI_SoilFoundationType1Widget.h \ + $$PWD/SIM/Femora.h \ $$PWD/ANALYSIS/FEA_Selection.h \ $$PWD/ANALYSIS/InputWidgetOpenSeesAnalysis.h \ $$PWD/EXECUTION/RunLocalWidget.h \ @@ -301,5 +311,5 @@ HEADERS += $$PWD/UQ/UQ_EngineSelection.h \ $$PWD/EDP/EDP_Selection.h \ $$PWD/EDP/StandardEDP.h \ $$PWD/EDP/SurrogateEDP.h \ - $$PWD/EDP/EDP.h \ - $$PWD/SIM/Femora.h + $$PWD/EDP/EDP.h +