Power System Platform  2026w10a-beta
Loading...
Searching...
No Matches
EMTElement.cpp
1#include "EMTElement.h"
2
3#include "../../forms/EMTElementForm.h"
4#include "../../utils/PropertiesData.h"
5#include "../../extLibs/fftw/fftw3.h"
6#include "../../elements/GCText.h"
7
8#include <wx/dcgraph.h>
9#include <wx/textfile.h>
10#include <wx/dir.h>
11#include <wx/process.h>
12#include <wx/wfstream.h>
13#include <wx/datstrm.h>
14
15EMTElement::EMTElement()
16{
17}
18
19EMTElement::EMTElement(wxString name)
20{
21 m_data.name = name;
22}
23
24EMTElement::~EMTElement()
25{
26}
27
29{
30 EMTElement* copy = new EMTElement();
31 *copy = *this;
32 return copy;
33}
34
35bool EMTElement::AddParent(Element* parent, wxPoint2DDouble position)
36{
37 if (parent) {
38 m_parentList.push_back(parent);
39 parent->AddChild(this);
40 wxPoint2DDouble parentPt =
41 parent->RotateAtPosition(position, -parent->GetAngle()); // Rotate click to horizontal position.
42 parentPt.m_y = parent->GetPosition().m_y; // Centralize on bus.
43 parentPt = parent->RotateAtPosition(parentPt, parent->GetAngle()); // Rotate back.
44
45 m_position = parentPt + wxPoint2DDouble(0.0, 100.0); // Shifts the position to the down of the bus.
46 m_width = 50;
47 m_height = 50;
48 m_rect = wxRect2DDouble(m_position.m_x - m_width / 2.0, m_position.m_y - m_height / 2.0, m_width, m_height);
49
50 m_pointList.push_back(parentPt);
51 m_pointList.push_back(GetSwitchPoint(parent, parentPt, m_position));
52 m_pointList.push_back(m_position + wxPoint2DDouble(0.0, -m_height / 2.0 - 10.0));
53 m_pointList.push_back(m_position + wxPoint2DDouble(0.0, -m_height / 2.0));
54
55 m_inserted = true;
56
57 wxRect2DDouble genRect(0, 0, 0, 0);
58 m_switchRect.push_back(genRect); // Push a general rectangle.
60
61 // Base data
62 auto data = static_cast<Bus*>(parent)->GetElectricalData();
63 m_data.puVoltage = data.voltage;
64 m_data.baseVoltage = GetValueFromUnit(data.nominalVoltage, data.nominalVoltageUnit);
65 m_data.frequency = data.stabFreq;
66
67 return true;
68 }
69 return false;
70}
71
72void EMTElement::DrawDC(wxPoint2DDouble translation, double scale, wxGraphicsContext* gc) const
73{
74 wxColour elementColour;
75 if (m_online) {
76 if (m_dynEvent)
77 elementColour = m_dynamicEventColour;
78 else
79 elementColour = m_onlineElementColour;
80 }
81 else
82 elementColour = m_offlineElementColour;
83
84 if (m_inserted) {
85
86 if (m_selected) {
87 gc->SetPen(wxPen(wxColour(m_selectionColour), 2 + m_borderSize * 2.0));
88 gc->SetBrush(*wxTRANSPARENT_BRUSH);
89
90 gc->StrokeLines(m_pointList.size(), &m_pointList[0]);
91
92 // Push the current matrix on stack.
93 gc->PushState();
94 // Rotate the matrix around the object position.
95 gc->Translate(m_position.m_x, m_position.m_y);
96 gc->Rotate(wxDegToRad(m_angle));
97 gc->Translate(-m_position.m_x, -m_position.m_y);
98
99 //gc->DrawRectangle(m_position.m_x - m_width / 2.0, m_position.m_y - m_height / 2.0, m_width, m_height);
100 gc->DrawRoundedRectangle(m_position.m_x - m_width / 2.0, m_position.m_y - m_height / 2.0, m_width, m_height, 10.0);
101
102 gc->PopState();
103
104 // Draw node selection.
105 gc->SetPen(*wxTRANSPARENT_PEN);
106 gc->SetBrush(wxBrush(wxColour(m_selectionColour)));
107 DrawDCCircle(m_pointList[0], 5.0 + m_borderSize / scale, 10, gc);
108 }
109 // Draw EMTElement (layer 2).
110 // Draw node.
111 gc->SetPen(*wxTRANSPARENT_PEN);
112 gc->SetBrush(wxBrush(wxColour(elementColour)));
113 DrawDCCircle(m_pointList[0], 5.0, 10, gc);
114
115 gc->SetPen(wxPen(wxColour(elementColour), 2));
116 gc->SetBrush(*wxTRANSPARENT_BRUSH);
117 gc->StrokeLines(m_pointList.size(), &m_pointList[0]);
118
119 DrawDCSwitches(gc);
120
121 // Push the current matrix on stack.
122 gc->PushState();
123 // Rotate the matrix around the object position.
124 gc->Translate(m_position.m_x, m_position.m_y);
125 gc->Rotate(wxDegToRad(m_angle));
126 gc->Translate(-m_position.m_x, -m_position.m_y);
127
128 gc->SetPen(wxPen(wxColour(elementColour), 2));
129 gc->SetBrush(*wxWHITE_BRUSH);
130 //gc->DrawRectangle(m_position.m_x - m_width / 2.0, m_position.m_y - m_height / 2.0, m_width, m_height);
131 gc->DrawRoundedRectangle(m_position.m_x - m_width / 2.0, m_position.m_y - m_height / 2.0, m_width, m_height, 10.0);
132
133 //gc->SetPen(*wxBLACK_PEN);
134 //wxFont font(10, wxFONTFAMILY_ROMAN, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD);
135 wxFont font;
136 font.SetFaceName(wxT("CMU Serif"));
137 font.SetPointSize(10);
138 font.MakeBold();
139 gc->SetFont(font, m_online ? wxColour(255, 60, 0) : m_offlineElementColour);
140 double textWidth, textHeight;
141 gc->GetTextExtent(_("EMT"), &textWidth, &textHeight);
142 gc->DrawText(_("EMT"), m_position.m_x - textWidth / 2.0, m_position.m_y - textHeight / 2.0 + 20.0);
143
144 wxGCDC* gcdc = new wxGCDC(gc);
145 gcdc->SetPen(wxPen(m_online ? wxColour(0, 60, 255) : m_offlineElementColour, 2));
146 std::vector<wxPoint> ptList;
147 for (double x = m_position.m_x - m_width / 2.0 + 2; x < (m_position.m_x + m_width / 2.0); x += (m_width - 4.0) / 6.0) {
148 ptList.emplace_back(x, m_position.m_y + std::sin((x - (m_position.m_x - m_width / 2.0 + 2)) / m_width * 6 * M_PI) * 20.0);
149 }
150 gcdc->DrawSpline(ptList.size(), ptList.data());
151
152 gc->PopState();
153 }
154}
155
156void EMTElement::DrawDC(wxPoint2DDouble translation, double scale, wxDC& dc) const
157{
158 wxColour elementColour;
159 if (m_online) {
160 if (m_dynEvent)
161 elementColour = m_dynamicEventColour;
162 else
163 elementColour = m_onlineElementColour;
164 }
165 else
166 elementColour = m_offlineElementColour;
167
168 std::vector<wxPoint> pointListInt;
169 for (auto& pt : m_pointList) {
170 pointListInt.emplace_back(static_cast<int>(pt.m_x), static_cast<int>(pt.m_y));
171 }
172
173 if (m_inserted) {
174
175 if (m_selected) {
176 dc.SetPen(wxPen(wxColour(m_selectionColour), 2 + m_borderSize * 2.0));
177 dc.SetBrush(*wxTRANSPARENT_BRUSH);
178
179 dc.DrawLines(pointListInt.size(), &pointListInt[0]);
180
181 DrawDCRoundedRectRotated(dc, m_position, m_width, m_height, 10.0, m_angle);
182
183 // Draw node selection.
184 dc.SetPen(*wxTRANSPARENT_PEN);
185 dc.SetBrush(wxBrush(wxColour(m_selectionColour)));
186 DrawDCCircle(m_pointList[0], 5.0 + m_borderSize / scale, dc);
187 }
188 // Draw EMTElement (layer 2).
189 // Draw node.
190 dc.SetPen(*wxTRANSPARENT_PEN);
191 dc.SetBrush(wxBrush(wxColour(elementColour)));
192 DrawDCCircle(pointListInt[0], 5.0, dc);
193
194 dc.SetPen(wxPen(wxColour(elementColour), 2));
195 dc.SetBrush(*wxTRANSPARENT_BRUSH);
196 dc.DrawLines(pointListInt.size(), &pointListInt[0]);
197
198 DrawDCSwitches(dc);
199
200 dc.SetPen(wxPen(wxColour(elementColour), 2));
201 dc.SetBrush(*wxWHITE_BRUSH);
202 DrawDCRoundedRectRotated(dc, m_position, m_width, m_height, 10.0, m_angle);
203
204 wxFont font;
205 font.SetFaceName(wxT("CMU Serif"));
206 font.SetPointSize(10);
207 font.MakeBold();
208 GCText gcText;
209 gcText.SetFont(font);
210 gcText.SetText(_("EMT"));
211 wxPoint pt = RotateAround(m_position + wxPoint2DDouble(0.0, 20.0), m_position, m_angle);
212 gcText.Draw(pt, gcText.GetWidth(), gcText.GetHeight(), dc, m_angle, m_online ? wxColour(255, 60, 0) : m_offlineElementColour);
213 //dc.SetFont(font);
214 //dc.SetTextForeground(m_online ? wxColour(255, 60, 0) : m_offlineElementColour);
215 //int textWidth, textHeight;
216 //dc.GetTextExtent(_("EMT"), &textWidth, &textHeight);
217 //wxPoint pt = RotateAround(wxPoint2DDouble(m_position.m_x, m_position.m_y - m_height / 2.0 + 20.0), m_position, m_angle);
218 //dc.DrawRotatedText(_("EMT"), pt.x, pt.y, -m_angle);
219
220 //wxGCDC* gcdc = new wxGCDC(gc);
221 dc.SetPen(wxPen(m_online ? wxColour(0, 60, 255) : m_offlineElementColour, 2));
222 std::vector<wxPoint> ptList;
223 for (double x = m_position.m_x - m_width / 2.0 + 2; x < (m_position.m_x + m_width / 2.0); x += (m_width - 4.0) / 6.0) {
224 wxPoint pt = RotateAround(wxPoint2DDouble(x, m_position.m_y + std::sin((x - (m_position.m_x - m_width / 2.0 + 2)) / m_width * 6 * M_PI) * 20.0), m_position, m_angle);
225 ptList.emplace_back(pt);
226 }
227 dc.DrawSpline(ptList.size(), ptList.data());
228 }
229}
230
231void EMTElement::Rotate(bool clockwise)
232{
233 double rotAngle = m_rotationAngle;
234 if (!clockwise) rotAngle = -m_rotationAngle;
235
236 m_angle += rotAngle;
237 if (m_angle >= 360 || m_angle <= -360) m_angle = 0.0;
238 m_pointList[2] = RotateAtPosition(m_pointList[2], rotAngle);
239 m_pointList[3] = RotateAtPosition(m_pointList[3], rotAngle);
240 UpdateSwitchesPosition();
241}
242
244{
245 menu.Append(ID_EDIT_ELEMENT, _("Edit Electromagnetic Transient"));
246 GeneralMenuItens(menu);
247 return true;
248}
249
251{
252 wxString tipText = m_data.name;
253 tipText += wxT("\n");
254
255 tipText += wxString::Format(_("\nP = %.5f p.u."), m_data.power.real());
256 tipText += wxString::Format(_("\nQ = %.5f p.u."), m_data.power.imag());
257 if (auto itCurrrent = m_data.currHarmonics.find(1); itCurrrent != m_data.currHarmonics.end())
258 tipText += wxString::Format(_("\nI = %.5f A"), std::abs(itCurrrent->second));
259
260 wxString harmonicsInfo = _("\n\nHarmonics info:");
261 bool hasHarmonics = false;
262 for (auto& [order, current] : m_data.currHarmonics) {
263 if (order != 1) {
264 hasHarmonics = true;
265 harmonicsInfo += wxString::Format(_("\nIh(%d): %.5f%s%.2f%s A"), order, std::abs(current), wxString(L'\u2220'), wxRadToDeg(std::arg(current)), wxString(L'\u00B0'));
266 }
267 }
268
269 if (hasHarmonics) tipText += harmonicsInfo;
270
271 return tipText;
272}
273
274bool EMTElement::ShowForm(wxWindow* parent, Element* element)
275{
276 EMTElementForm emtForm(parent, this);
277 emtForm.SetTitle(_("Electromagnetic Transient"));
278 emtForm.CenterOnParent();
279 if (emtForm.ShowModal() == wxID_OK) {
280 return true;
281 }
282 return false;
283}
284
285rapidxml::xml_node<>* EMTElement::SaveElement(rapidxml::xml_document<>& doc, rapidxml::xml_node<>* elementListNode)
286{
287 auto elementNode = XMLParser::AppendNode(doc, elementListNode, "EMTElement");
288 XMLParser::SetNodeAttribute(doc, elementNode, "ID", m_elementID);
289
290 SaveCADProperties(doc, elementNode);
291
292 auto electricalProp = XMLParser::AppendNode(doc, elementNode, "ElectricalProperties");
293 auto isOnline = XMLParser::AppendNode(doc, electricalProp, "IsOnline");
294 XMLParser::SetNodeValue(doc, isOnline, m_online);
295 auto name = XMLParser::AppendNode(doc, electricalProp, "Name");
296 XMLParser::SetNodeValue(doc, name, m_data.name);
297 auto atpFilePath = XMLParser::AppendNode(doc, electricalProp, "ATPFilePath");
298 XMLParser::SetNodeValue(doc, atpFilePath, m_data.atpFile.GetFullPath());
299 auto atpNodeName = XMLParser::AppendNode(doc, electricalProp, "ATPNodeName");
300 XMLParser::SetNodeValue(doc, atpNodeName, m_data.atpNodeName);
301 auto stepSize = XMLParser::AppendNode(doc, electricalProp, "StepSize");
302 XMLParser::SetNodeValue(doc, stepSize, m_data.stepSize);
303 auto cyclesToSS = XMLParser::AppendNode(doc, electricalProp, "CyclesToSS");
304 XMLParser::SetNodeValue(doc, cyclesToSS, m_data.cyclesToSS);
305 auto recordFrequency = XMLParser::AppendNode(doc, electricalProp, "RecordFrequency");
306 XMLParser::SetNodeValue(doc, recordFrequency, m_data.recordFrequency);
307 auto useMedianFilter = XMLParser::AppendNode(doc, electricalProp, "UseMedianFilter");
308 XMLParser::SetNodeValue(doc, useMedianFilter, m_data.useMedianFilter);
309 auto numMaxHarmonics = XMLParser::AppendNode(doc, electricalProp, "NumMaxHarmonics");
310 XMLParser::SetNodeValue(doc, numMaxHarmonics, m_data.numMaxHarmonics);
311
312 return elementNode;
313}
314
315bool EMTElement::OpenElement(rapidxml::xml_node<>* elementNode, std::vector<Element*> parentList)
316{
317 if (!OpenCADProperties(elementNode, parentList)) return false;
318
319 auto electricalProp = elementNode->first_node("ElectricalProperties");
320 if (!electricalProp) return false;
321
322 SetOnline(XMLParser::GetNodeValueInt(electricalProp, "IsOnline"));
323 m_data.name = electricalProp->first_node("Name")->value();
324 m_data.atpFile = wxFileName(electricalProp->first_node("ATPFilePath")->value());
325 m_data.atpNodeName = electricalProp->first_node("ATPNodeName")->value();
326 m_data.stepSize = XMLParser::GetNodeValueDouble(electricalProp, "StepSize");
327 m_data.cyclesToSS = XMLParser::GetNodeValueInt(electricalProp, "CyclesToSS");
328 m_data.recordFrequency = XMLParser::GetNodeValueInt(electricalProp, "RecordFrequency");
329 m_data.useMedianFilter = XMLParser::GetNodeValueInt(electricalProp, "UseMedianFilter");
330 m_data.numMaxHarmonics = XMLParser::GetNodeValueInt(electricalProp, "NumMaxHarmonics");
331
332 m_inserted = true;
333 return true;
334}
335
336wxArrayString EMTElement::GetATPNodes(wxArrayString atpFile)
337{
338 wxArrayString nodeList;
339 uint32_t mode = 0;
340 std::vector<wxString> mode0Cards{ "/OUTPUT", "BLANK", "TACS", "INCLUDE", "TRANSFORMER" };
341 std::vector<wxString> mode1Cards{ "/SOURCE" };
342 std::vector<wxString> mode2Cards{ "/BRANCH", "/SWITCH", };
343 wxString ground = "000000";
344 for (wxString line : atpFile) {
345 if (line.IsEmpty()) continue;
346 if (tolower(line[0]) == 'c') continue; // Skip comments
347
348 bool cardHeader = false;
349 for (wxString card : mode0Cards) {
350 if (line.Find(card) != wxNOT_FOUND) {
351 mode = 0;
352 cardHeader = true;
353 break;
354 }
355 }
356 for (wxString card : mode1Cards) {
357 if (line.Find(card) != wxNOT_FOUND) {
358 mode = 1;
359 cardHeader = true;
360 break;
361 }
362 }
363 for (wxString card : mode2Cards) {
364 if (line.Find(card) != wxNOT_FOUND) {
365 mode = 2;
366 cardHeader = true;
367 break;
368 }
369 }
370 if (cardHeader) continue;
371
372
373 wxString node = "";
374 switch (mode)
375 {
376 case 1: {
377 node = line(2, 6).Trim();
378 node.Replace(" ", "0");
379 if (node != ground && node[5] >= 'A') { // Ignore ground and monophasic nodes
380 if (nodeList.Index(node(0, 5)) == wxNOT_FOUND)
381 nodeList.Add(node(0, 5));
382 }
383 } break;
384 case 2: {
385 node = line(2, 6);
386 if (node != ground && node[5] >= 'A') { // Ignore ground and monophasic nodes
387 if (nodeList.Index(node(0, 5)) == wxNOT_FOUND)
388 nodeList.Add(node(0, 5));
389 }
390 node = line(8, 6);
391 if (node != ground && node[5] >= 'A') { // Ignore ground and monophasic nodes
392 if (nodeList.Index(node(0, 5)) == wxNOT_FOUND)
393 nodeList.Add(node(0, 5));
394 }
395 } break;
396 }
397 }
398 return nodeList;
399}
400
401bool EMTElement::SetATPParameter(wxTextFile& atpFile, const wxString& card, const int& line, const int& initPos, const int& size, const wxString& value)
402{
403 bool foundCard = false;
404 int currLine = -1;
405 wxString lineStr = "";
406 lineStr = atpFile.GetFirstLine();
407 while (!atpFile.Eof())
408 {
409 if (tolower(lineStr[0]) == 'c') {
410 lineStr = atpFile.GetNextLine();
411 continue; // Skip comments
412 }
413 if (lineStr.Find(card) != wxNOT_FOUND) {
414 foundCard = true;
415 currLine = -1;
416 }
417 if (foundCard) currLine++;
418 if (foundCard && currLine == line) {
419 for (int i = initPos; i < initPos + size; i++) {
420 lineStr[i] = value[i - initPos];
421 }
422 atpFile.RemoveLine(atpFile.GetCurrentLine());
423 atpFile.InsertLine(lineStr, atpFile.GetCurrentLine());
424 return true;
425 }
426
427 lineStr = atpFile.GetNextLine();
428 }
429 return false;
430}
431
432bool EMTElement::AddConnectionToNode(wxTextFile& atpFile, const wxString& node)
433{
434 // Get connnected bus data
435 bool hasBusData = false;
436 BusElectricalData busData;
437 if (!m_parentList.empty()) {
438 if (m_parentList[0] != nullptr) {
439 Bus* bus = static_cast<Bus*>(m_parentList[0]);
440 busData = bus->GetElectricalData();
441 hasBusData = true;
442 }
443 }
444
445 wxString switchMask = " PSPNC%c%s%c MEASURING %d";
446 wxString sourceMask = "14PSPNC%c %s%s%s -1. 100.";
447 wxString lineStr = "";
448 int outCardPos = -1, switchCardPos = -1, sourceCardPos = -1;
449 lineStr = atpFile.GetFirstLine();
450 while (!atpFile.Eof())
451 {
452 if (lineStr.IsEmpty()) {
453 lineStr = atpFile.GetNextLine();
454 continue;
455 }
456 if (tolower(lineStr[0]) == 'c') {
457 lineStr = atpFile.GetNextLine();
458 continue; // Skip comments
459 }
460
461 if (lineStr.Find("/SWITCH") != wxNOT_FOUND)
462 switchCardPos = atpFile.GetCurrentLine() + 1;
463
464 if (lineStr.Find("/SOURCE") != wxNOT_FOUND)
465 sourceCardPos = atpFile.GetCurrentLine() + 1;
466
467 if (lineStr.Find("/OUTPUT") != wxNOT_FOUND)
468 outCardPos = atpFile.GetCurrentLine();
469
470 lineStr = atpFile.GetNextLine();
471 }
472
473 if (outCardPos < 0) return false;
474
475 if (switchCardPos < 0 && outCardPos > 0) {
476 atpFile.InsertLine("/SWITCH", outCardPos);
477 switchCardPos = outCardPos + 1;
478 }
479
480 if (sourceCardPos < 0 && outCardPos > 0) {
481 atpFile.InsertLine("/SOURCE", outCardPos);
482 sourceCardPos = outCardPos + 1;
483 }
484
485 for (char i = 'A'; i <= 'C'; ++i) {
486 lineStr = wxString::Format(switchMask, i, node, i, i == 'A' ? 1 : 0);
487 atpFile.InsertLine(lineStr, switchCardPos + (i - 'A'));
488 }
489 sourceCardPos += 3;
490
491 double voltage = std::abs(m_data.puVoltage) * m_data.baseVoltage * (std::sqrt(2) / std::sqrt(3)); // phase-ground, peak value
492 wxString ampl = wxString::FromCDouble(voltage, 3); // Amplitude in pu
493 wxString freq = wxString::FromCDouble(m_data.frequency, 5); // Frequency in Hz
494 // Insert spaces before to complete 10 characters
495 while (freq.Length() < 10) freq = " " + freq;
496 while (ampl.Length() < 10) ampl = " " + ampl;
497
498 int cardPos = sourceCardPos;
499 for (char i = 'A'; i <= 'C'; ++i) {
500 wxString angle = wxString::FromCDouble(std::arg(m_data.puVoltage) * 180.0 / M_PI - 120.0 * (i - 'A'), 5); // Angle in degrees
501
502 // Insert spaces before to complete 10 characters
503 while (angle.Length() < 10) angle = " " + angle;
504
505 lineStr = wxString::Format(sourceMask, i, ampl, freq, angle);
506 atpFile.InsertLine(lineStr, cardPos);
507 cardPos++;
508
509 if (hasBusData) {
510 // Insert harmonic voltage
511 for (size_t j = 0; j < busData.harmonicOrder.size(); ++j) {
512 int order = busData.harmonicOrder[j];
513 if (order == 1) continue; // Skip fundamental
514
515 std::complex<double> harmVoltage = busData.harmonicVoltage[j];
516
517 voltage = std::abs(busData.harmonicVoltage[j]) * m_data.baseVoltage * (std::sqrt(2) / std::sqrt(3));
518 wxString amplH = wxString::FromCDouble(voltage, 3);
519 while (amplH.Length() < 10) amplH = " " + amplH;
520
521 wxString freqH = wxString::FromCDouble(m_data.frequency * static_cast<double>(order), 5);
522 while (freqH.Length() < 10) freqH = " " + freqH;
523
524 double angleValue = std::arg(harmVoltage) * 180.0 / M_PI;
525 if (order % 3 == 1) { // Positive sequence
526 angleValue += -120.0 * (i - 'A');
527 }
528 else if (order % 3 == 2) { // Negative sequence
529 angleValue += 120.0 * (i - 'A');
530 }
531 // Zero sequence doesn't change the angle because it's the same for all phases
532
533 wxString angleH = wxString::FromCDouble(angleValue, 5);
534 while (angleH.Length() < 10) angleH = " " + angleH;
535
536 lineStr = wxString::Format(sourceMask, i, amplH, freqH, angleH);
537 atpFile.InsertLine(lineStr, cardPos);
538 cardPos++;
539 }
540 }
541 }
542
543 return true;
544}
545
546std::vector<double> EMTElement::MedianFilter(const std::vector<double>& data)
547{
548 // Based on: http://www.librow.com/articles/article-1
549
550 size_t n = data.size();
551 std::vector<double> result(data);
552
553 // Check arguments
554 if (data.empty() || n < 1)
555 return result;
556 // Treat special case N = 1
557 if (n == 1)
558 {
559 result[0] = data[0];
560 return result;
561 }
562 // Allocate memory for signal extension
563 double* extension = new double[n + 4];
564 // Check memory allocation
565 //if (!extension)
566 // return;
567 // Create signal extension
568 memcpy(extension + 2, data.data(), n * sizeof(double));
569 for (int i = 0; i < 2; ++i)
570 {
571 extension[i] = data[1 - i];
572 extension[n + 2 + i] = data[n - 1 - i];
573 }
574 // Call median filter implementation
575 result = DoMedianFilter(extension, result, n + 4);
576 // Free memory
577 delete[] extension;
578
579 return result;
580}
581
582bool EMTElement::CalculateCurrent(wxString& errorMsg, const bool& saveFFTData)
583{
584 wxFileName fileName(m_data.atpFile);
585 if (!fileName.IsOk()) {
586 errorMsg = wxString::Format(_("Invalid ATP file path for the electromagnetic element \"%s\"."), m_data.name);
587 return false;
588 }
589
590 //double fundFreq = properties.GetSimulationPropertiesData().stabilityFrequency;
591 double fundFreq = m_data.frequency;
592 double timeSim = (1.0 / fundFreq) * static_cast<double>(m_data.cyclesToSS);
593
594 //wxString atpFolder = properties.GetGeneralPropertiesData().atpPath.GetPath();
595 wxString atpFolder = m_data.atpPath.GetPath();
596 // Set fundamental frequency to EMT element
597 //m_data.frequency = fundFreq;
598
599 wxExecuteEnv env;
600 env.cwd = atpFolder;
601
602 // Save the ATP file in ATP work folder
603 wxTextFile origFile(m_data.atpFile.GetFullPath());
604 //fileName.SetPath(properties.GetGeneralPropertiesData().atpWorkFolder);
605 fileName.SetPath(m_data.atpWorkFolder);
606 wxString name = fileName.GetFullPath();
607 wxTextFile copyFile(fileName.GetFullPath());
608 if (origFile.Open()) {
609 copyFile.Create();
610 for (size_t i = 0; i < origFile.GetLineCount(); i++) {
611 copyFile.AddLine(origFile.GetLine(i));
612 }
613 origFile.Close();
614
615 wxString stepSizeStr = wxString::Format("%.2E", m_data.stepSize);
616 stepSizeStr.Replace(wxT(","), wxT("."));
617 wxString timeStr = wxString::Format("%.6f", timeSim);
618 timeStr.Replace(wxT(","), wxT("."));
619 SetATPParameter(copyFile, wxT("BEGIN NEW DATA CASE"), 1, 0, 8, stepSizeStr);
620 SetATPParameter(copyFile, wxT("BEGIN NEW DATA CASE"), 1, 8, 8, timeStr);
621 SetATPParameter(copyFile, wxT("BEGIN NEW DATA CASE"), 2, 0, 8, wxT("99999999"));
622 SetATPParameter(copyFile, wxT("BEGIN NEW DATA CASE"), 2, 8, 8, wxString::Format("%8d", m_data.recordFrequency));
623
624 // Add source and switch to selectec node
625 AddConnectionToNode(copyFile, m_data.atpNodeName);
626
627 copyFile.Write();
628 }
629 else {
630 errorMsg = wxString::Format(_("Fail to open ATP file of the electromagnetic element \"%s\"."), m_data.name);
631 return false;
632 }
633
634 //wxString cmd = properties.GetGeneralPropertiesData().atpPath.GetFullPath() + wxT(" both ") + fileName.GetFullPath() + wxT(" s -R");
635 wxString cmd = m_data.atpPath.GetFullPath() + wxT(" both ") + fileName.GetFullPath() + wxT(" s -R");
636 wxExecute(cmd, wxEXEC_SYNC | wxEXEC_HIDE_CONSOLE | wxEXEC_NODISABLE, nullptr, &env);
637
638 // Delete all .tmp files
639 wxDir dir(atpFolder);
640 if (dir.IsOpened()) {
641 wxString file;
642 bool cont = dir.GetFirst(&file, wxT("*.tmp"), wxDIR_FILES);
643 while (cont) {
644 wxRemoveFile(atpFolder + wxT("/") + file);
645 cont = dir.GetNext(&file);
646 }
647 }
648
649 fileName.SetFullName(fileName.GetName() + wxT(".pl4"));
650 wxFileInputStream pl4File(fileName.GetFullPath());
651 // https://github.com/ldemattos/readPL4/wiki/PISA's-PL4-Format---Time-domain
652 if (pl4File.IsOk() && pl4File.GetSize() > 0) {
653 wxDataInputStream store(pl4File);
654 store.UseBasicPrecisions();
655 float* buffer = new float[2];
656 std::vector<double> timeVec;
657 std::vector<double> valueVec;
658 double oldTime = 0.0;
659 int read16Bytes = 0;
660 while (!pl4File.Eof()) {
661 store.ReadFloat(buffer, 2);
662 if (read16Bytes > 11 && (buffer[0] >= oldTime)) {
663 oldTime = buffer[0];
664 timeVec.emplace_back(buffer[0]);
665 valueVec.emplace_back(buffer[1]);
666 }
667 read16Bytes++;
668 }
669 delete[] buffer;
670
671 if (m_data.useMedianFilter)
672 valueVec = MedianFilter(valueVec);
673
674 // FFT
675 double dataStepSize = timeVec[1] - timeVec[0]; // Real sampling time from data
676 bool useRemainder = false;
677 size_t n = ceil(1.0 / (dataStepSize * fundFreq)); // Number of samples
678 // Due to ATP logic, the number of samples can be greater than the number of samples in the PL4 file
679 // In this case, I choose to follow the remainder approach
680 if (n > valueVec.size()) {
681 n = valueVec.size();
682 useRemainder = true;
683 }
684
685 double fs = 1.0 / dataStepSize; // Sampling frequency
686 double df = fs / static_cast<double>(n); // Frequency resolution
687
688 if (useRemainder) {
689 double rmder = abs(remainder(fundFreq, df));
690 int minRmderN = n;
691 while (rmder > 1e-3) {
692 n--;
693 df = fs / static_cast<double>(n);
694
695 if (abs(remainder(fundFreq, df)) < rmder) minRmderN = n;
696
697 rmder = abs(remainder(fundFreq, df));
698
699 if (n <= 0) {
700 n = minRmderN;
701 df = fs / static_cast<double>(n);
702 break;
703 }
704 }
705 }
706
707 double dtWindow = timeVec[timeVec.size() - 1] - timeVec[timeVec.size() - n];
708
709 fftw_complex* out;
710 double* in;
711 fftw_plan p;
712 out = (fftw_complex*)fftw_malloc(sizeof(fftw_complex) * (n / 2 + 1));
713 in = (double*)fftw_malloc(sizeof(double) * n);
714 // Populate input array
715 for (size_t i = (valueVec.size() - n); i < valueVec.size(); ++i) {
716 size_t index = i - (valueVec.size() - n);
717 in[index] = valueVec[i];
718 }
719
720 p = fftw_plan_dft_r2c_1d(n, in, out, FFTW_ESTIMATE);
721 fftw_execute(p); /* repeat as needed */
722 fftw_destroy_plan(p);
723
724 double ampCorrection = 2.0 / static_cast<double>(n);
725
726 // Identify fundamental frequency
727 int fundIndex = -1;
728 double fundMagnitude = 0.0;
729 for (size_t i = 0; i < (n / 2 + 1); i++) {
730 double freq = static_cast<double>(i) * df;
731 if (remainder(freq, fundFreq)) {
732 fundIndex = i;
733 fundMagnitude = sqrt(out[i][0] * out[i][0] + out[i][1] * out[i][1]) * ampCorrection;
734 break;
735 }
736 }
737 if (fundIndex < 0) {
738 errorMsg = wxString::Format(_("Fail to identify the fundamental frequency of the electromagnetic element \"%s\"."), m_data.name);
739 return false;
740 }
741
742 // Fundamental and harmonics
743 m_data.currHarmonics.clear();
744 //m_data.currHarmonicsOrder.clear();
745 for (size_t i = 0; i < (n / 2 + 1); i++) {
746 double freq = static_cast<double>(i) * df;
747 int order = static_cast<int>(round(freq / fundFreq));
748
749 if (order > m_data.numMaxHarmonics) break; // Stop if the order is greater than the maximum number of harmonics
750
751 double magnitude = sqrt(out[i][0] * out[i][0] + out[i][1] * out[i][1]) * ampCorrection;
752 if ((magnitude / fundMagnitude) > (m_data.harmonicsThreshold / 100.0)) {
753 //m_data.currHarmonics.emplace_back(std::complex<double>(out[i][0], out[i][1]) * ampCorrection);
754 //m_data.currHarmonicsOrder.emplace_back(order);
755 m_data.currHarmonics[order] = std::complex<double>(out[i][0], out[i][1]) * ampCorrection / sqrt(2.0); // RMS value
756 }
757 }
758
759 if (saveFFTData) {
760 m_data.atpData.clear();
761 m_data.inFFTData.clear();
762 m_data.outFFTData.clear();
763 for (size_t i = 0; i < valueVec.size(); i++) {
764 m_data.atpData.emplace_back(std::make_pair(timeVec[i], valueVec[i]));
765 }
766 for (size_t i = 0; i < n; i++) {
767 m_data.inFFTData.emplace_back(std::make_pair(timeVec[i + timeVec.size() - n], in[i]));
768 }
769 for (size_t i = 0; i < (n / 2 + 1); i++) {
770 std::complex<double> value(out[i][0] * ampCorrection, out[i][1] * ampCorrection);
771 double freq = static_cast<double>(i) * df;
772 m_data.outFFTData.emplace_back(std::make_pair(freq, value));
773 }
774 }
775
776 fftw_free(in);
777 fftw_free(out);
778
779 }
780 else {
781 wxString atpErrorMsg = _("No error found.");
782
783 fileName.SetFullName(fileName.GetName() + wxT(".lis"));
784 wxTextFile lisFile(fileName.GetFullPath());
785
786 if (!lisFile.Exists()) {
787 errorMsg = wxString::Format(_("Fail to run ATP file of the electromagnetic element \"%s\".\nThe ATP program did not return any error messages."), m_data.name);
788 return false;
789 }
790 if (lisFile.Open()) {
791 wxString line = lisFile.GetFirstLine() + "\n";
792 bool foundError = false;
793 int lineCount = -1;
794 atpErrorMsg.Clear();
795 while (!lisFile.Eof())
796 {
797 if (line.Find(wxT("KILL ")) != wxNOT_FOUND) {
798 foundError = true;
799 lineCount++;
800 }
801 if (line.Find(wxT("----------")) != wxNOT_FOUND) {
802 foundError = false;
803 }
804 if (foundError && lineCount == 1)
805 atpErrorMsg += line + " ";
806 line = lisFile.GetNextLine();
807 }
808 lisFile.Close();
809 }
810 else
811 {
812 errorMsg = wxString::Format(_("Fail to run ATP file of the electromagnetic element \"%s\".\nThe ATP program did not return any error messages."), m_data.name);
813 return false;
814 }
815
816 errorMsg = wxString::Format(_("Fail to run ATP file of the electromagnetic element \"%s\".\nThe ATP returned the following error message:\n\"%s\""), m_data.name, atpErrorMsg);
817 return false;
818 }
819 return true;
820}
821
822void EMTElement::UpdateData(const PropertiesData* properties, bool updateVoltageBase)
823{
824 if (properties != nullptr) {
825 m_data.frequency = properties->GetSimulationPropertiesData().stabilityFrequency;
826 m_data.atpPath = properties->GetGeneralPropertiesData().atpPath;
827 m_data.atpWorkFolder = properties->GetGeneralPropertiesData().atpWorkFolder;
828 }
829 if (!m_parentList.empty()) {
830 Bus* bus = static_cast<Bus*>(m_parentList[0]);
831 if (bus != nullptr) {
832 auto busData = bus->GetElectricalData();
833 std::complex<double> voltage = std::complex<double>(1.0, 0.0);
834 if (abs(busData.voltage) > 1e-3)
835 m_data.puVoltage = busData.voltage;
836 if (updateVoltageBase) {
837 m_data.baseVoltage = GetValueFromUnit(busData.nominalVoltage, busData.nominalVoltageUnit);
838 }
839 }
840 }
841
842}
843
844std::vector<double> EMTElement::DoMedianFilter(double* extension, std::vector<double>& result, const int& n)
845{
846 // Move window through all elements of the signal
847 for (int i = 2; i < n - 2; ++i)
848 {
849 // Pick up window elements
850 double window[5];
851 for (int j = 0; j < 5; ++j)
852 window[j] = extension[i - 2 + j];
853 // Order elements (only half of them)
854 for (int j = 0; j < 3; ++j)
855 {
856 // Find position of minimum element
857 int min = j;
858 for (int k = j + 1; k < 5; ++k)
859 if (window[k] < window[min])
860 min = k;
861 // Put found minimum element in its place
862 const double temp = window[j];
863 window[j] = window[min];
864 window[min] = temp;
865 }
866 // Get result - the middle element
867 result[i - 2] = window[2];
868 }
869 return result;
870}
@ ID_EDIT_ELEMENT
Definition Element.h:75
Node for power elements. All others power elements are connected through this.
Definition Bus.h:86
Element to connect ATP-EMTP.
Definition EMTElement.h:65
virtual void Rotate(bool clockwise=true)
Rotate the element.
virtual bool GetContextMenu(wxMenu &menu)
Get the element contex menu.
virtual void DrawDC(wxPoint2DDouble translation, double scale, wxGraphicsContext *gc) const
Draw the element using GDI+.
virtual wxString GetTipText() const
Get the tip text.
virtual bool AddParent(Element *parent, wxPoint2DDouble position)
Add a parent to the element. This method must be used on power elements that connect to a bus,...
virtual bool ShowForm(wxWindow *parent, Element *element)
Show element data form.
virtual Element * GetCopy()
Get a the element copy.
Base class of all elements of the program. This class is responsible for manage graphical and his dat...
Definition Element.h:112
virtual void GeneralMenuItens(wxMenu &menu)
Insert general itens to context menu.
Definition Element.cpp:457
wxPoint2DDouble GetPosition() const
Get the element position.
Definition Element.h:186
double GetAngle() const
Get the element angle.
Definition Element.h:211
virtual wxPoint2DDouble RotateAtPosition(wxPoint2DDouble pointToRotate, double angle, bool degrees=true) const
Rotate a point as element position being the origin.
Definition Element.cpp:292
virtual void AddChild(Element *child)
Add a child to the child list.
Definition Element.cpp:566
bool SetOnline(bool online=true)
Set if the element is online or offline.
Definition Element.cpp:447
virtual void DrawDCCircle(wxPoint2DDouble position, double radius, int numSegments, wxGraphicsContext *gc) const
Draw a circle using device context.
Definition Element.cpp:177
Class to draw text on Graphics Context using wxWidgets.
Definition GCText.h:32
virtual void Draw(wxPoint2DDouble position, wxGraphicsContext *gc, double angle=0.0, wxColour colour= *wxBLACK) const
Draw the text in wxGraphicsContext.
Definition GCText.cpp:35
virtual void SetText(wxString text)
Set correctly a new text string.
Definition GCText.cpp:68
virtual void DrawDCSwitches(wxGraphicsContext *gc) const
Draw switch.
virtual void UpdateSwitches()
Update the switch position.
virtual wxPoint2DDouble GetSwitchPoint(Element *parent, wxPoint2DDouble parentPoint, wxPoint2DDouble secondPoint) const
Get the correct switch position.
General and simulation data manager.