diff --git a/docs/fracture_well_ui_redesign_plan.md b/docs/fracture_well_ui_redesign_plan.md new file mode 100644 index 0000000..f6b2338 --- /dev/null +++ b/docs/fracture_well_ui_redesign_plan.md @@ -0,0 +1,392 @@ +# Fracture Well UI Redesign Plan + +## Problem Summary + +Current `Wells` page structure is not suitable for the following scenario: + +- reservoir may contain zero fracture wells +- reservoir may contain one fracture well +- reservoir may contain multiple fracture wells +- `num_fracture_wells` must equal the fracture-well count +- `welloc{i,1}` stores perforation coordinates for fracture well `i` +- perforation coordinates are often fracture coordinates, but they may also be independent coordinates +- fracture engineering input can stay unified in `fracture.input_content` +- perforation data cannot be merged into one shared array, otherwise multiple fracture wells cannot be distinguished + +The current controller only reads and writes the first entry of `c.wells.welloc`, so the present UI effectively assumes only one fracture well. + +## Current Constraint In Existing Code + +Existing runtime logic already supports multiple fracture wells in principle: + +- `run_case.m` + - `build_fracture_wells(...)` loops over `num_fracture_wells` +- original case files + - example case 7 uses `num_fracture_wells = 6` + - `welloc{1}` to `welloc{6}` are defined separately + - `well2` also has 6 rows and matches them one by one + +The main mismatch is in the GUI/controller layer: + +- `FractureWellLocationTable` is a single table +- controller uses `unwrapFirstCell(c.wells.welloc)` +- controller writes back with `replaceFirstCell(...)` + +So the GUI schema is the real bottleneck. + +## Recommended UI Principle + +Do not organize the fracture-well section as: + +- one table for all perforation coordinates only + +Instead organize it as: + +- one summary table for fracture well definitions +- one detail editor for the currently selected fracture well's perforation coordinates + +This is the safest structure because: + +- it supports `0 / 1 / N` fracture wells +- each fracture well keeps its own `welloc{i,1}` +- `well2(i,:)` and `welloc{i,1}` can stay naturally linked by row index +- fracture geometry remains independent in `fracture.input_content` + +## Recommended Data Mapping + +### Keep These Backend Fields + +Keep the runtime/backend idea below: + +```matlab +c.wells.num_fracture_wells +c.wells.well2 +c.wells.welloc +``` + +Recommended interpretation: + +- `c.wells.num_fracture_wells = size(c.wells.well2, 1)` +- `c.wells.welloc{i,1}` is the perforation coordinate matrix of fracture well `i` +- `size(c.wells.welloc, 1)` should equal `size(c.wells.well2, 1)` + +Recommended invariant: + +```matlab +num_fracture_wells == size(well2,1) == numel(welloc) +``` + +### Suggested Meaning Of `well2` + +Treat `well2` as the fracture-well summary table only. + +Recommended editable columns: + +1. `well_name` +2. `rw` +3. `skin` +4. `welltype` + +Recommended non-editable or derived columns: + +1. `nperf` +2. `perf_index` + +Reason: + +- `nperf` is derived from `welloc{i,1}` +- `perf_index` is generated by `findWelloc(...)` +- these two should not be manually edited in the GUI + +If keeping the current 6-column backend shape for compatibility, then GUI can still display 6 columns, but: + +- column 2 `nperf` should be read-only +- column 3 `perf_index` should be read-only or hidden + +## Recommended Wells Page Layout + +### Section A: Conventional Wells + +Keep existing `Well1Table`. + +This remains for ordinary wells already handled by `handle_well1(...)`. + +### Section B: Fracture Wells Summary + +Repurpose current `Well2Table` into a fracture-well summary table. + +Recommended label: + +- `Fracture Wells` + +Recommended columns: + +1. `well_name` +2. `rw` +3. `skin` +4. `welltype` +5. `nperf` optional read-only display + +Buttons: + +- `Add Fracture Well` +- `Delete Fracture Well` + +Behavior: + +- add one row to `well2` +- also create one empty `welloc{i,1} = []` +- delete row `i` from both `well2` and `welloc` +- after any add/delete: + - `num_fracture_wells = size(well2,1)` + +### Section C: Perforation Detail Editor + +Do not keep one generic `FractureWellLocationTable` without context. + +Replace it conceptually with: + +- one selector showing which fracture well is being edited +- one table that edits only that well's perforation coordinates + +Recommended controls: + +1. `SelectedFracWellDropDown` or `SelectedFracWellListBox` +2. `FractureWellLocationTable` +3. `Add Perf Row` +4. `Delete Perf Row` +5. optional helper text label + +Recommended table columns: + +1. `x` +2. `y` +3. `z` + +Behavior: + +- when user selects fracture well `i` + - table shows `welloc{i,1}` +- when table changes + - write back to `welloc{i,1}` +- when no fracture well exists + - disable this table and show hint text like: + - `No fracture well is defined. Please add a fracture well first.` + +This is the key redesign. + +## Relationship Between Fracture Input And Perforation Input + +These two parts should stay separate in the UI. + +### Fracture Tab + +Keep: + +- `fracture.input_content` + +This is for fracture geometry/engineering input. + +### Wells Tab + +Keep separate: + +- fracture-well summary +- per-well perforation coordinates + +Reason: + +- fractures describe the reservoir fracture system +- perforations describe where a specific fracture well connects to the model +- one fracture well may connect to one or more fracture coordinates +- perforation coordinates may match fracture coordinates, but not always + +So they should not be merged into the same editable widget. + +## Whether `num_fracture_wells` Should Be A Separate UI Field + +Recommendation: + +- do not expose `num_fracture_wells` as an independent editable numeric field + +Instead: + +- derive it automatically from fracture-well summary row count + +Reason: + +- avoids inconsistency +- user only manages actual fracture-well rows +- controller maintains `num_fracture_wells` + +## Recommended App Designer Changes + +These are the parts you should change manually in `.mlapp`. + +### Minimum Change Version + +If you want the smallest UI change: + +1. keep `Well1Table` +2. keep `Well2Table` +3. keep `FractureWellLocationTable` +4. add one selector control above `FractureWellLocationTable` + - name suggestion: `SelectedFracWellDropDown` +5. change labels so that: + - `Well2Label` -> `Fracture Wells` + - `WellLocationLabel` -> `Perforation Coordinates` +6. change `FractureWellLocationTable` columns from one generic column to three columns: + - `x`, `y`, `z` +7. add a small note label: + - `Each fracture well has its own perforation coordinate list.` + +This is the recommended minimum viable fix. + +### Better Version + +If you can slightly improve layout: + +Use a 3-block layout in `WellsTab`: + +1. top-left: `Well1Table` +2. top-right: `Well2Table` +3. bottom-wide: selected fracture well perforation editor + +This will be clearer than the current four unrelated tables. + +## Recommended Table Definitions + +### `Well1Table` + +No major structural change required. + +### `Well2Table` + +Recommended columns: + +1. `well_name` +2. `rw` +3. `skin` +4. `welltype` + +Optional extra read-only columns: + +5. `nperf` + +If you want to keep backend compatibility without changing much controller code later, you may still visually keep 6 columns: + +1. `well_name` +2. `nperf` read-only +3. `perf_index` hidden or read-only expression +4. `rw` +5. `skin` +6. `welltype` + +But from user experience perspective, 4 editable columns are better. + +### `FractureWellLocationTable` + +Recommended columns: + +1. `x` +2. `y` +3. `z` + +Each row is one perforation point for the currently selected fracture well. + +## Controller Change Direction + +When you later modify `EDFMAppController.m`, the well section should change conceptually as follows. + +### Current Wrong Logic + +```matlab +obj.setTableValue('FractureWellLocationTable', obj.unwrapFirstCell(c.wells.welloc)); +... +fractureWellLocation = obj.getNumericTableValue('FractureWellLocationTable', obj.unwrapFirstCell(c.wells.welloc)); +c.wells.welloc = obj.replaceFirstCell(c.wells.welloc, fractureWellLocation); +c.wells.num_fracture_wells = numel(c.wells.welloc); +``` + +This only handles the first fracture well. + +### Target Logic + +Need a selected index such as: + +```matlab +selectedFracWellIndex +``` + +Read UI: + +```matlab +c.wells.well2 = ... +c.wells.num_fracture_wells = size(c.wells.well2,1); +ensure c.wells.welloc has same length +c.wells.welloc{selectedFracWellIndex,1} = currentPerfTable +``` + +Write UI: + +```matlab +set Well2Table from c.wells.well2 +refresh fracture-well selector items +load FractureWellLocationTable from c.wells.welloc{selectedFracWellIndex,1} +``` + +## Validation Rules To Add + +When this section is later coded, these checks are recommended: + +1. `size(well2,1) == numel(welloc)` +2. `num_fracture_wells == size(well2,1)` +3. each `welloc{i,1}` must be empty or an `N x 3` numeric matrix +4. if `well2(i,1)` is empty, generate default name like `wf1`, `wf2` +5. when no fracture well exists: + - `well2 = {}` + - `welloc = {}` + - `num_fracture_wells = 0` +6. if user deletes a fracture well, also delete its perforation list + +## Practical Recommendation + +For your current stage, the best balance is: + +1. fracture geometry remains in `FractureTab` +2. ordinary wells remain in `Well1Table` +3. fracture wells use `Well2Table` +4. perforations are edited through one selected-well detail table +5. `num_fracture_wells` is auto-derived, not manually entered + +This will match both: + +- the old case-script logic +- the future GUI editing logic + +And it also naturally supports the case where there is no fracture well. + +## Manual UI Rename Suggestions + +Suggested visible labels: + +- `Well1Label` -> `Conventional Wells` +- `Well2Label` -> `Fracture Wells` +- `WellLocationLabel` -> `Perforation Coordinates` +- `ScheduleLabel` keep as `Schedule` + +Suggested new control names if you add them: + +- `SelectedFracWellDropDown` +- `SelectedFracWellDropDownLabel` +- `FracWellHintLabel` + +## Conclusion + +The main point is: + +- fracture information can stay unified in `fracture.input_content` +- perforation information must be stored per fracture well +- therefore the GUI must be changed from "one perforation table" to "fracture-well summary + selected-well perforation detail" + +That is the most stable and least confusing design for the current codebase. diff --git a/gui_support/app/EDFMAppController.m b/gui_support/app/EDFMAppController.m index a02a852..0e58926 100644 --- a/gui_support/app/EDFMAppController.m +++ b/gui_support/app/EDFMAppController.m @@ -5,6 +5,7 @@ classdef EDFMAppController < handle Results struct = struct() ProjectRoot char CurrentPlotAction string = "" + CurrentFracWellIndex double = 1 end methods @@ -25,6 +26,7 @@ classdef EDFMAppController < handle obj.setDropDownItems('OWoil_modelDropDown', {'1', '2'}, '1'); obj.setDropDownItems('MCgas_modelDropDown', {'1'}, '1'); obj.configureWellTables(); + obj.configureFracWellControls(); obj.applyFixedUIState(); obj.Results = struct(); @@ -180,6 +182,9 @@ classdef EDFMAppController < handle obj.App.(tableName).UserData = []; else obj.App.(tableName).UserData = indices(1, 1); + if strcmp(tableName, 'Well2Table') + obj.setSelectedFracWellIndex(indices(1, 1), true); + end end end @@ -188,6 +193,37 @@ classdef EDFMAppController < handle return; end + switch tableName + case 'Well2Table' + obj.pushUIToConfig(); + c = normalize_config(obj.Config); + well2 = obj.getTableValue('Well2Table', c.wells.well2); + newRow = obj.getDefaultRowForTable(tableName); + nextIndex = size(well2, 1) + 1; + if isempty(newRow{1}) + newRow{1} = sprintf('wf%d', nextIndex); + end + if isempty(well2) + well2 = newRow; + else + well2(end + 1, :) = newRow; + end + c.wells.well2 = well2; + c.wells.welloc = obj.resizeWellocCell(c.wells.welloc, size(well2, 1)); + c.wells.num_fracture_wells = size(well2, 1); + obj.Config = c; + obj.CurrentFracWellIndex = c.wells.num_fracture_wells; + obj.refreshUIFromConfig(); + return; + case 'FractureWellLocationTable' + if obj.getFractureWellCount() == 0 + uialert(obj.App.UIFigure, ... + 'Please add a fracture well before editing perforation coordinates.', ... + 'No Fracture Well'); + return; + end + end + data = obj.App.(tableName).Data; newRow = obj.getDefaultRowForTable(tableName); @@ -209,6 +245,38 @@ classdef EDFMAppController < handle return; end + if strcmp(tableName, 'Well2Table') + obj.pushUIToConfig(); + c = normalize_config(obj.Config); + selectedRow = []; + if isprop(obj.App.(tableName), 'UserData') + selectedRow = obj.App.(tableName).UserData; + end + if isempty(selectedRow) || ~isscalar(selectedRow) || selectedRow < 1 + uialert(obj.App.UIFigure, ... + sprintf('Please select a row in %s first.', tableName), ... + 'Delete Row'); + return; + end + if selectedRow > size(c.wells.well2, 1) + return; + end + c.wells.well2(selectedRow, :) = []; + if iscell(c.wells.welloc) && numel(c.wells.welloc) >= selectedRow + c.wells.welloc(selectedRow, :) = []; + end + c.wells.welloc = obj.resizeWellocCell(c.wells.welloc, size(c.wells.well2, 1)); + c.wells.num_fracture_wells = size(c.wells.well2, 1); + if c.wells.num_fracture_wells == 0 + obj.CurrentFracWellIndex = 1; + else + obj.CurrentFracWellIndex = min(selectedRow, c.wells.num_fracture_wells); + end + obj.Config = c; + obj.refreshUIFromConfig(); + return; + end + data = obj.App.(tableName).Data; if isempty(data) return; @@ -455,8 +523,10 @@ classdef EDFMAppController < handle obj.setNumericFieldValue('MCstress_factor_ref_pressureEditField', c.flow.multi_component.gas_prop.stress_factor_ref_pressure); obj.setTableValue('Well1Table', c.wells.well1); - obj.setTableValue('FractureWellLocationTable', obj.unwrapFirstCell(c.wells.welloc)); obj.setTableValue('Well2Table', c.wells.well2); + c = obj.normalizeScheduleConfig(c); + obj.Config = c; + obj.refreshFracWellControlsFromConfig(); obj.setTableValue('ScheduleTable', obj.unwrapFirstCell(c.schedule.well_schedules)); obj.setTextAreaExpr('TimeTextArea', c.schedule.time); obj.setTextAreaExpr('DtMinTextArea', c.schedule.dtmin); @@ -648,11 +718,11 @@ classdef EDFMAppController < handle c.flow.multi_component.cvw = commonCvw; c.wells.well1 = obj.getTableValue('Well1Table', c.wells.well1); - fractureWellLocation = obj.getNumericTableValue( ... - 'FractureWellLocationTable', obj.unwrapFirstCell(c.wells.welloc)); - c.wells.welloc = obj.replaceFirstCell(c.wells.welloc, fractureWellLocation); - c.wells.num_fracture_wells = numel(c.wells.welloc); c.wells.well2 = obj.getTableValue('Well2Table', c.wells.well2); + c.wells.num_fracture_wells = size(c.wells.well2, 1); + c.wells.welloc = obj.resizeWellocCell(c.wells.welloc, c.wells.num_fracture_wells); + c = obj.persistCurrentFractureWellLocation(c); + c.wells.welloc = obj.resizeWellocCell(c.wells.welloc, c.wells.num_fracture_wells); firstPhaseSchedule = obj.getTableValue( ... 'ScheduleTable', obj.unwrapFirstCell(c.schedule.well_schedules)); @@ -661,6 +731,7 @@ classdef EDFMAppController < handle c.schedule.time = obj.getTextAreaExpr('TimeTextArea', c.schedule.time); c.schedule.dtmin = obj.getTextAreaExpr('DtMinTextArea', c.schedule.dtmin); c.schedule.dtmax = obj.getTextAreaExpr('DtMaxTextArea', c.schedule.dtmax); + c = obj.normalizeScheduleConfig(c); c.solver.yitap = obj.getNumericFieldValue('YitaPEditField', c.solver.yitap); c.solver.yitas = obj.getNumericFieldValue('YitaSEditField', c.solver.yitas); @@ -673,6 +744,16 @@ classdef EDFMAppController < handle obj.syncModelTabs(); end + function onSelectedFracWellChanged(obj) + if isempty(fieldnames(obj.Config)) + obj.Config = normalize_config(create_empty_config()); + else + obj.Config = obj.persistCurrentFractureWellLocation(normalize_config(obj.Config)); + end + obj.CurrentFracWellIndex = obj.getSelectedFracWellIndexFromUI(); + obj.refreshFracWellControlsFromConfig(); + end + function applyFixedUIState(obj) obj.setControlVisible('ModelFlagDropDownLabel', false); obj.setControlVisible('ModelFlagDropDown', false); @@ -1505,6 +1586,14 @@ classdef EDFMAppController < handle end end + function configureFracWellControls(obj) + if isprop(obj.App, 'SelectedFracWellDropDown') + obj.App.SelectedFracWellDropDown.Items = {''}; + obj.App.SelectedFracWellDropDown.Value = ''; + obj.App.SelectedFracWellDropDown.ValueChangedFcn = @(src, event) obj.onSelectedFracWellChanged(); + end + end + function configureSingleTable(obj, tableName) if ~isprop(obj.App, tableName) return; @@ -1518,7 +1607,12 @@ classdef EDFMAppController < handle tableHandle = obj.App.(tableName); tableHandle.ColumnName = columnNames; tableHandle.RowName = {}; - tableHandle.ColumnEditable = true(1, numel(columnNames)); + switch tableName + case 'Well2Table' + tableHandle.ColumnEditable = [true, false, false, true, true, true]; + otherwise + tableHandle.ColumnEditable = true(1, numel(columnNames)); + end end function columnNames = getColumnNamesForTable(~, tableName) @@ -1589,5 +1683,321 @@ classdef EDFMAppController < handle end textValue = strtrim(evalc('disp(value)')); end + + function c = persistCurrentFractureWellLocation(obj, c) + c = normalize_config(c); + fractureWellCount = size(c.wells.well2, 1); + c.wells.welloc = obj.resizeWellocCell(c.wells.welloc, fractureWellCount); + if fractureWellCount == 0 + c.wells.welloc = cell(0, 1); + c.wells.num_fracture_wells = 0; + return; + end + + selectedIndex = min(max(obj.getSelectedFracWellIndex(), 1), fractureWellCount); + fractureWellLocation = obj.getNumericTableValue('FractureWellLocationTable', []); + c.wells.welloc{selectedIndex, 1} = fractureWellLocation; + c.wells.num_fracture_wells = fractureWellCount; + end + + function refreshFracWellControlsFromConfig(obj) + c = normalize_config(obj.Config); + fractureWellCount = size(c.wells.well2, 1); + c.wells.welloc = obj.resizeWellocCell(c.wells.welloc, fractureWellCount); + c.wells.num_fracture_wells = fractureWellCount; + obj.Config = c; + + obj.refreshFracWellDropDown(fractureWellCount, c.wells.well2); + + if fractureWellCount == 0 + obj.CurrentFracWellIndex = 1; + obj.setTableValue('FractureWellLocationTable', []); + obj.setControlEnabled('SelectedFracWellDropDown', false); + obj.setControlEnabled('FractureWellLocationTable', false); + obj.setControlEnabled('AddFracLocRowButton', false); + obj.setControlEnabled('DeleteFracLocRowButton', false); + return; + end + + obj.CurrentFracWellIndex = min(max(obj.CurrentFracWellIndex, 1), fractureWellCount); + obj.setSelectedFracWellIndex(obj.CurrentFracWellIndex, false); + obj.setControlEnabled('SelectedFracWellDropDown', true); + obj.setControlEnabled('FractureWellLocationTable', true); + obj.setControlEnabled('AddFracLocRowButton', true); + obj.setControlEnabled('DeleteFracLocRowButton', true); + obj.setTableValue('FractureWellLocationTable', c.wells.welloc{obj.CurrentFracWellIndex, 1}); + end + + function refreshFracWellDropDown(obj, fractureWellCount, well2) + if ~isprop(obj.App, 'SelectedFracWellDropDown') + return; + end + + if fractureWellCount == 0 + obj.App.SelectedFracWellDropDown.Items = {''}; + obj.App.SelectedFracWellDropDown.Value = ''; + return; + end + + items = cell(fractureWellCount, 1); + for i = 1:fractureWellCount + wellName = sprintf('wf%d', i); + if size(well2, 1) >= i && size(well2, 2) >= 1 && ~isempty(well2{i, 1}) + wellName = char(string(well2{i, 1})); + end + items{i} = sprintf('%d: %s', i, wellName); + end + obj.App.SelectedFracWellDropDown.Items = items; + selectedValue = items{min(max(obj.CurrentFracWellIndex, 1), fractureWellCount)}; + obj.App.SelectedFracWellDropDown.Value = selectedValue; + end + + function count = getFractureWellCount(obj) + if isempty(fieldnames(obj.Config)) + count = 0; + return; + end + c = normalize_config(obj.Config); + count = size(c.wells.well2, 1); + end + + function index = getSelectedFracWellIndex(obj) + fractureWellCount = obj.getFractureWellCount(); + if fractureWellCount == 0 + index = 1; + return; + end + index = min(max(obj.CurrentFracWellIndex, 1), fractureWellCount); + end + + function index = getSelectedFracWellIndexFromUI(obj) + if ~isprop(obj.App, 'SelectedFracWellDropDown') + index = obj.CurrentFracWellIndex; + return; + end + + value = string(obj.App.SelectedFracWellDropDown.Value); + token = regexp(char(value), '^\s*(\d+)', 'tokens', 'once'); + if isempty(token) + index = obj.CurrentFracWellIndex; + else + index = str2double(token{1}); + end + if isnan(index) || index < 1 + index = obj.CurrentFracWellIndex; + end + end + + function setSelectedFracWellIndex(obj, index, refreshTable) + if nargin < 3 + refreshTable = true; + end + + fractureWellCount = obj.getFractureWellCount(); + if fractureWellCount == 0 + obj.CurrentFracWellIndex = 1; + return; + end + + obj.CurrentFracWellIndex = min(max(index, 1), fractureWellCount); + if isprop(obj.App, 'SelectedFracWellDropDown') && ~isempty(obj.App.SelectedFracWellDropDown.Items) + items = obj.App.SelectedFracWellDropDown.Items; + obj.App.SelectedFracWellDropDown.Value = items{obj.CurrentFracWellIndex}; + end + + if refreshTable + obj.onSelectedFracWellChanged(); + end + end + + function welloc = resizeWellocCell(~, welloc, targetCount) + if nargin < 3 || isempty(targetCount) || targetCount <= 0 + welloc = cell(0, 1); + return; + end + + if isempty(welloc) || ~iscell(welloc) + welloc = cell(targetCount, 1); + else + welloc = welloc(:); + currentCount = numel(welloc); + if currentCount < targetCount + welloc(currentCount + 1:targetCount, 1) = {[]}; + elseif currentCount > targetCount + welloc = welloc(1:targetCount, 1); + end + end + + for i = 1:targetCount + if isempty(welloc{i, 1}) + welloc{i, 1} = []; + end + end + end + + function setControlEnabled(obj, propName, isEnabled) + if ~isprop(obj.App, propName) + return; + end + component = obj.App.(propName); + if isprop(component, 'Enable') + if isEnabled + component.Enable = 'on'; + else + component.Enable = 'off'; + end + end + end + + function c = normalizeScheduleConfig(obj, c) + c = normalize_config(c); + wellNames = obj.getAllConfiguredWellNames(c); + totalWells = numel(wellNames); + + phaseCount = max([ ... + numel(c.schedule.well_schedules), ... + numel(c.schedule.time), ... + numel(c.schedule.dtmin), ... + numel(c.schedule.dtmax), ... + c.schedule.number_phases, ... + 1]); + + if ~iscell(c.schedule.well_schedules) + c.schedule.well_schedules = cell(phaseCount, 1); + elseif numel(c.schedule.well_schedules) < phaseCount + c.schedule.well_schedules(end + 1:phaseCount, 1) = {[]}; + elseif numel(c.schedule.well_schedules) > phaseCount + c.schedule.well_schedules = c.schedule.well_schedules(1:phaseCount, 1); + else + c.schedule.well_schedules = c.schedule.well_schedules(:); + end + + for k = 1:phaseCount + c.schedule.well_schedules{k, 1} = obj.normalizeSinglePhaseSchedule( ... + c.schedule.well_schedules{k, 1}, wellNames, totalWells); + end + + c.schedule.number_phases = phaseCount; + end + + function phaseSchedule = normalizeSinglePhaseSchedule(obj, phaseSchedule, wellNames, totalWells) + if totalWells == 0 + phaseSchedule = cell(0, 10); + return; + end + + phaseSchedule = obj.ensureScheduleCellMatrix(phaseSchedule); + normalized = cell(totalWells, 10); + usedRows = false(size(phaseSchedule, 1), 1); + + for i = 1:totalWells + matchedRow = []; + for rowIndex = 1:size(phaseSchedule, 1) + if usedRows(rowIndex) + continue; + end + if size(phaseSchedule, 2) >= 1 && strcmp(string(phaseSchedule{rowIndex, 1}), string(wellNames{i})) + matchedRow = rowIndex; + break; + end + end + + if isempty(matchedRow) && i <= size(phaseSchedule, 1) && ~usedRows(i) + matchedRow = i; + end + + if isempty(matchedRow) + normalized(i, :) = obj.getDefaultClosedScheduleRow(wellNames{i}); + else + rowData = obj.padScheduleRow(phaseSchedule(matchedRow, :)); + rowData{1} = wellNames{i}; + normalized(i, :) = rowData; + usedRows(matchedRow) = true; + end + end + + phaseSchedule = normalized; + end + + function phaseSchedule = ensureScheduleCellMatrix(~, phaseSchedule) + if isempty(phaseSchedule) + phaseSchedule = cell(0, 10); + return; + end + + if ~iscell(phaseSchedule) + phaseSchedule = num2cell(phaseSchedule); + end + + if isvector(phaseSchedule) && size(phaseSchedule, 1) == 1 + if size(phaseSchedule, 2) == 10 + return; + end + end + + if isvector(phaseSchedule) && size(phaseSchedule, 2) == 1 + phaseSchedule = phaseSchedule'; + end + + if size(phaseSchedule, 2) > 10 + phaseSchedule = phaseSchedule(:, 1:10); + elseif size(phaseSchedule, 2) < 10 + phaseSchedule(:, end + 1:10) = {[]}; + end + end + + function rowData = padScheduleRow(~, rowData) + if ~iscell(rowData) + rowData = num2cell(rowData); + end + if size(rowData, 1) ~= 1 + rowData = rowData(1, :); + end + if size(rowData, 2) > 10 + rowData = rowData(:, 1:10); + elseif size(rowData, 2) < 10 + rowData(:, end + 1:10) = {[]}; + end + end + + function row = getDefaultClosedScheduleRow(~, wellName) + row = {char(string(wellName)), 'close', 'pro', 'const_pwf', 0, 0, '', '', '', ''}; + end + + function wellNames = getAllConfiguredWellNames(~, c) + well1Names = {}; + well2Names = {}; + + if isfield(c.wells, 'well1') && iscell(c.wells.well1) && ~isempty(c.wells.well1) + well1Names = cell(size(c.wells.well1, 1), 1); + for i = 1:size(c.wells.well1, 1) + name = ''; + if size(c.wells.well1, 2) >= 1 && ~isempty(c.wells.well1{i, 1}) + name = char(string(c.wells.well1{i, 1})); + end + if isempty(name) + name = sprintf('w%d', i); + end + well1Names{i} = name; + end + end + + if isfield(c.wells, 'well2') && iscell(c.wells.well2) && ~isempty(c.wells.well2) + well2Names = cell(size(c.wells.well2, 1), 1); + for i = 1:size(c.wells.well2, 1) + name = ''; + if size(c.wells.well2, 2) >= 1 && ~isempty(c.wells.well2{i, 1}) + name = char(string(c.wells.well2{i, 1})); + end + if isempty(name) + name = sprintf('wf%d', i); + c.wells.well2{i, 1} = name; + end + well2Names{i} = name; + end + end + + wellNames = [well1Names; well2Names]; + end end end diff --git a/gui_support/app/EDFM_Simulator_App.mlapp b/gui_support/app/EDFM_Simulator_App.mlapp index f094274..feda5ff 100644 Binary files a/gui_support/app/EDFM_Simulator_App.mlapp and b/gui_support/app/EDFM_Simulator_App.mlapp differ diff --git a/gui_support/app/context.txt b/gui_support/app/context.txt index 905dc03..d915b9c 100644 --- a/gui_support/app/context.txt +++ b/gui_support/app/context.txt @@ -183,10 +183,11 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase MCcs_NcTextArea matlab.ui.control.TextArea cs_NcTextAreaLabel matlab.ui.control.Label WellsTab matlab.ui.container.Tab + SelectedFracWellDropDown matlab.ui.control.DropDown + SelectedFractureWellLabel matlab.ui.control.Label ScheduleLabel matlab.ui.control.Label - WellLocationLabel matlab.ui.control.Label - Well2Label matlab.ui.control.Label - Well1Label matlab.ui.control.Label + FractureWellsLabel matlab.ui.control.Label + ConventionalWellsLabel matlab.ui.control.Label ScheduleTimeConfigLabel matlab.ui.control.Label AddFracLocRowButton matlab.ui.control.Button DeleteFracLocRowButton matlab.ui.control.Button @@ -1390,7 +1391,7 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase app.Well1Table.RowName = {}; app.Well1Table.ColumnEditable = true; app.Well1Table.CellSelectionCallback = createCallbackFcn(app, @Well1TableCellSelection, true); - app.Well1Table.Position = [35 475 372 182]; + app.Well1Table.Position = [43 194 446 167]; % Create FractureWellLocationTable app.FractureWellLocationTable = uitable(app.WellsTab); @@ -1398,7 +1399,7 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase app.FractureWellLocationTable.RowName = {}; app.FractureWellLocationTable.ColumnEditable = true; app.FractureWellLocationTable.CellSelectionCallback = createCallbackFcn(app, @FractureWellLocationTableCellSelection, true); - app.FractureWellLocationTable.Position = [44 179 366 185]; + app.FractureWellLocationTable.Position = [737 520 445 100]; % Create Well2Table app.Well2Table = uitable(app.WellsTab); @@ -1406,7 +1407,7 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase app.Well2Table.RowName = {}; app.Well2Table.ColumnEditable = true; app.Well2Table.CellSelectionCallback = createCallbackFcn(app, @Well2TableCellSelection, true); - app.Well2Table.Position = [552 472 594 185]; + app.Well2Table.Position = [35 476 594 185]; % Create ScheduleTable app.ScheduleTable = uitable(app.WellsTab); @@ -1449,25 +1450,25 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase % Create AddWell1RowButton app.AddWell1RowButton = uibutton(app.WellsTab, 'push'); app.AddWell1RowButton.ButtonPushedFcn = createCallbackFcn(app, @AddWell1RowButtonPushed, true); - app.AddWell1RowButton.Position = [136 420 100 23]; + app.AddWell1RowButton.Position = [144 128 100 41]; app.AddWell1RowButton.Text = 'AddWell1Row'; % Create DeleteWell1RowButton app.DeleteWell1RowButton = uibutton(app.WellsTab, 'push'); app.DeleteWell1RowButton.ButtonPushedFcn = createCallbackFcn(app, @DeleteWell1RowButtonPushed, true); - app.DeleteWell1RowButton.Position = [301 418 103 23]; + app.DeleteWell1RowButton.Position = [309 126 103 41]; app.DeleteWell1RowButton.Text = 'DeleteWell1Row'; % Create DeleteWell2RowButton app.DeleteWell2RowButton = uibutton(app.WellsTab, 'push'); app.DeleteWell2RowButton.ButtonPushedFcn = createCallbackFcn(app, @DeleteWell2RowButtonPushed, true); - app.DeleteWell2RowButton.Position = [931 421 103 23]; + app.DeleteWell2RowButton.Position = [414 425 103 23]; app.DeleteWell2RowButton.Text = 'DeleteWell2Row'; % Create AddWell2RowButton app.AddWell2RowButton = uibutton(app.WellsTab, 'push'); app.AddWell2RowButton.ButtonPushedFcn = createCallbackFcn(app, @AddWell2RowButtonPushed, true); - app.AddWell2RowButton.Position = [705 423 100 23]; + app.AddWell2RowButton.Position = [188 427 100 23]; app.AddWell2RowButton.Text = 'AddWell2Row'; % Create AddScheduleRowButton @@ -1485,13 +1486,13 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase % Create DeleteFracLocRowButton app.DeleteFracLocRowButton = uibutton(app.WellsTab, 'push'); app.DeleteFracLocRowButton.ButtonPushedFcn = createCallbackFcn(app, @DeleteFracLocRowButtonPushed, true); - app.DeleteFracLocRowButton.Position = [288 123 117 23]; + app.DeleteFracLocRowButton.Position = [1096 466 117 23]; app.DeleteFracLocRowButton.Text = 'DeleteFracLocRow'; % Create AddFracLocRowButton app.AddFracLocRowButton = uibutton(app.WellsTab, 'push'); app.AddFracLocRowButton.ButtonPushedFcn = createCallbackFcn(app, @AddFracLocRowButtonPushed, true); - app.AddFracLocRowButton.Position = [149 123 104 23]; + app.AddFracLocRowButton.Position = [954 464 104 23]; app.AddFracLocRowButton.Text = 'AddFracLocRow'; % Create ScheduleTimeConfigLabel @@ -1499,26 +1500,33 @@ classdef EDFM_Simulator_App < matlab.apps.AppBase app.ScheduleTimeConfigLabel.Position = [1003 331 131 30]; app.ScheduleTimeConfigLabel.Text = 'Schedule Time Config'; - % Create Well1Label - app.Well1Label = uilabel(app.WellsTab); - app.Well1Label.Position = [41 422 35 22]; - app.Well1Label.Text = 'Well1'; + % Create ConventionalWellsLabel + app.ConventionalWellsLabel = uilabel(app.WellsTab); + app.ConventionalWellsLabel.Position = [27 130 108 40]; + app.ConventionalWellsLabel.Text = 'Conventional Wells'; - % Create Well2Label - app.Well2Label = uilabel(app.WellsTab); - app.Well2Label.Position = [560 424 35 22]; - app.Well2Label.Text = 'Well2'; - - % Create WellLocationLabel - app.WellLocationLabel = uilabel(app.WellsTab); - app.WellLocationLabel.Position = [48 125 73 22]; - app.WellLocationLabel.Text = 'WellLocation'; + % Create FractureWellsLabel + app.FractureWellsLabel = uilabel(app.WellsTab); + app.FractureWellsLabel.Position = [43 428 82 22]; + app.FractureWellsLabel.Text = 'Fracture Wells'; % Create ScheduleLabel app.ScheduleLabel = uilabel(app.WellsTab); app.ScheduleLabel.Position = [555 127 55 22]; app.ScheduleLabel.Text = 'Schedule'; + % Create SelectedFractureWellLabel + app.SelectedFractureWellLabel = uilabel(app.WellsTab); + app.SelectedFractureWellLabel.HorizontalAlignment = 'right'; + app.SelectedFractureWellLabel.Position = [723 464 126 22]; + app.SelectedFractureWellLabel.Text = 'Selected Fracture Well'; + + % Create SelectedFracWellDropDown + app.SelectedFracWellDropDown = uidropdown(app.WellsTab); + app.SelectedFracWellDropDown.Items = {''}; + app.SelectedFracWellDropDown.Position = [864 464 70 22]; + app.SelectedFracWellDropDown.Value = ''; + % Create SolverRunTab app.SolverRunTab = uitab(app.TabGroup); app.SolverRunTab.Title = 'Solver&Run'; diff --git a/gui_support/results/render_case_plot_in_axes.m b/gui_support/results/render_case_plot_in_axes.m index 166b21f..a3c07ee 100644 --- a/gui_support/results/render_case_plot_in_axes.m +++ b/gui_support/results/render_case_plot_in_axes.m @@ -48,29 +48,32 @@ switch metric case "GPR" field_name = 'qg'; y_label = 'Gas production rate, m^3/d'; - for i = 1:n, data(i) = wellpara{i}{1, well_index}.(field_name); end + for i = 1:n, data(i) = get_well_metric_value(wellpara, i, well_index, field_name); end plot(ax, times, data, 'k^-', 'LineWidth', 1.2); case "OPR" field_name = 'qo'; y_label = 'Oil production rate, m^3/d'; - for i = 1:n, data(i) = wellpara{i}{1, well_index}.(field_name); end + for i = 1:n, data(i) = get_well_metric_value(wellpara, i, well_index, field_name); end plot(ax, times, data, 'k^-', 'LineWidth', 1.2); case "WPR" field_name = 'qw'; y_label = 'Water production rate, m^3/d'; - for i = 1:n, data(i) = wellpara{i}{1, well_index}.(field_name); end + for i = 1:n, data(i) = get_well_metric_value(wellpara, i, well_index, field_name); end plot(ax, times, data, 'k^-', 'LineWidth', 1.2); case "DPWF" y_label = 'Pressure derivative, MPa'; for i = 1:n if i == 1 - data(i) = (wellpara{i + 1}{1, well_index}.pwf - wellpara{i}{1, well_index}.pwf) / ... + data(i) = (get_well_metric_value(wellpara, i + 1, well_index, 'pwf') - ... + get_well_metric_value(wellpara, i, well_index, 'pwf')) / ... (log(times(i + 1)) - log(times(i))); elseif i == n - data(i) = (wellpara{i}{1, well_index}.pwf - wellpara{i - 1}{1, well_index}.pwf) / ... + data(i) = (get_well_metric_value(wellpara, i, well_index, 'pwf') - ... + get_well_metric_value(wellpara, i - 1, well_index, 'pwf')) / ... (log(times(i)) - log(times(i - 1))); else - data(i) = (wellpara{i + 1}{1, well_index}.pwf - wellpara{i - 1}{1, well_index}.pwf) / ... + data(i) = (get_well_metric_value(wellpara, i + 1, well_index, 'pwf') - ... + get_well_metric_value(wellpara, i - 1, well_index, 'pwf')) / ... (log(times(i + 1)) - log(times(i - 1))); end end @@ -79,7 +82,7 @@ switch metric otherwise field_name = 'pwf'; y_label = 'BHP, MPa'; - for i = 1:n, data(i) = wellpara{i}{1, well_index}.(field_name); end + for i = 1:n, data(i) = get_well_metric_value(wellpara, i, well_index, field_name); end plot(ax, times, data, 'k^-', 'LineWidth', 1.2); end @@ -89,6 +92,47 @@ ylabel(ax, y_label); title(ax, sprintf('Well Response: well %d, %s', well_index, metric)); end +function value = get_well_metric_value(wellpara, time_index, well_index, field_name) +well_entry = get_well_entry(wellpara, time_index, well_index); +if isempty(well_entry) + value = NaN; + return; +end +if ~isstruct(well_entry) + value = NaN; + return; +end +if ~isfield(well_entry, field_name) + value = NaN; + return; +end +value = well_entry.(field_name); +end + +function well_entry = get_well_entry(wellpara, time_index, well_index) +time_entry = wellpara{time_index}; + +if iscell(time_entry) + if isempty(time_entry) + well_entry = []; + return; + end + if well_index > numel(time_entry) + well_entry = []; + return; + end + well_entry = time_entry{well_index}; +elseif isstruct(time_entry) + if numel(time_entry) < well_index + well_entry = []; + return; + end + well_entry = time_entry(well_index); +else + well_entry = []; +end +end + function render_2d_layer(ax, config, results, options) r = results.r; output = results.OutputRs{get_option(options, 'time_step', numel(results.OutputRs))};