Add 2 files
Browse files- index.html +285 -36
- prompts.txt +2 -1
index.html
CHANGED
|
@@ -62,6 +62,23 @@
|
|
| 62 |
transform: translateY(-1px);
|
| 63 |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
|
| 64 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
</style>
|
| 66 |
</head>
|
| 67 |
<body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen">
|
|
@@ -70,7 +87,7 @@
|
|
| 70 |
<div class="text-center mb-10">
|
| 71 |
<h1 class="text-4xl font-bold text-gray-800 mb-3">Decline Curve Analysis</h1>
|
| 72 |
<p class="text-lg text-gray-600 max-w-3xl mx-auto">
|
| 73 |
-
Enter current well information and upload your production data in Excel format to analyze production decline and forecast future performance.
|
| 74 |
Supports exponential, hyperbolic, and harmonic decline models.
|
| 75 |
</p>
|
| 76 |
</div>
|
|
@@ -135,13 +152,57 @@
|
|
| 135 |
</div>
|
| 136 |
</div>
|
| 137 |
|
| 138 |
-
<!-- Upload Section -->
|
| 139 |
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-4xl mx-auto transform hover:shadow-2xl transition-shadow duration-300">
|
| 140 |
<div class="flex items-center mb-6 text-blue-700">
|
| 141 |
<i class="fas fa-chart-line text-2xl mr-3"></i>
|
| 142 |
-
<h2 class="text-2xl font-semibold">Upload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
</div>
|
| 144 |
|
|
|
|
| 145 |
<div id="dropZone" class="upload-area mb-6">
|
| 146 |
<i class="fas fa-cloud-upload-alt text-5xl text-gray-400 mb-4"></i>
|
| 147 |
<h3 class="text-xl font-medium text-gray-700 mb-2">Drag & Drop Excel File</h3>
|
|
@@ -159,9 +220,12 @@
|
|
| 159 |
|
| 160 |
<!-- Results Section -->
|
| 161 |
<div id="resultsSection" class="hidden bg-white rounded-2xl shadow-xl p-8 max-w-6xl mx-auto">
|
| 162 |
-
<div class="flex items-center
|
| 163 |
-
<
|
| 164 |
-
|
|
|
|
|
|
|
|
|
|
| 165 |
</div>
|
| 166 |
|
| 167 |
<!-- Key Metrics Row -->
|
|
@@ -213,12 +277,8 @@
|
|
| 213 |
<div class="mb-6">
|
| 214 |
<div class="flex justify-between items-center mb-4">
|
| 215 |
<h3 class="text-xl font-semibold text-gray-800">Production Rate vs Time</h3>
|
| 216 |
-
<div class="flex space-x-2">
|
| 217 |
-
<
|
| 218 |
-
<option value="exponential">Exponential</option>
|
| 219 |
-
<option value="hyperbolic" selected>Hyperbolic</option>
|
| 220 |
-
<option value="harmonic">Harmonic</option>
|
| 221 |
-
</select>
|
| 222 |
<input type="number" id="forecastMonths" class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-sm text-center" value="24" min="1" max="120">
|
| 223 |
<span class="self-center text-sm text-gray-600">months</span>
|
| 224 |
</div>
|
|
@@ -257,6 +317,7 @@
|
|
| 257 |
<script>
|
| 258 |
// Global variables
|
| 259 |
let productionData = [];
|
|
|
|
| 260 |
let productionChart = null;
|
| 261 |
let currentAnalysis = null;
|
| 262 |
|
|
@@ -265,9 +326,14 @@
|
|
| 265 |
const fileInput = document.getElementById('fileInput');
|
| 266 |
const uploadBtn = document.getElementById('uploadBtn');
|
| 267 |
const resultsSection = document.getElementById('resultsSection');
|
| 268 |
-
const
|
| 269 |
const forecastMonths = document.getElementById('forecastMonths');
|
| 270 |
const exportBtn = document.getElementById('exportBtn');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
// Current well information input elements
|
| 273 |
const currentMonthInput = document.getElementById('currentMonthInput');
|
|
@@ -279,6 +345,13 @@
|
|
| 279 |
const cumulativeProductionEl = document.getElementById('cumulativeProduction');
|
| 280 |
const productionLifeEl = document.getElementById('productionLife');
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
// Event Listeners
|
| 283 |
dropZone.addEventListener('click', () => fileInput.click());
|
| 284 |
dropZone.addEventListener('dragover', handleDragOver);
|
|
@@ -286,10 +359,29 @@
|
|
| 286 |
dropZone.addEventListener('drop', handleDrop);
|
| 287 |
fileInput.addEventListener('change', handleFileSelect);
|
| 288 |
uploadBtn.addEventListener('click', processFile);
|
| 289 |
-
declineModel.addEventListener('change', updateChart);
|
| 290 |
forecastMonths.addEventListener('change', updateChart);
|
| 291 |
exportBtn.addEventListener('click', exportResults);
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
// Update the current performance display when manual inputs change
|
| 294 |
currentMonthInput.addEventListener('change', updateManualCurrentPerformance);
|
| 295 |
currentProductionInput.addEventListener('input', updateManualCurrentPerformance);
|
|
@@ -299,6 +391,18 @@
|
|
| 299 |
const now = new Date();
|
| 300 |
const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' });
|
| 301 |
currentMonthEl.textContent = dateString;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
});
|
| 303 |
|
| 304 |
// Update current performance display with manual inputs
|
|
@@ -383,6 +487,12 @@
|
|
| 383 |
// Sort by date
|
| 384 |
productionData.sort((a, b) => a.timeMonths - b.timeMonths);
|
| 385 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
// Make sure current performance info is updated
|
| 387 |
if (currentMonthInput.value || currentProductionInput.value) {
|
| 388 |
updateManualCurrentPerformance();
|
|
@@ -391,8 +501,8 @@
|
|
| 391 |
// Display results
|
| 392 |
displayResults();
|
| 393 |
|
| 394 |
-
// Analyze decline curve
|
| 395 |
-
currentAnalysis = performDeclineAnalysis(
|
| 396 |
|
| 397 |
// Update analysis info
|
| 398 |
updateAnalysisInfo(currentAnalysis);
|
|
@@ -401,11 +511,14 @@
|
|
| 401 |
updateCurrentPerformance();
|
| 402 |
|
| 403 |
// Create chart
|
| 404 |
-
createChart(
|
| 405 |
|
| 406 |
// Show results section
|
| 407 |
resultsSection.classList.remove('hidden');
|
| 408 |
|
|
|
|
|
|
|
|
|
|
| 409 |
// Scroll to results
|
| 410 |
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
| 411 |
|
|
@@ -418,6 +531,107 @@
|
|
| 418 |
reader.readAsArrayBuffer(file);
|
| 419 |
}
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
function parseProductionData(jsonData) {
|
| 422 |
const parsedData = [];
|
| 423 |
let startDate = null;
|
|
@@ -469,15 +683,34 @@
|
|
| 469 |
return parsedData;
|
| 470 |
}
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
function performDeclineAnalysis(data) {
|
| 473 |
// Simple decline curve analysis using hyperbolic decline
|
| 474 |
// q(t) = qi / (1 + b * Di * t)^(1/b)
|
| 475 |
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
| 478 |
|
| 479 |
// Simple estimation of decline parameters
|
| 480 |
-
// This is a basic implementation - in reality, you'd use non-linear regression
|
| 481 |
let Di = 0; // Nominal decline rate
|
| 482 |
let b = 0.5; // Decline exponent (b=0 exponential, b=1 harmonic, 0<b<1 hyperbolic)
|
| 483 |
let rSquared = 0;
|
|
@@ -487,14 +720,14 @@
|
|
| 487 |
const qf = data[data.length - 1].rate;
|
| 488 |
|
| 489 |
// For hyperbolic decline, we'll make a simple estimation
|
| 490 |
-
const avgDecline = (qi - qf) / qi / productionMonths;
|
| 491 |
-
Di = avgDecline
|
| 492 |
|
| 493 |
// Adjust b factor based on the shape of the decline
|
| 494 |
-
|
| 495 |
-
if (
|
| 496 |
b = 0.3; // Steeper decline
|
| 497 |
-
} else if (
|
| 498 |
b = 0.5;
|
| 499 |
} else {
|
| 500 |
b = 0.8; // Gentle decline
|
|
@@ -504,6 +737,16 @@
|
|
| 504 |
b = 0.5;
|
| 505 |
}
|
| 506 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
// Calculate R-squared (very simplified)
|
| 508 |
let ssRes = 0;
|
| 509 |
let ssTot = 0;
|
|
@@ -515,7 +758,7 @@
|
|
| 515 |
ssTot += Math.pow(point.rate - yMean, 2);
|
| 516 |
});
|
| 517 |
|
| 518 |
-
rSquared = 1 - (ssRes /
|
| 519 |
|
| 520 |
return { qi, Di, b, rSquared };
|
| 521 |
}
|
|
@@ -582,7 +825,7 @@
|
|
| 582 |
const tableBody = document.getElementById('tableBody');
|
| 583 |
tableBody.innerHTML = '';
|
| 584 |
|
| 585 |
-
productionData.forEach(point => {
|
| 586 |
const row = document.createElement('tr');
|
| 587 |
row.innerHTML = `
|
| 588 |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.date.toLocaleDateString()}</td>
|
|
@@ -595,6 +838,8 @@
|
|
| 595 |
}
|
| 596 |
|
| 597 |
function updateAnalysisInfo(analysis) {
|
|
|
|
|
|
|
| 598 |
document.getElementById('qiValue').textContent = `${analysis.qi.toFixed(2)} BOPD`;
|
| 599 |
document.getElementById('diValue').textContent = `${(analysis.Di * 100).toFixed(1)}%/yr`;
|
| 600 |
document.getElementById('bValue').textContent = analysis.b.toFixed(2);
|
|
@@ -602,6 +847,8 @@
|
|
| 602 |
}
|
| 603 |
|
| 604 |
function createChart(data, analysis) {
|
|
|
|
|
|
|
| 605 |
const ctx = document.getElementById('productionChart').getContext('2d');
|
| 606 |
|
| 607 |
// Destroy existing chart if it exists
|
|
@@ -609,18 +856,16 @@
|
|
| 609 |
productionChart.destroy();
|
| 610 |
}
|
| 611 |
|
| 612 |
-
const
|
| 613 |
const actualRates = data.map(d => d.rate);
|
| 614 |
|
| 615 |
// Generate forecast data
|
| 616 |
-
const maxTime = Math.max(...
|
| 617 |
const forecastTime = [];
|
| 618 |
const forecastRates = [];
|
| 619 |
|
| 620 |
const forecastPeriod = parseInt(forecastMonths.value);
|
| 621 |
-
const
|
| 622 |
-
|
| 623 |
-
const b = model === 'exponential' ? 0 : (model === 'harmonic' ? 1 : analysis.b);
|
| 624 |
|
| 625 |
// Create data points for the chart
|
| 626 |
for (let t = 0; t <= maxTime + forecastPeriod; t += 0.5) {
|
|
@@ -737,8 +982,11 @@
|
|
| 737 |
}
|
| 738 |
|
| 739 |
function updateChart() {
|
| 740 |
-
if (productionData.length > 0 && currentAnalysis) {
|
| 741 |
-
|
|
|
|
|
|
|
|
|
|
| 742 |
}
|
| 743 |
}
|
| 744 |
|
|
@@ -751,7 +999,7 @@
|
|
| 751 |
// Create a worksheet
|
| 752 |
const ws_data = [
|
| 753 |
['Date', 'Rate (BOPD)', 'Cumulative (STB)', 'Time (months)'],
|
| 754 |
-
...productionData.map(d => [d.date.toLocaleDateString(), d.rate, d.cumulative, d.timeMonths])
|
| 755 |
];
|
| 756 |
|
| 757 |
const ws = XLSX.utils.aoa_to_sheet(ws_data);
|
|
@@ -768,7 +1016,8 @@
|
|
| 768 |
['Current Month', currentMonthEl.textContent],
|
| 769 |
['Current Production', currentProductionEl.textContent],
|
| 770 |
['Cumulative Production', cumulativeProductionEl.textContent],
|
| 771 |
-
['Model Used',
|
|
|
|
| 772 |
['Forecast Period', `${forecastMonths.value} months`]
|
| 773 |
], { origin: -1 });
|
| 774 |
|
|
|
|
| 62 |
transform: translateY(-1px);
|
| 63 |
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
|
| 64 |
}
|
| 65 |
+
|
| 66 |
+
.filter-badge {
|
| 67 |
+
background: linear-gradient(90deg, #3b82f6, #60a5fa);
|
| 68 |
+
padding: 0.25rem 0.75rem;
|
| 69 |
+
border-radius: 9999px;
|
| 70 |
+
color: white;
|
| 71 |
+
font-size: 0.75rem;
|
| 72 |
+
font-weight: 600;
|
| 73 |
+
margin: 0.25rem;
|
| 74 |
+
display: inline-flex;
|
| 75 |
+
align-items: center;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.filter-badge i {
|
| 79 |
+
margin-right: 0.25rem;
|
| 80 |
+
font-size: 0.625rem;
|
| 81 |
+
}
|
| 82 |
</style>
|
| 83 |
</head>
|
| 84 |
<body class="bg-gradient-to-br from-slate-50 to-slate-100 min-h-screen">
|
|
|
|
| 87 |
<div class="text-center mb-10">
|
| 88 |
<h1 class="text-4xl font-bold text-gray-800 mb-3">Decline Curve Analysis</h1>
|
| 89 |
<p class="text-lg text-gray-600 max-w-3xl mx-auto">
|
| 90 |
+
Enter current well information, select the analysis model, specify date range for calculations, and upload your production data in Excel format to analyze production decline and forecast future performance.
|
| 91 |
Supports exponential, hyperbolic, and harmonic decline models.
|
| 92 |
</p>
|
| 93 |
</div>
|
|
|
|
| 152 |
</div>
|
| 153 |
</div>
|
| 154 |
|
| 155 |
+
<!-- Upload Section with Advanced Controls -->
|
| 156 |
<div class="bg-white rounded-2xl shadow-xl p-8 mb-8 max-w-4xl mx-auto transform hover:shadow-2xl transition-shadow duration-300">
|
| 157 |
<div class="flex items-center mb-6 text-blue-700">
|
| 158 |
<i class="fas fa-chart-line text-2xl mr-3"></i>
|
| 159 |
+
<h2 class="text-2xl font-semibold">Upload & Configure Analysis</h2>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<!-- Analysis Configuration Section -->
|
| 163 |
+
<div class="bg-indigo-50 p-6 rounded-xl border border-indigo-200 mb-8">
|
| 164 |
+
<h3 class="text-lg font-semibold text-indigo-800 mb-4 flex items-center">
|
| 165 |
+
<i class="fas fa-cogs mr-2"></i> Analysis Configuration
|
| 166 |
+
</h3>
|
| 167 |
+
|
| 168 |
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 169 |
+
<div>
|
| 170 |
+
<label for="declineModelSelect" class="block text-sm font-medium text-gray-700 mb-2">Decline Curve Model</label>
|
| 171 |
+
<div class="relative">
|
| 172 |
+
<select id="declineModelSelect" class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 appearance-none bg-white">
|
| 173 |
+
<option value="hyperbolic" selected>Hyperbolic Decline</option>
|
| 174 |
+
<option value="exponential">Exponential Decline</option>
|
| 175 |
+
<option value="harmonic">Harmonic Decline</option>
|
| 176 |
+
</select>
|
| 177 |
+
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-3 text-gray-700">
|
| 178 |
+
<i class="fas fa-chevron-down"></i>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
<p id="modelDescription" class="mt-2 text-sm text-gray-600">
|
| 182 |
+
Hyperbolic decline provides the most flexible modeling of production decline patterns.
|
| 183 |
+
</p>
|
| 184 |
+
</div>
|
| 185 |
+
|
| 186 |
+
<div>
|
| 187 |
+
<label class="block text-sm font-medium text-gray-700 mb-2">Date Range for Analysis</label>
|
| 188 |
+
<div class="grid grid-cols-2 gap-2">
|
| 189 |
+
<div>
|
| 190 |
+
<label for="startDateFilter" class="block text-xs text-gray-500 mb-1">From</label>
|
| 191 |
+
<input type="date" id="startDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
| 192 |
+
</div>
|
| 193 |
+
<div>
|
| 194 |
+
<label for="endDateFilter" class="block text-xs text-gray-500 mb-1">To</label>
|
| 195 |
+
<input type="date" id="endDateFilter" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500">
|
| 196 |
+
</div>
|
| 197 |
+
</div>
|
| 198 |
+
<div id="dateFilterStatus" class="mt-2 text-sm text-gray-500">
|
| 199 |
+
Use date filters to analyze specific production periods.
|
| 200 |
+
</div>
|
| 201 |
+
</div>
|
| 202 |
+
</div>
|
| 203 |
</div>
|
| 204 |
|
| 205 |
+
<!-- Upload Section -->
|
| 206 |
<div id="dropZone" class="upload-area mb-6">
|
| 207 |
<i class="fas fa-cloud-upload-alt text-5xl text-gray-400 mb-4"></i>
|
| 208 |
<h3 class="text-xl font-medium text-gray-700 mb-2">Drag & Drop Excel File</h3>
|
|
|
|
| 220 |
|
| 221 |
<!-- Results Section -->
|
| 222 |
<div id="resultsSection" class="hidden bg-white rounded-2xl shadow-xl p-8 max-w-6xl mx-auto">
|
| 223 |
+
<div class="flex items-center justify-between mb-6">
|
| 224 |
+
<div class="flex items-center text-green-700">
|
| 225 |
+
<i class="fas fa-check-circle text-2xl mr-3"></i>
|
| 226 |
+
<h2 class="text-2xl font-semibold">Analysis Results</h2>
|
| 227 |
+
</div>
|
| 228 |
+
<div id="activeFilters" class="flex flex-wrap"></div>
|
| 229 |
</div>
|
| 230 |
|
| 231 |
<!-- Key Metrics Row -->
|
|
|
|
| 277 |
<div class="mb-6">
|
| 278 |
<div class="flex justify-between items-center mb-4">
|
| 279 |
<h3 class="text-xl font-semibold text-gray-800">Production Rate vs Time</h3>
|
| 280 |
+
<div class="flex items-center space-x-2">
|
| 281 |
+
<span class="text-sm text-gray-600">Forecast:</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
<input type="number" id="forecastMonths" class="w-20 px-3 py-2 border border-gray-300 rounded-lg text-sm text-center" value="24" min="1" max="120">
|
| 283 |
<span class="self-center text-sm text-gray-600">months</span>
|
| 284 |
</div>
|
|
|
|
| 317 |
<script>
|
| 318 |
// Global variables
|
| 319 |
let productionData = [];
|
| 320 |
+
let filteredData = [];
|
| 321 |
let productionChart = null;
|
| 322 |
let currentAnalysis = null;
|
| 323 |
|
|
|
|
| 326 |
const fileInput = document.getElementById('fileInput');
|
| 327 |
const uploadBtn = document.getElementById('uploadBtn');
|
| 328 |
const resultsSection = document.getElementById('resultsSection');
|
| 329 |
+
const declineModelSelect = document.getElementById('declineModelSelect');
|
| 330 |
const forecastMonths = document.getElementById('forecastMonths');
|
| 331 |
const exportBtn = document.getElementById('exportBtn');
|
| 332 |
+
const modelDescription = document.getElementById('modelDescription');
|
| 333 |
+
const startDateFilter = document.getElementById('startDateFilter');
|
| 334 |
+
const endDateFilter = document.getElementById('endDateFilter');
|
| 335 |
+
const dateFilterStatus = document.getElementById('dateFilterStatus');
|
| 336 |
+
const activeFilters = document.getElementById('activeFilters');
|
| 337 |
|
| 338 |
// Current well information input elements
|
| 339 |
const currentMonthInput = document.getElementById('currentMonthInput');
|
|
|
|
| 345 |
const cumulativeProductionEl = document.getElementById('cumulativeProduction');
|
| 346 |
const productionLifeEl = document.getElementById('productionLife');
|
| 347 |
|
| 348 |
+
// Model descriptions
|
| 349 |
+
const modelDescriptions = {
|
| 350 |
+
'exponential': 'Exponential decline assumes a constant percentage decline rate over time. Suitable for pressure-dominated reservoirs.',
|
| 351 |
+
'hyperbolic': 'Hyperbolic decline provides the most flexibility, combining elements of both exponential and harmonic decline. Often the best fit for actual production data.',
|
| 352 |
+
'harmonic': 'Harmonic decline assumes the decline rate decreases proportionally with production rate. Suitable for boundary-dominated flow.'
|
| 353 |
+
};
|
| 354 |
+
|
| 355 |
// Event Listeners
|
| 356 |
dropZone.addEventListener('click', () => fileInput.click());
|
| 357 |
dropZone.addEventListener('dragover', handleDragOver);
|
|
|
|
| 359 |
dropZone.addEventListener('drop', handleDrop);
|
| 360 |
fileInput.addEventListener('change', handleFileSelect);
|
| 361 |
uploadBtn.addEventListener('click', processFile);
|
|
|
|
| 362 |
forecastMonths.addEventListener('change', updateChart);
|
| 363 |
exportBtn.addEventListener('click', exportResults);
|
| 364 |
|
| 365 |
+
// Model selection
|
| 366 |
+
declineModelSelect.addEventListener('change', function() {
|
| 367 |
+
modelDescription.textContent = modelDescriptions[this.value];
|
| 368 |
+
if (currentAnalysis) {
|
| 369 |
+
updateAnalysisParameters();
|
| 370 |
+
updateChart();
|
| 371 |
+
}
|
| 372 |
+
});
|
| 373 |
+
|
| 374 |
+
// Date filters
|
| 375 |
+
startDateFilter.addEventListener('change', function() {
|
| 376 |
+
applyDateFilter();
|
| 377 |
+
updateDateFilterStatus();
|
| 378 |
+
});
|
| 379 |
+
|
| 380 |
+
endDateFilter.addEventListener('change', function() {
|
| 381 |
+
applyDateFilter();
|
| 382 |
+
updateDateFilterStatus();
|
| 383 |
+
});
|
| 384 |
+
|
| 385 |
// Update the current performance display when manual inputs change
|
| 386 |
currentMonthInput.addEventListener('change', updateManualCurrentPerformance);
|
| 387 |
currentProductionInput.addEventListener('input', updateManualCurrentPerformance);
|
|
|
|
| 391 |
const now = new Date();
|
| 392 |
const dateString = now.toLocaleDateString('default', { month: 'short', year: 'numeric' });
|
| 393 |
currentMonthEl.textContent = dateString;
|
| 394 |
+
|
| 395 |
+
// Set default model description
|
| 396 |
+
modelDescription.textContent = modelDescriptions[declineModelSelect.value];
|
| 397 |
+
|
| 398 |
+
// Set date filter defaults to maximum range
|
| 399 |
+
const minDate = new Date();
|
| 400 |
+
minDate.setFullYear(minDate.getFullYear() - 10);
|
| 401 |
+
startDateFilter.min = minDate.toISOString().split('T')[0];
|
| 402 |
+
|
| 403 |
+
const maxDate = new Date();
|
| 404 |
+
maxDate.setFullYear(maxDate.getFullYear() + 1);
|
| 405 |
+
endDateFilter.max = maxDate.toISOString().split('T')[0];
|
| 406 |
});
|
| 407 |
|
| 408 |
// Update current performance display with manual inputs
|
|
|
|
| 487 |
// Sort by date
|
| 488 |
productionData.sort((a, b) => a.timeMonths - b.timeMonths);
|
| 489 |
|
| 490 |
+
// Set date filter min/max values based on data
|
| 491 |
+
setFilterDateBounds();
|
| 492 |
+
|
| 493 |
+
// Apply any existing filters
|
| 494 |
+
applyDateFilter();
|
| 495 |
+
|
| 496 |
// Make sure current performance info is updated
|
| 497 |
if (currentMonthInput.value || currentProductionInput.value) {
|
| 498 |
updateManualCurrentPerformance();
|
|
|
|
| 501 |
// Display results
|
| 502 |
displayResults();
|
| 503 |
|
| 504 |
+
// Analyze decline curve with selected model
|
| 505 |
+
currentAnalysis = performDeclineAnalysis(filteredData);
|
| 506 |
|
| 507 |
// Update analysis info
|
| 508 |
updateAnalysisInfo(currentAnalysis);
|
|
|
|
| 511 |
updateCurrentPerformance();
|
| 512 |
|
| 513 |
// Create chart
|
| 514 |
+
createChart(filteredData, currentAnalysis);
|
| 515 |
|
| 516 |
// Show results section
|
| 517 |
resultsSection.classList.remove('hidden');
|
| 518 |
|
| 519 |
+
// Show active filters if any
|
| 520 |
+
updateActiveFiltersDisplay();
|
| 521 |
+
|
| 522 |
// Scroll to results
|
| 523 |
resultsSection.scrollIntoView({ behavior: 'smooth' });
|
| 524 |
|
|
|
|
| 531 |
reader.readAsArrayBuffer(file);
|
| 532 |
}
|
| 533 |
|
| 534 |
+
function setFilterDateBounds() {
|
| 535 |
+
if (productionData.length === 0) return;
|
| 536 |
+
|
| 537 |
+
// Set min/max for date filters based on data
|
| 538 |
+
const minDate = new Date(productionData[0].date);
|
| 539 |
+
const maxDate = new Date(productionData[productionData.length - 1].date);
|
| 540 |
+
|
| 541 |
+
startDateFilter.min = minDate.toISOString().split('T')[0];
|
| 542 |
+
startDateFilter.max = maxDate.toISOString().split('T')[0];
|
| 543 |
+
startDateFilter.value = minDate.toISOString().split('T')[0];
|
| 544 |
+
|
| 545 |
+
endDateFilter.min = minDate.toISOString().split('T')[0];
|
| 546 |
+
endDateFilter.max = maxDate.toISOString().split('T')[0];
|
| 547 |
+
endDateFilter.value = maxDate.toISOString().split('T')[0];
|
| 548 |
+
|
| 549 |
+
updateDateFilterStatus();
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
function applyDateFilter() {
|
| 553 |
+
if (productionData.length === 0) return;
|
| 554 |
+
|
| 555 |
+
const start = startDateFilter.value ? new Date(startDateFilter.value) : null;
|
| 556 |
+
const end = endDateFilter.value ? new Date(endDateFilter.value) : null;
|
| 557 |
+
|
| 558 |
+
// Apply filters
|
| 559 |
+
filteredData = productionData.filter(point => {
|
| 560 |
+
const pointDate = new Date(point.date);
|
| 561 |
+
const afterStart = !start || pointDate >= start;
|
| 562 |
+
const beforeEnd = !end || pointDate <= end;
|
| 563 |
+
return afterStart && beforeEnd;
|
| 564 |
+
});
|
| 565 |
+
|
| 566 |
+
// Reset time months based on filtered data
|
| 567 |
+
if (filteredData.length > 0) {
|
| 568 |
+
const firstDate = new Date(filteredData[0].date);
|
| 569 |
+
filteredData.forEach(point => {
|
| 570 |
+
point.timeMonths = (new Date(point.date) - firstDate) / (1000 * 60 * 60 * 24 * 30.44);
|
| 571 |
+
point.timeMonths = parseFloat(point.timeMonths.toFixed(2));
|
| 572 |
+
});
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
// Update analysis if we have new filtered data
|
| 576 |
+
if (currentAnalysis && filteredData.length > 0) {
|
| 577 |
+
currentAnalysis = performDeclineAnalysis(filteredData);
|
| 578 |
+
updateAnalysisInfo(currentAnalysis);
|
| 579 |
+
createChart(filteredData, currentAnalysis);
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
// Update display results
|
| 583 |
+
displayResults();
|
| 584 |
+
updateActiveFiltersDisplay();
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
function updateDateFilterStatus() {
|
| 588 |
+
const start = startDateFilter.value;
|
| 589 |
+
const end = endDateFilter.value;
|
| 590 |
+
|
| 591 |
+
if (!start && !end) {
|
| 592 |
+
dateFilterStatus.textContent = "No date filters applied. Analyzing all available data.";
|
| 593 |
+
dateFilterStatus.className = "mt-2 text-sm text-gray-500";
|
| 594 |
+
} else if (start && end) {
|
| 595 |
+
dateFilterStatus.textContent = `Analyzing data from ${start} to ${end}.`;
|
| 596 |
+
dateFilterStatus.className = "mt-2 text-sm text-green-600 font-medium";
|
| 597 |
+
} else if (start) {
|
| 598 |
+
dateFilterStatus.textContent = `Analyzing data from ${start} onwards.`;
|
| 599 |
+
dateFilterStatus.className = "mt-2 text-sm text-blue-600 font-medium";
|
| 600 |
+
} else if (end) {
|
| 601 |
+
dateFilterStatus.textContent = `Analyzing data up to ${end}.`;
|
| 602 |
+
dateFilterStatus.className = "mt-2 text-sm text-blue-600 font-medium";
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
function updateActiveFiltersDisplay() {
|
| 607 |
+
activeFilters.innerHTML = '';
|
| 608 |
+
|
| 609 |
+
const start = startDateFilter.value;
|
| 610 |
+
const end = endDateFilter.value;
|
| 611 |
+
const model = declineModelSelect.options[declineModelSelect.selectedIndex].text;
|
| 612 |
+
|
| 613 |
+
// Add model filter badge
|
| 614 |
+
const modelBadge = document.createElement('div');
|
| 615 |
+
modelBadge.className = 'filter-badge';
|
| 616 |
+
modelBadge.innerHTML = `<i class="fas fa-cogs"></i> ${model}`;
|
| 617 |
+
activeFilters.appendChild(modelBadge);
|
| 618 |
+
|
| 619 |
+
// Add date range badges if filters are active
|
| 620 |
+
if (start) {
|
| 621 |
+
const startBadge = document.createElement('div');
|
| 622 |
+
startBadge.className = 'filter-badge';
|
| 623 |
+
startBadge.innerHTML = `<i class="fas fa-calendar"></i> From: ${new Date(start).toLocaleDateString()}`;
|
| 624 |
+
activeFilters.appendChild(startBadge);
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
if (end) {
|
| 628 |
+
const endBadge = document.createElement('div');
|
| 629 |
+
endBadge.className = 'filter-badge';
|
| 630 |
+
endBadge.innerHTML = `<i class="fas fa-calendar"></i> To: ${new Date(end).toLocaleDateString()}`;
|
| 631 |
+
activeFilters.appendChild(endBadge);
|
| 632 |
+
}
|
| 633 |
+
}
|
| 634 |
+
|
| 635 |
function parseProductionData(jsonData) {
|
| 636 |
const parsedData = [];
|
| 637 |
let startDate = null;
|
|
|
|
| 683 |
return parsedData;
|
| 684 |
}
|
| 685 |
|
| 686 |
+
function updateAnalysisParameters() {
|
| 687 |
+
// Update b factor based on selected model
|
| 688 |
+
switch(declineModelSelect.value) {
|
| 689 |
+
case 'exponential':
|
| 690 |
+
currentAnalysis.b = 0;
|
| 691 |
+
break;
|
| 692 |
+
case 'harmonic':
|
| 693 |
+
currentAnalysis.b = 1;
|
| 694 |
+
break;
|
| 695 |
+
case 'hyperbolic':
|
| 696 |
+
// Keep calculated value or default to 0.5
|
| 697 |
+
if (currentAnalysis.b === 0 || currentAnalysis.b === 1) {
|
| 698 |
+
currentAnalysis.b = 0.5;
|
| 699 |
+
}
|
| 700 |
+
break;
|
| 701 |
+
}
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
function performDeclineAnalysis(data) {
|
| 705 |
// Simple decline curve analysis using hyperbolic decline
|
| 706 |
// q(t) = qi / (1 + b * Di * t)^(1/b)
|
| 707 |
|
| 708 |
+
if (data.length === 0) return null;
|
| 709 |
+
|
| 710 |
+
const qi = data[0].rate; // Initial rate from first point in filtered data
|
| 711 |
+
const productionMonths = data.length > 1 ? (data[data.length - 1].timeMonths - data[0].timeMonths) : 0;
|
| 712 |
|
| 713 |
// Simple estimation of decline parameters
|
|
|
|
| 714 |
let Di = 0; // Nominal decline rate
|
| 715 |
let b = 0.5; // Decline exponent (b=0 exponential, b=1 harmonic, 0<b<1 hyperbolic)
|
| 716 |
let rSquared = 0;
|
|
|
|
| 720 |
const qf = data[data.length - 1].rate;
|
| 721 |
|
| 722 |
// For hyperbolic decline, we'll make a simple estimation
|
| 723 |
+
const avgDecline = (qi - qf) / qi / (productionMonths/12); // Annual decline rate
|
| 724 |
+
Di = Math.abs(avgDecline);
|
| 725 |
|
| 726 |
// Adjust b factor based on the shape of the decline
|
| 727 |
+
const declineRatio = qf / qi;
|
| 728 |
+
if (declineRatio > 0.5) {
|
| 729 |
b = 0.3; // Steeper decline
|
| 730 |
+
} else if (declineRatio > 0.3) {
|
| 731 |
b = 0.5;
|
| 732 |
} else {
|
| 733 |
b = 0.8; // Gentle decline
|
|
|
|
| 737 |
b = 0.5;
|
| 738 |
}
|
| 739 |
|
| 740 |
+
// Apply model-specific constraints
|
| 741 |
+
switch(declineModelSelect.value) {
|
| 742 |
+
case 'exponential':
|
| 743 |
+
b = 0;
|
| 744 |
+
break;
|
| 745 |
+
case 'harmonic':
|
| 746 |
+
b = 1;
|
| 747 |
+
break;
|
| 748 |
+
}
|
| 749 |
+
|
| 750 |
// Calculate R-squared (very simplified)
|
| 751 |
let ssRes = 0;
|
| 752 |
let ssTot = 0;
|
|
|
|
| 758 |
ssTot += Math.pow(point.rate - yMean, 2);
|
| 759 |
});
|
| 760 |
|
| 761 |
+
rSquared = ssTot > 0 ? 1 - (ssRes / ssTot) : 0;
|
| 762 |
|
| 763 |
return { qi, Di, b, rSquared };
|
| 764 |
}
|
|
|
|
| 825 |
const tableBody = document.getElementById('tableBody');
|
| 826 |
tableBody.innerHTML = '';
|
| 827 |
|
| 828 |
+
(filteredData.length > 0 ? filteredData : productionData).forEach(point => {
|
| 829 |
const row = document.createElement('tr');
|
| 830 |
row.innerHTML = `
|
| 831 |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-700">${point.date.toLocaleDateString()}</td>
|
|
|
|
| 838 |
}
|
| 839 |
|
| 840 |
function updateAnalysisInfo(analysis) {
|
| 841 |
+
if (!analysis) return;
|
| 842 |
+
|
| 843 |
document.getElementById('qiValue').textContent = `${analysis.qi.toFixed(2)} BOPD`;
|
| 844 |
document.getElementById('diValue').textContent = `${(analysis.Di * 100).toFixed(1)}%/yr`;
|
| 845 |
document.getElementById('bValue').textContent = analysis.b.toFixed(2);
|
|
|
|
| 847 |
}
|
| 848 |
|
| 849 |
function createChart(data, analysis) {
|
| 850 |
+
if (!analysis || data.length === 0) return;
|
| 851 |
+
|
| 852 |
const ctx = document.getElementById('productionChart').getContext('2d');
|
| 853 |
|
| 854 |
// Destroy existing chart if it exists
|
|
|
|
| 856 |
productionChart.destroy();
|
| 857 |
}
|
| 858 |
|
| 859 |
+
const actualTime = data.map(d => d.timeMonths);
|
| 860 |
const actualRates = data.map(d => d.rate);
|
| 861 |
|
| 862 |
// Generate forecast data
|
| 863 |
+
const maxTime = Math.max(...actualTime);
|
| 864 |
const forecastTime = [];
|
| 865 |
const forecastRates = [];
|
| 866 |
|
| 867 |
const forecastPeriod = parseInt(forecastMonths.value);
|
| 868 |
+
const b = analysis.b; // Use the b value from analysis which is set by the model selection
|
|
|
|
|
|
|
| 869 |
|
| 870 |
// Create data points for the chart
|
| 871 |
for (let t = 0; t <= maxTime + forecastPeriod; t += 0.5) {
|
|
|
|
| 982 |
}
|
| 983 |
|
| 984 |
function updateChart() {
|
| 985 |
+
if ((filteredData.length > 0 ? filteredData : productionData).length > 0 && currentAnalysis) {
|
| 986 |
+
updateAnalysisParameters();
|
| 987 |
+
currentAnalysis = performDeclineAnalysis(filteredData.length > 0 ? filteredData : productionData);
|
| 988 |
+
updateAnalysisInfo(currentAnalysis);
|
| 989 |
+
createChart(filteredData.length > 0 ? filteredData : productionData, currentAnalysis);
|
| 990 |
}
|
| 991 |
}
|
| 992 |
|
|
|
|
| 999 |
// Create a worksheet
|
| 1000 |
const ws_data = [
|
| 1001 |
['Date', 'Rate (BOPD)', 'Cumulative (STB)', 'Time (months)'],
|
| 1002 |
+
...(filteredData.length > 0 ? filteredData : productionData).map(d => [d.date.toLocaleDateString(), d.rate, d.cumulative, d.timeMonths])
|
| 1003 |
];
|
| 1004 |
|
| 1005 |
const ws = XLSX.utils.aoa_to_sheet(ws_data);
|
|
|
|
| 1016 |
['Current Month', currentMonthEl.textContent],
|
| 1017 |
['Current Production', currentProductionEl.textContent],
|
| 1018 |
['Cumulative Production', cumulativeProductionEl.textContent],
|
| 1019 |
+
['Model Used', declineModelSelect.options[declineModelSelect.selectedIndex].text],
|
| 1020 |
+
['Date Range', startDateFilter.value ? (endDateFilter.value ? `${startDateFilter.value} to ${endDateFilter.value}` : `From ${startDateFilter.value}`) : (endDateFilter.value ? `To ${endDateFilter.value}` : 'All data')],
|
| 1021 |
['Forecast Period', `${forecastMonths.value} months`]
|
| 1022 |
], { origin: -1 });
|
| 1023 |
|
prompts.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
tolong buatkan decline curve analisis untuk produksi sumur dengan melakukan upload excel dan di tampilkan dalam grafik
|
| 2 |
tolong tambahkan informasi untuk bulan dan produksi saat ini
|
| 3 |
-
tolong tambahkan input manual untuk bulan dan produksi saat ini sebagai informasi tambahan sebelum upload excel
|
|
|
|
|
|
| 1 |
tolong buatkan decline curve analisis untuk produksi sumur dengan melakukan upload excel dan di tampilkan dalam grafik
|
| 2 |
tolong tambahkan informasi untuk bulan dan produksi saat ini
|
| 3 |
+
tolong tambahkan input manual untuk bulan dan produksi saat ini sebagai informasi tambahan sebelum upload excel
|
| 4 |
+
tambahkan picklist untuk pemilihan model dca , dan tambahkan filter jika di lakukan perhitungan dari tanggal berapa ke tanggal berapa
|